C++中“引用”的底层实现
原文鏈接:http://www.cnblogs.com/hoodlum1980/archive/2012/06/19/2554270.html
聲明】本文無(wú)技術(shù)含量!在博客園上回復(fù)某個(gè)帖子,招來(lái)他的非議,我不想去細(xì)究這個(gè)人的治學(xué)態(tài)度,不想去問(wèn)去管他到底有沒(méi)有修改過(guò)自己的文章,對(duì)我來(lái)說(shuō)沒(méi)必要。我只能說(shuō)不負(fù)責(zé)任,態(tài)度自大的,不嚴(yán)謹(jǐn)?shù)娜耸橇钗沂摹5菍?duì)于一個(gè)問(wèn)題,這里涉及到了“引用”,這是C++引入的一種新的形式,可以說(shuō)是給程序員的一個(gè)語(yǔ)法上的好處,但是我翻看了BS的《The C++ Programming Lanuage》,并沒(méi)有看到對(duì)引用的實(shí)現(xiàn)的解釋。所以雖然我一直默認(rèn)為引用是這樣實(shí)現(xiàn)的,但在對(duì)別人提出自己的觀點(diǎn)之前,我需要驗(yàn)證自己的“猜想”。這個(gè)問(wèn)題很好去驗(yàn)證,所以我先給出一個(gè)最簡(jiǎn)單的試驗(yàn):用一個(gè) int 類型的引用來(lái)說(shuō)明問(wèn)題。
?
輸入下面的代碼,我使用的是VC6.0:
void ModifyNum(int& x) {x = x + 10; }int main(int argc, char* argv[]) {int a = 5;ModifyNum(a);printf("a=%d\n", a);return 0; }?
?
上面的代碼將輸出 a = 15,然后用 IDA 打開(kāi)編譯后的 exe 文件,查看函數(shù) main 和 ModifyNum 的代碼:
.text:00401060 main proc near ; CODE XREF: j_mainj .text:00401060 .text:00401060 var_44 = dword ptr -44h .text:00401060 var_4 = dword ptr -4 .text:00401060 .text:00401060 push ebp .text:00401061 mov ebp, esp .text:00401063 sub esp, 44h .text:00401066 push ebx .text:00401067 push esi .text:00401068 push edi.text:00401078 mov [ebp+var_4], 5 .text:0040107F lea eax, [ebp+var_4] .text:00401082 push eax .text:00401083 call j_ModifyNum .text:00401088 add esp, 4 .text:0040108B mov ecx, [ebp+var_4] .text:0040108E push ecx .text:0040108F push offset ??_C@_06DJNL@a?$DN?$CFd?$CB?6?$AA@ ; "a=%d!\n" .text:00401094 call printf .text:00401099 add esp, 8//eax = 0 , return 0; .text:0040109C xor eax, eax .text:0040109E pop edi .text:0040109F pop esi .text:004010A0 pop ebx .text:004010A1 add esp, 44h .text:004010A4 cmp ebp, esp .text:004010A6 call __chkesp .text:004010AB mov esp, ebp .text:004010AD pop ebp .text:004010AE retn .text:004010AE main endp
注意上面的黃色背景的代碼,顯然函數(shù)的參數(shù)在底層上是把 int 變量的地址 (int*)作為參數(shù)傳遞的。那么 ModifyNum 的代碼實(shí)際上不看也就能猜到了,它和 ModifyNum ( int* pX) 應(yīng)該是一樣的。這個(gè)代碼很好找,在代碼段(.text)的頂部,緊跟跳轉(zhuǎn)表后面依次是 ModifyNum, main, printf, mainCRTStartup (即 PE 文件頭中記錄的入口點(diǎn)) 這幾個(gè)函數(shù)。
?
.text:00401020 ModifyNum proc near ; CODE XREF: j_ModifyNumj .text:00401020 .text:00401020 var_40 = dword ptr -40h .text:00401020 arg_0 = dword ptr 8 .text:00401020 .text:00401020 push ebp .text:00401021 mov ebp, esp .text:00401023 sub esp, 40h .text:00401026 push ebx .text:00401027 push esi .text:00401028 push edi.text:00401038 mov eax, [ebp+arg_0] .text:0040103B mov ecx, [eax] .text:0040103D add ecx, 0Ah .text:00401040 mov edx, [ebp+arg_0] .text:00401043 mov [edx], ecx .text:00401045 pop edi .text:00401046 pop esi .text:00401047 pop ebx .text:00401048 mov esp, ebp .text:0040104A pop ebp .text:0040104B retn .text:0040104B ModifyNum endp?
上面的代碼的實(shí)現(xiàn)顯然就是針對(duì)指針操作,也就是說(shuō),ModifyNum 的實(shí)現(xiàn)相當(dāng)于:
?
void ModifyNum(int* pX) {*pX = *pX + 10; }?
我也觀察了下面的代碼在匯編級(jí)別的實(shí)現(xiàn)(匯編代碼就不貼了):
int a =5;
int& b = a;
這里在匯編級(jí)別,b 相當(dāng)于是一個(gè) int* 類型的臨時(shí)變量,和 int* b = &a 等效。當(dāng)然在語(yǔ)言層面上我們可以理解成“b 是 a 的別名,b 就是 a”,只是看起來(lái)是這樣,但它并不是實(shí)現(xiàn),尤其是作為參數(shù)傳遞的時(shí)候編譯器只能使用指針去實(shí)現(xiàn)。而且非常重要的是,b 作為 a 的引用,它是一個(gè)指向 a 的指針變量,它是需要在棧上額外占用存儲(chǔ)空間的(如果理解成別名,有可能會(huì)誤以為 b 不需要占用存儲(chǔ)空間,這是不確切的)。
?
也就是說(shuō),C++中引用是編譯器通過(guò)指針實(shí)現(xiàn)的,但這個(gè)實(shí)現(xiàn)在語(yǔ)言層面對(duì)程序員做了透明化處理。?
很顯然,在C++里,如果一個(gè)函數(shù)需要使用(讀)一個(gè)比較大的對(duì)象中的數(shù)據(jù)(而不是修改它),和在棧上構(gòu)造出一個(gè)臨時(shí)拷貝比起來(lái),傳遞他的指針/引用是更高效的,《The C++ Programming Language》這本書(shū)中指出,這種情況,參數(shù)類型應(yīng)該加 const 即 const T&,這在語(yǔ)義上明確表示,你僅僅是使用而不是修改它。相對(duì)的,如果不加 const,則意味著你想明確的在函數(shù)中修改對(duì)象。在規(guī)模越大的項(xiàng)目中,這種約定對(duì)代碼可理解性起到的作用越大。
?
現(xiàn)在很多上層表述有一些“按引用傳遞”,“按值傳遞”,這種表述,我想它是比較模糊一點(diǎn)的,他們實(shí)際上意味著前者是傳遞了對(duì)象的地址,在函數(shù)中因此可以修改對(duì)象,即為所謂的引用。后者,按值傳遞,意味著棧上是一個(gè)對(duì)象的拷貝,所以在函數(shù)中修改的是棧上的臨時(shí)對(duì)象(當(dāng)然臨時(shí)對(duì)象是沒(méi)必要修改的),而不能影響函數(shù)以外的那個(gè)對(duì)象。由于參數(shù)是通過(guò)棧通知給函數(shù),所以只有“拷貝”(即 push)這個(gè)“傳遞”動(dòng)作(所以你可以說(shuō)底層上不存在前述的那些說(shuō)法,那只是站在函數(shù)調(diào)用功能的上層角度來(lái)說(shuō)的,而函數(shù)調(diào)用的底層實(shí)現(xiàn)只有按值傳遞一種,不管數(shù)據(jù)是從那里 push 到棧上的,棧上的數(shù)據(jù)都是從函數(shù)外傳入,且之后對(duì)棧上參數(shù)的值的修改和傳入的那個(gè)“源”無(wú)關(guān)),“按引用”和“按值”指的主要是參數(shù)意義(以及函數(shù)如何使用參數(shù),這和參數(shù)意義是相關(guān)的)。例如,如果參數(shù)是一個(gè)地址,你可以通過(guò)這個(gè)地址讀寫(xiě)它指向的對(duì)象即按引用方式,而你對(duì)這個(gè)地址的修改是無(wú)意義的,不會(huì)影響到函數(shù)以外的任何指針變量之類的東西。所以如果你想在函數(shù)里修改一個(gè)整數(shù),傳遞它的地址,即整數(shù)的指針。如果修改一個(gè)指針,傳遞指針的地址,即指針的指針,修改一個(gè)對(duì)象,傳遞它的地址,。。。不論你想改的是什么(T),傳遞它的地址(參數(shù)類型是 T*),而不是它的值(拷貝),然后在函數(shù)里去解析引用(dereference)。
?
順著這個(gè)話題說(shuō)下去,說(shuō)的更精確一些。在 C# 里,假設(shè)一個(gè)對(duì)象 T,一個(gè)函數(shù) void foo(T t); 存在下面的代碼:
T t = new T(); //或者 T t = null;
foo(t);
如果 T 是引用類型(class),它是按引用傳遞的,函數(shù)可以修改 T 的成員變量,但是不能修改 T 的指向。即函數(shù)foo調(diào)用后,t 的指向不會(huì)發(fā)生變化,依然是原來(lái)的對(duì)象,不能從 null 變?yōu)?其他對(duì)象,也不會(huì)被修改為 null。
如果 T 是值類型(struct),它是按值傳遞的,函數(shù)對(duì) T 的成員的修改只是針對(duì)棧上的臨時(shí)拷貝,而不會(huì)影響外面的 t。例如如下 c# 代碼:
?
struct StructA {public int num;public int x;public int y;public StructA(int _n){num = _n;x = 0;y = 0;}public StructA(int _n, int _x){num = _n;x = _x;y = 0;} }static void foo3(StructA a) {a.num = 300; } static void foo4(ref StructA a) {//a = new StructA(50, 1000); a.num = 400; } static void Main(string[] args) { StructA a = new StructA(10);foo3(a);Console.WriteLine("a.num = {0}", a.num); //a.num=10 foo4(ref a);Console.WriteLine("a.x = {0}", a.x);Console.WriteLine("a.num = {0}", a.num); //a.num=400 }?
由于 foo3 函數(shù)中 StructA 是“按值傳遞”,所以函數(shù)內(nèi)對(duì)對(duì)象的修改并不能影響到函數(shù)之外的那個(gè)“源對(duì)象”。加了 ref 參數(shù)以后,它相當(dāng)于是引用類型的“按引用傳遞”。對(duì)于引用類型的對(duì)象來(lái)說(shuō),ref 使函數(shù)中不僅可以修改對(duì)象的成員,還可以修改 a 的指向指向另一個(gè)對(duì)象,也就是可以修改“指向”和“被指向?qū)ο蟮膬?nèi)容”[1]。?
?
out 參數(shù)的應(yīng)用場(chǎng)景更加明確,要求函數(shù)必須明確的修改一個(gè)指針的指向或者值類型的值。而對(duì) ref 來(lái)說(shuō),對(duì)被指向的變量(注意這個(gè)變量通常已經(jīng)是一個(gè)對(duì)象的指針)的使用是自由的,即你可以不修改而僅僅使用它。相對(duì)于 out ,用處是,就是一個(gè)對(duì)象在傳入函數(shù)時(shí)可能沒(méi)有賦值過(guò)(可能是 null ),在函數(shù)里如果發(fā)現(xiàn)它是 null 就創(chuàng)建它(要求影響到外部變量),其他情況我們使用或修改它,這時(shí)候就應(yīng)該加 ref 了。
?
在C#中由于完全 OO 的需要,所以隱藏了指針,而代之以“引用類型”的對(duì)象,所以對(duì)于一個(gè)引用類型的對(duì)象 T,在C++里相當(dāng)于對(duì)象T 的指針,在參數(shù)上加 ref 在 C++ 里相當(dāng)于指針的指針,即二級(jí)指針 T**。所以如果參數(shù)類型加 ref 意味著要修改一個(gè)指針變量的指向,這也就意味著你想在函數(shù)里對(duì)函數(shù)以外的那個(gè)對(duì)象重新賦值,即讓它指向其他對(duì)象或者 null。如果僅僅修改或讀取對(duì)象 T 的成員,就無(wú)須加 ref,因?yàn)橐眯蛯?duì)象本身已經(jīng)是指針了!這是在函數(shù)參數(shù)前面是否加 ref 的應(yīng)用場(chǎng)景,對(duì)于 C++ 因?yàn)楸仨氂袃?nèi)存模型的概念,所以這非常自然,可以毫無(wú)歧義的理解清楚。但對(duì)于 .net 程序員可能很難搞清楚這里的原因和區(qū)別。
?
希望每個(gè)人都能嚴(yán)謹(jǐn)?shù)膶?duì)待技術(shù),而不是覺(jué)得自己非常了不起,聽(tīng)不進(jìn)任何批評(píng)意見(jiàn)。PS:我盡可能去除了本文中的主觀評(píng)論成分。
?
[1]? 上面給出的范例代碼中值類型的 a 在 C++ 對(duì)應(yīng)著什么取決于 C# 底層的內(nèi)存管理,根據(jù) MSDN 的說(shuō)法:
“值類型對(duì)象(例如結(jié)構(gòu))是在堆棧上創(chuàng)建的,而引用類型對(duì)象(例如類)是在堆上創(chuàng)建的。兩種類型的對(duì)象都是自動(dòng)銷毀的,但是,基于值類型的對(duì)象是在超出范圍時(shí)銷毀,而基于引用類型的對(duì)象則是在對(duì)該對(duì)象的最后一個(gè)引用被移除之后在某個(gè)不確定的時(shí)間銷毀。對(duì)于占用固定資源(例如大量?jī)?nèi)存、文件句柄或網(wǎng)絡(luò)連接)的引用類型,有時(shí)需要使用確定性終止以確保對(duì)象被盡快銷毀。有關(guān)更多信息,請(qǐng)參見(jiàn)?using 語(yǔ)句(C# 參考)?”。
“基于值類型的對(duì)象是在超出范圍時(shí)銷毀”,這句話印證了值類型對(duì)象是分配在函數(shù)的棧上的,因此一旦離開(kāi)函數(shù),這個(gè)對(duì)象占用的空間也就會(huì)被釋放,所以被銷毀的時(shí)間是確定的,即函數(shù)返回的時(shí)刻。而堆上的對(duì)象,由于依賴 GC 回收,所以銷毀的時(shí)間是不確定的。
值類型的 a 分配在棧上,相當(dāng)于棧上臨時(shí)對(duì)象,也就是 a 在 c++ 中對(duì)應(yīng)的是對(duì)象本身(非指針),因此加了 ref 對(duì)應(yīng)的是指針。所以 c# 中的值類型即使加了 ref 也是無(wú)法修改指向的(因?yàn)?c# 的語(yǔ)法達(dá)不到 c++ 中的二級(jí)指針,當(dāng)然這個(gè)操作對(duì)“值類型”來(lái)說(shuō)也沒(méi)什么必要,“指向一個(gè)值類型的指針變量”和“改變指向”屬于建立在“線性內(nèi)存”模型上的說(shuō)法,在完全面向?qū)ο髸r(shí),沒(méi)有這個(gè)模型,也沒(méi)有“指向值類型對(duì)象的指針”的概念,也就無(wú)所謂有“修改指向”這個(gè)需求),而只能修改指向的對(duì)象的內(nèi)容。當(dāng)然這是由值類型的語(yǔ)言意義決定的,在 c++?中 struct 的主要是數(shù)據(jù)封裝手段,并沒(méi)有上升到“值類型”這種語(yǔ)言意義高度去特殊看待,所以 c++ 中的struct,可以分配到棧上,堆上,其指針也是能通過(guò)函數(shù)修改指向的,因?yàn)樗鼪](méi)有語(yǔ)言上的特殊含義,所以和 class 的操作沒(méi)有特別區(qū)別。而在 c# 中的 struct 被賦予“值類型”的語(yǔ)言級(jí)別的意義,一些語(yǔ)言層次的代碼的含義就會(huì)不同,例如比較和賦值。
如果函數(shù) foo4 中調(diào)用 a = new StructA(...),則這句話修改的并不是 a 的指向,而是把一個(gè)新的臨時(shí)對(duì)象的內(nèi)容拷貝到 a,因?yàn)?a 是值類型,賦值操作是一種內(nèi)容拷貝。例如下面的代碼中b = a 的賦值代碼的效果,如果是值類型,a,b 是兩個(gè)獨(dú)立對(duì)象,如果是引用類型,a,b 將指向同一個(gè)對(duì)象:
?
? ?StructA a = new StructA();
????????? a.num = 1980;
??????????StructA b = a;????//因?yàn)镾tructA是值類型,因此 b?獨(dú)立于 a 的另一個(gè)結(jié)構(gòu)體!
????????? b.num = 2012;
????????? Console.WriteLine("a.num = {0}", a.num);? //a.num = 1980
????????? Console.WriteLine("b.num = {0}", b.num);??//b.num = 2012
總結(jié)
以上是生活随笔為你收集整理的C++中“引用”的底层实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 实用小公举
- 下一篇: c++函数重载机制实现原理