首頁 > 軟體

c++超細緻講解除參照

2022-05-16 16:00:50

參照的概念

參照 不是新定義一個變數,而 是給已存在變數取了一個別名 ,編譯器不會為參照變數開闢記憶體空間,它和它參照的變數共用同一塊記憶體空間。

參照的表示方法

型別 & 參照變數名 ( 物件名 ) = 參照實體;

如果熟悉C語言的同學可能會發現參照符號(&)看上去就像取地址運運算元(&)或者按位元AND運運算元(&),其實這是一個運運算元過載的例子。通過過載,同一個運運算元將會有不同的含義。編譯器會通過上下文來確定運運算元的含義。除了這裡所提到的,其實在C++中還有一些運運算元過載的情況。例如:* 即表示乘法,又表示對指標的解除參照操作;<<即表示插入運運算元,又表示按位元左移運運算元等。

程式碼範例:

int main()
{
	//參照:取別名
	int a = 10;
	int& b = a;//定義參照型別
	int& c = b;
	return 0;
}

本段程式碼我們可以得知,a變數取了b,c兩個別名。

我們也可以通過偵錯觀察他們的記憶體:

通過調取記憶體我們可以發現,a,b,c所指向的是同一塊記憶體空間。

注意: 參照型別 必須和參照 實體 是 同種型別 的

參照特性

參照有三個特性,分別是:

1. 參照在 定義時必須初始化

2. 一個變數可以有多個參照

3. 參照一旦參照一個實體,再不能參照其他實體

1.參照在定義的時候必須初始化

由於參照是對已經存在的變數進行取別名,因此使用參照時必須指定變數(初始化)。

int& d;//錯誤,未初始化

2.一個變數可以有多個參照

在C++語法中,一個變數有多個參照,就類似於一個人可以有多個外號。在1.1的程式碼範例中變數a就有2個參照,分別是b和c。

3.參照一旦參照一個實體,再不能參照其他實體

這個也比較好理解,因為參照一旦參照了一個已經存在的實體,就是這個實體的別名,當然不能再成為其他實體的別名。

常參照與參照許可權

我們來觀察下面這段程式碼,他能編譯成功嗎?

int main()
{
   //1.
	const int x = 20;
	int& y = x;      
	return 0;
}

當我們編譯這段程式碼發現編譯器報出錯誤警告:無法從“const int”轉換為“int &”

這是因為我們在參照的時候要遵守參照的原則:

參照原則:對原變數的參照,許可權不能放大。

1.3這段程式碼中x變數是const修飾是一個常變數,只有可讀許可權。而我們參照的型別是int,不僅有可讀許可權,還有可修改許可權。這就造成了對原變數的許可權放大。根據我們參照原則知道,對原變數的參照,許可權是不能放大的,這就是為什麼這段程式碼會報錯的原因。

那我們再來看這一段程式碼,它能編譯成功嗎?

int main()
{
	//2.
	const int x = 20;
	const int& y = x;//不變
    //3.
	int c = 30;
	const int& d = c;//縮小
	return 0;
}

這段程式碼我們發現編譯成功了,我們也可以輕鬆地分析出這裡的參照是遵守參照規則的,我們發現,許可權不變或者許可權縮小都是符合規則的,唯一需要注意的是:許可權不能放大。

參照的使用場景

做引數

void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 0, b = 1;
	Swap(a, b);
	return 0;
}

參照可以作函數的形參,x是a的別名,y是b的別名。這裡使用參照更加方便,也更好理解。

那既然以值作為函數引數和以參照作為函數引數都能解決這個問題,那為什麼還要使用參照來做引數呢?這是因為參照的效率更高,我們可以通過下面這段測試程式碼更加直觀看出效率的差別:

#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
	A a;
	// 以值作為函數引數
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以參照作為函數引數
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc2(a);
	size_t end2 = clock();
	// 分別計算兩個函數執行結束後的時間
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
	TestRefAndValue();
	return 0;
}

我們發現使用參照作為函數引數效率大大提高。以值作為引數或者返回值型別,在傳參和返回期間,函數不會直接傳遞實參或者將變數本身直接返回,而是傳遞實參或者返回變數的一份臨時的拷貝,因此用值作為引數或者返回值型別,效率是非常低下的,尤其是當引數或者返回值型別非常大時,效率就更低。

參照做引數的意義:

1.輸出型引數。

2.減少拷貝,提高效率。

做返回值

首先我們來觀察這段程式碼的返回值是什麼?

int Count()
{
	static int n = 0;
	n++;
	return n;
}
int main()
{
	cout << Count() << endl;
	cout << Count() << endl;
	cout << Count() << endl;
	return 0;
}

這裡的結果是:

1 2 3

因為n是區域性靜態的成員變數,只會初始化一次,雖然作用域在Count函數內部,但是生命週期是全域性,我們可以通過偵錯觀看他是否再執行函數的第一句?

傳值的底層過程

