C++的那些事:你真的瞭解引用嗎

☆Ronny丶發表於2014-04-13

一、引用的本質是什麼

說到引用,一般C++的教材中都是這麼定義的:

1,引用就是一個物件的別名。

2,引用不是值不佔記憶體空間。

3,引用必須在定義時賦值,將變數與引用繫結。

那你有沒有想過,上面的定義正確嗎?編譯器是如何解釋引用的?

這裡先給出引用的本質定義,後面我們再進一步論證。

1,引用實際是通過指標實現的。

2,引用是一個常量指標。

3,引用在記憶體中佔4個位元組。

4,在對引用定義時,需要對這個常量指標初始化。

二、探究本質

我們從最簡單的變數的定義開始,看編譯器會做哪些事情。

int var = 42;
mov         dword ptr [var],2Ah  // 對應彙編程式碼

上面語句申請了一塊記憶體空間,佔4個位元組,存放了一個int型的變數。記憶體裡放的是42的二進位制碼。

彙編程式碼向我們表達的意思就是把42寫入以var為地址的內容區域。var有點像我們理解上的指標,只是編譯器並沒有把它抽象出來,而是讓我們更表象的理解:申請一個變數,它的值為42。

那麼var這個變數名放在哪呢

我們知道程式如果訪問記憶體裡的資料,需要通過地址來進行訪問,所以上面的程式碼在經過編譯器生成目的碼時,用存放42的地址了所有的var,所以結論時,目標檔案中不存在var,所以變數名本身是不佔記憶體的

而我們知道,引用是變數的一個別名。那麼,從這很多人會聯想到,引用會不會也只是一個名字而已,編譯器在生成目的碼的時候,會用實際地址替換引用呢?

答案並非這樣!

那我們接下來看看,當我們定義一個引用時,發生了什麼:

1     int var = 42; 
2 01303AC8  mov         dword ptr [var],2Ah  
3     int&  refVar = var; 
4 01303ACF  lea         eax,[var]  
5 01303AD2  mov         dword ptr [refVar],eax 

上面的程式碼顯示,當定義一個引用時,編譯器將var的地址賦給了以refVar為地址的一塊記憶體區域。也就是說refVar其實存放的是var的地址。

這讓我們聯想到了指標,那麼我們看看定義一個指標是發生了什麼:

1     int var = 42; 
2 01213AC8  mov         dword ptr [var],2Ah  
3     int* ptrVar = &var; 
4 01213ACF  lea         eax,[var]  
5 01213AD2  mov         dword ptr [ptrVar],eax 

沒錯,沒有任何差別,定義一個引用和一個指標的彙編程式碼完全一致!

三、const哪裡去了

相信從上面的分析時,你可能已經相信了,引用實際上就是一個指標。那麼為什麼說引用是一個常量指標呢,在目的碼裡有什麼體現呢?

這個問題其實要從C++底層機制談起,C++為我們提供的各種存取控制僅僅是在編譯階段給我們的限制,也就是說編譯器確保了你在完成任務之前的正確行為,如果你的行為不正確,那麼編譯器就是給你在編譯時提示錯誤。所謂的const和private等在實際的目的碼里根本不存在,所以在程式執行期間只要你願意,你可以通過記憶體工具修改它的任何一個變數的值。

這也就解釋了為什麼上面的兩段程式碼中引用和指標的彙編程式碼完全一致。

C++設計引用,並用常量指標來從編譯器的角度實現它,目標是為了提供比指標更高的安全性,因為常量指標一旦與變數地址繫結將不能更改,這樣降低了指標的危險係數,它提供了一種一對一的指標。

但是你覺得使用引用就安全了嗎?它同樣會有與使用指標一樣的問題

1 int *var = new int(42); 
2 int &ref = *var; 
3 delete var; 
4 ref = 42; 
5 return 0;

上面這段程式碼就很不安全,因為ref引用的記憶體區域不合法。

為了進一步驗證引用與指標在本質上的相同,我們看當引用作為函式引數傳遞時,編譯器的行為:

 1 void Swap(int& v1, int& v2); 
 2 void Swap(int* v1, int* v2);
 3 
 4     int var1 = 1; 
 5 00A64AF8  mov         dword ptr [var1],1  
 6     int var2 = 2; 
 7 00A64AFF  mov         dword ptr [var2],2  
 8     Swap(var1,var2); 
 9 00A64B06  lea         eax,[var2]  
10 00A64B09  push        eax  
11 00A64B0A  lea         ecx,[var1]  
12 00A64B0D  push        ecx  
13 00A64B0E  call        Swap (0A6141Fh)  
14 00A64B13  add         esp,8  
15     Swap(&var1, &var2); 
16 00A64B16  lea         eax,[var2]  
17 00A64B19  push        eax  
18 00A64B1A  lea         ecx,[var1]  
19 00A64B1D  push        ecx  
20 00A64B1E  call        Swap (0A61424h)  
21 00A64B23  add         esp,8 

上面程式碼再次證明了,引用與指標的行為完全一致,只是編譯器在編譯時對引用作了更嚴格的限制。

四、引用佔多大的記憶體空間

因為在在表示式中,使用引用實際上就像使用變數本身一樣,所以直接用sizeof是得不到引用本身的大小的。

double var = 42.0; 
double& ref = var;

cout << sizeof var << endl;  // print 8 
cout << sizeof ref << endl;   // print 8

我們可以通過定義一個只含有引用的類來解決這個問題:

1 class refClass{ 
2 private: 
3     double& ref; 
4 public: 
5     refClass(double var = 42.0) :ref(var){} 
6 };
7 
8 cout << sizeof refClass << endl;  // print 4

所以結論就是引用和指標一樣實際佔記憶體空間4個位元組。

 

參考文章:http://www.cnblogs.com/rollenholt/articles/1907408.html

相關文章