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

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

1,引用就是一個對象的別名。
比如:李逵,在家稱爲"鐵牛",江湖上人稱"黑旋風"。

在這裏插入圖片描述

2,引用不是值不佔內存空間。

3,引用必須在定義時賦值,將變量與引用綁定。

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

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

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

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

3,引用在內存中佔4個字節。

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

學習福利:C/C++學習交流羣:556791282 羣裏有大量免費學習資料,趕快加入和大佬一起學習吧!

二、探究本質
我們從最簡單的變量的定義開始,看編譯器會做哪些事情。

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個字節。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章