C++的引用到底是什麼?用了這麼久,還不知道它居然也是個指針…
前段時間寫過一篇《C++編程之引用的詳細總結》 ,看過就知道,哦,原來引用是對象/變量的一個別名,在使用時,是直接操作對象本體,因此通過引用傳參,不需要拷貝內存,效率很高。但是最近有人私下問我:“你寫的倒是挺全面的,但是引用的本質到底是什麼?”
因此,今天決定再深入解釋一下引用。
其實 引用的本質在C++內部實現是一個指針常量。C++編譯器在編譯過程中使用常指針作爲引用的內部實現,因此引用所佔用的空間大小和指針相同,這個過程是編譯器內部實現,用戶不可見。
#include<iostream>
using namespace std;
// 編譯器判斷是引用,會將入參自動轉換成int* const ref = &a;
void Test(int &ref)
{
ref = 10; // ref是引用,此處會轉換爲 *ref = 10;
}
int main()
{
int a = 100;
int & b = a; //自動轉換爲int* const b = &a;這就說明爲什麼引用必須要初始化。
b = 20; //編譯器判斷b是引用,自動轉換爲*b = 20;
Test(a);
return 0 ;
}
這就是引用爲什麼必須要初始化,因爲內部轉換爲常指針時,需要拿到它的地址,所以必須要先初始化。
接着通過反彙編,驗證兩種情況是等價的。
使用引用傳參
#include<iostream>
using namespace std;
void Test(int& a)
{
a = 100;
}
int main()
{
int b = 10;
Test(b);
cout << "b = " << b << endl;
return 0;
}
運行結果
斷點調試,進入彙編
00F52AF8 mov eax,dword ptr [a] //dword表示的是雙字,四字節。a中保存的是內存中的地址。將該地址處的4字節數據傳送到eax中。
00F52AFB mov dword ptr [eax],64h //將64h的值傳遞給 [eax] 所指示的內存單元,也就是a的本體
如圖顯示斷點到 a = 100 之前,a的值是10.,執行之後,如下圖所示。
這樣就通過操作引用,實際修改了本體的值。
使用常指針傳參
#include<iostream>
using namespace std;
void Test1(int* const a)
{
*a = 1000;
}
int main()
{
int b = 10;
Test1(&b);
cout << "b = " << b << endl;
system("pause");
return 0;
}
運行結果
同樣,斷點調試到彙編中看看結果
001D2A98 mov eax,dword ptr [a] //a中保存的是內存中的地址。將該地址處的4字節數據傳送到eax中。同樣是雙字節dwod,前面引用也是雙字節,說明引用內部就是常指針。
001D2A9B mov dword ptr [eax],3E8h // 將3E8h的值傳遞給 [eax] 所指示的內存單元,也就是a的本體
如下圖所示
當執行 a=1000 後,a的值也變爲1000,如下圖彙編代碼:
因此,從彙編層看,引用在內部的確被轉換爲常指針。這就解釋了引用必須要初始化的原因,初始化後,不能再修改指向。因此可以通過引用, 間接代替指針 ,比如一級指針可以直接用引用代替,二級指針可以用一級指針的引用代替,三級指針可以用二級指針的引用代替等。
總結完引用的本質,接着補充幾個實際的例子。
數組的引用
void Test()
{
int arr[10];
int (&pArr)[10] = arr;
for(int i = 0; i< 10;i++)
{
arr[i]=i;
}
for(int i =0; i<10;i++)
{
cout<<pArr[i]<<endl;
}
}
輸出結果:
0
1
2
3
...
9
指針的引用
#include<iostream>
using namespace std;
class Person
{
public:
int age;
}
// 使用二級指針給一級指針分配內存。可以通過指針的引用簡化爲一級指針,如下AllocSpace2
void AllocSpace1(Person **p)
{
*p = (Person*)malloc(sizeof(Person));
return;
}
// 使用指針的引用傳參
void AllocSpace2(Person* &p)
{
p = (Person*)malloc(sizeof(Person));
return;
}
int main()
{
Person* p = NULL;
AllocSpace1(&p); // 取地址,傳入二級指針
AllocSpace2(p); // 引用,直接傳入一級指針本體。
return 0;
}
// 因此,可以使用引用簡化指針,即二級指針可以用一級指針的引用代替,一級指針直接用引用代替,減少指針操作。
常量的引用
如何定義常量的引用,如下代碼解釋
void Test()
{
//int &a = 10; //非法操作,無法給非常量的引用賦值
int const &b = 10; //合法操作,編譯器在內部會做類似的轉換。int temp=10; int const &b = temp;
}
常量的引用的應用場景是什麼呢?
void Test1(int &b)
{
cout << "b = " << b << endl;
}
int main()
{
int a = 100;
Test1(a);
return 0;
}
如上代碼所示,使用常量作爲參數,在函數內部不會開闢新內存,節省內存空間。但是,如果在 Test1 中做如下操作,有什麼影響呢?
void Test1(int &b)
{
cout << "b = " << b << endl;
b = 10;
}
很明顯可以看出,b的值在新函數中被修改了,在main函數中再次使用時,值已經變了,這就造成了很大的問題,因此出現了常引用。
引入常量的引用目的是爲了防止誤操作,但是常量的引用能不能被修改值呢?答案是肯定可以的,如下:
void Test2()
{
int const &ref = 10;
int *p = (int*)&ref;
*p = 10000;
cout << "ref =" << ref << endl;
}
int main()
{
Test2();
system("pause");
return 0;
}
因此大家在編寫C/C++代碼時,常識性的在入參位置做好const保護,除非你的入參是要被修改的,這是C/C++編碼界默認的一條鐵律,記住這個,纔不會被大佬們蔑視哦。