C++中引用的本質到底是什麼?

C++的引用到底是什麼?用了這麼久,還不知道它居然也是個指針…

相關文章
C++編程之命名空間、const常量的總結
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++編碼界默認的一條鐵律,記住這個,纔不會被大佬們蔑視哦。

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