傳值返回這個過程當中會產生一個臨時變數,跟傳參一樣,如果小會用暫存器替代。傳值返回的型別其實是臨時變數的型別,將n拷貝給臨時變數,再將臨時變數拷貝給ret。那麼為什麼要設計臨時變數呢?直接把n給ret不好嗎?

這是因為在當臨時變數出了函數作用域之後會銷燬,函數棧楨也會銷燬,那麼此時n是不能作為返回值再賦值給ret的。那麼編譯器就在此生成了一個臨時變數,把n拷給臨時變數,再把臨時變數給ret。此時,函數棧楨銷燬是不會影響臨時變數的。

那我們怎麼可以證明這個過程產生了臨時變數,我們可以給ret前加個參照。

此時我們發現,編譯器是過不了的,這是因為此時ret是參照的臨時變數,而臨時變數具有常性,這裡屬於許可權的放大,因此我們只需要加上const即可。我們也通過這個例子證明了臨時變數的存在。

那現在我們給Count函數加個參照是什麼意思?我們來看這段程式碼。

int& Count()
{
	int n = 0;
	n++; 
	return n;
}
//中間產生了一個臨時變數
int main()
{
	int ret = Count(); 
	return 0;
}

這裡可以這麼認為,中間也會產生一個臨時變數,這個臨時變數的型別為int&,此時這個臨時變數是n的別名,再把臨時變數賦給ret。返回的是一個n的別名,就相當於是吧n返回給了ret。

此時我們再觀察這段程式碼我們發現編譯器可以通過了,這裡ret相當於是n的別名。

我們可以列印n和ret的地址看看:

這裡ret和n的地址相同,也能證明ret是n的別名。因此,參照作為返回值其實返回的就是n的別名。

參照導致野指標

這段程式碼合法嗎?

其實這段程式碼是不合法的,因為出了函數的作用域,Count函數已經銷燬了,我們再對此空間進行存取,就會造成非法存取,這裡就是參照搞出來的野指標。

我們來驗證一下:

//傳參照返回的是n的別名
int& Count()
{
	int n = 0;
	n++; 
	//cout << "n:"<< & n << endl;
	return n;
}
//中間產生了一個臨時變數
int main()
{
	int& ret = Count(); //ret是別名的別名  也就是n的別名
	cout << ret << endl;
	cout << "ret"<< & ret << endl;
	cout << ret << endl;
	return 0;
}

通過列印我們能夠發現:第二個ret列印的是隨機值。

因此此處需要注意 :

如果函數返回時,出了函數作用域,如果返回物件還未還給系統,則可以使用參照返回,如果已 經還給系統了,則必須使用傳值返回。

我們來做一個範例鞏固一下:

下面這段程式碼的結果是什麼?為什麼?

int& Add(int a, int b) 
{
	int c = a + b;
	return c;
}
int main()
{
	int& ret = Add(1, 2);
	Add(3, 4);
	cout << "Add(1, 2) is :" << ret << endl;
	return 0;
}

結果:7,這裡是因為在第一次呼叫Add時,ret為3,Add函數的棧楨銷燬,在第二次呼叫時,Add函數的棧楨是相同的,c的位置為覆蓋為7,再次存取ret此時就為7,因此這裡使用是不安全的。以下列印就可以更加清晰瞭解這個過程。

值和參照作為返回值型別的效能比較

#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 參照返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
	// 以值作為函數的返回值型別
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();
	// 以參照作為函數的返回值型別
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();
	// 計算兩個函數運算完成之後的時間
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
	TestReturnByRefOrValue();
	return 0;
}

通過列印我們發現參照作為返回值型別大大提高了效率。

原因:以值作為引數或者返回值型別,在傳參和返回期間,函數不會直接傳遞實參或者將變數本身直接返回,而是傳遞實參或者返回變數的一份臨時的拷貝,因此用值作為引數或者返回值型別,效率是非常低下的,尤其是當引數或者返回值型別非常大時,效率就更低。

參照和指標的區別

參照在語法概念上參照就是一個別名,沒有獨立空間,和其參照實體共用同一塊空間。 在底層實現上實際是有空間的,因為參照是按照指標方式來實現的。

int main()
{
	int a = 10;
	int& ra = a;
	ra = 20;
	int* pa = &a;
	*pa = 20;
	return 0;
}

我們來看參照和指標的組合程式碼對比:

因此參照的底層實現上是按照指標的方式來實現的。

參照和指標的不同點:

1. 參照在定義時必須初始化,指標沒有要求

2. 參照在初始化時參照一個實體後,就不能再參照其他實體,而指標可以在任何時候指向任何一個同型別實體

3. 沒有NULL參照,但有NULL指標

4. 在sizeof中含義不同:參照結果為參照型別的大小,但指標始終是地址空間所佔位元組個數(32位元平臺下佔4個位元組)

5. 參照自加即參照的實體增加1,指標自加即指標向後偏移一個型別的大小

6. 有多級指標,但是沒有多級參照

7. 存取實體方式不同,指標需要顯式解除參照,參照編譯器自己處理

8. 參照比指標使用起來相對更安全

到此這篇關於c++超細緻講解除參照的文章就介紹到這了,更多相關 c++ 參照內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com