首頁 > 軟體

C++深入探究參照的使用

2022-05-16 19:00:19

一. 參照的概念

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

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

如下:

void TestRef()
{
     int a = 10;
     int& ra = a;//<====定義參照型別
     printf("%pn", &a);
     printf("%pn", &ra);
}

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

二. 參照特性

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

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

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

如下:

void TestRef()
{
     int a = 10;
     int a2 = 20;
     //a的多個參照
     int& b = a;
     int& c = a;
     int& d = b;
     int& ra;//該條語句編譯時會出錯,未初始化
     int &ra = a2;//報錯,參照了其他實體
     printf("%p %p %p %pn", &a, &b, &c, &d); 
}

三. 常參照

void TestConstRef()
{
     const int a = 10;
     //int& ra = a; // 該語句編譯時會出錯,a為常數
     const int& ra = a;
     // int& b = 10; // 該語句編譯時會出錯,b為常數
     const int& b = 10;
     double d = 12.34;
     //int& rd = d; // 該語句編譯時會出錯,型別不同
     const int& rd = d;
     //int& c = 100; // 該語句編譯時會出錯,常數是唯讀的
     const int& c = 100;
}

注意:

參照取別名原則:對原參照變數,讀寫許可權只能縮小,不能放大

const int a = 10;

int& ra = a;

編譯不通過,因為放大了許可權,原參照本來是唯讀,但是參照以後卻變成了可讀可寫

int& b = 10;

const int& b = 10;

編譯可以通過,因為縮小了許可權,原參照本來是可讀可寫,參照後變成了唯讀

double d = 12.34;

int& rd = d;

編譯不通過,這裡比較特殊,看起來是因為型別不同而報錯,其實不然,報錯是因為許可權放大了,為什麼?

int型別要參照double型別,double型別轉化到int型別屬於隱式型別轉換會捨棄小數位,隱式型別轉換會產生臨時變數,double型別到int型別會建立一個臨時變數儲存double變成了int型別的值,這裡需要注意,這個臨時變數具有常性是唯讀的,rd其實是參照了這個臨時變數,因為臨時變數是唯讀的,參照了臨時變數的rd也應該是唯讀的,所以這就是為什麼const int& rd = d 可以編譯通過。

int& c = 100;

編譯通過,因為常數本來就是唯讀的,不加const代表參照後變成了可讀可寫,許可權放大。

四. 使用場景

1. 做引數

void Swap(int& left, int& right)
{
     int temp = left;
     left = right;
     right = temp;
}
  • 輸出型引數
  • 減少拷貝,提高效率

2. 做返回值

int& Count()
{
     static int n = 0;
     n++;
     // ...
     return n;
}

減少拷貝

(傳值返回需要拷貝資料,傳參照返回直接返回變數的別名)

3. 做返回值需要注意的問題

首先,我們要知道當函數返回一個值時,會生成一個臨時變數,而函數的返回型別就是這個臨時變數的型別

int Add(int a, int b)
{
    return a + b;
}
int Count()
{
    static int n = 0;
    n++;
    return n;
}
int main()
{
    int temp = Add(2, 3);
    int tmp = Count();
    return 0;
}

以上程式碼將a+b(n)的值賦值給臨時變數,臨時變數再賦值給temp(tmp),為什麼要設定這個臨時變數?

其實很簡單,在這個程式碼裡是會有問題的,出了函數作用域a+b的值就已經被銷燬了,需要一個臨時變數去儲存這個返回值,再去存取那塊空間是非法的,而被static修飾的n由於它的生命週期變長了,即使出了函數也不會被銷燬

那麼問題來了,以下程式碼是正確的嗎?

int& Add(int a, int b)
{
     int c = a + b;
     return c;
}
int main()
{
     int& ret = Add(1, 2);
     return 0;
}

很明顯是有問題的!這裡將c的參照返回 ,而一旦出了函數c就被銷燬了,這塊空間也被作業系統收回,再將c的參照賦值給ret就變成了非法存取了,就變成了由參照造成的野指標

由上面的問題可以衍生出以下程式碼:

這裡的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,ret是c的參照,由於出了函數以後這塊空間的使用權還給了作業系統,由於第二次函數呼叫仍然是在第一次函數呼叫的空間進行棧幀的建立,因為ret的地址(ret的地址就是之前那塊臨時變數的地址)還是之前那個地址,所以由於第二次返回c時建立的臨時變數已經變成了7,所以ret也變成了7

但是一定會是7嗎?其實不然,我們知道這塊空間的使用權還給了作業系統,這塊空間也有可能會被其他程式使用了,導致數值變成了不確定性,因為這裡是直接馬上又呼叫了這個函數,所以會是7,所以,其實正確答案應該是隨機值才對

看下面這個程式碼就是典型的例子:

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;
     cout << "Add(1, 2) is :"<< ret <<endl;
     return 0;
}

這裡的輸出語句其實也是呼叫了函數,由上面可知第一個是7,那麼第二個呢?隨機值!因為進行了第一次輸出後其實也是進行了函數呼叫,函數呼叫會建立棧幀,在上一個輸出建立的棧幀處重新建立了棧幀,函數呼叫前需要先傳參,由於上一個輸出語句銷燬完棧幀以後ret地址處的值被覆蓋成隨機值,在第二次輸出語句中此時就會把這個隨機值作為引數傳過去給函數,導致輸出了隨機值,所以傳參照返回不是所有情況都可以使用的,像一開始加上了static關鍵字之類的才可以返回,因為n的生命週期變長了,出了函數作用域沒有被銷燬,取值都是去靜態區取資料。

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

五. 傳值傳參照效率對比

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

1. 值和參照傳參時的效率比較

#include <time.h>
struct A { 
	int a[10000]; 
};
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestFunc3(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();
	// 以指標作為引數
	size_t begin3 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc3(&a);
	size_t end3 = clock();
	// 分別計算兩個函數執行結束後的時間
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
	cout << "TestFunc2(A&)-time:" << end3 - begin3 << endl;
}

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

#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()
{
     int a = 10;
     int& ra = a;
     cout<<"&a = "<<&a<<endl;
     cout<<"&ra = "<<&ra<<endl;
     return 0;
}

在底層實現上實際是有空間的,因為參照是按照指標方式來實現的。

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

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

參照和指標的不同點:

  • 參照在定義時必須初始化,指標沒有要求(建議初始化)
  • 參照在初始化時參照一個實體後,就不能再參照其他實體,而指標可以在任何時候指向任何一個同型別實體
  • 沒有NULL參照,但有NULL指標
  • 在sizeof中含義不同:參照結果為參照型別的大小,但指標始終是地址空間所佔位元組個數(32位元平臺下佔 4個位元組)
  • 參照自加即參照的實體增加1,指標自加即指標向後偏移一個型別的大小
  • 有多級指標,但是沒有多級參照
  • 存取實體方式不同,指標需要顯式解除參照,參照編譯器自己處理
  • 參照比指標使用起來相對更安全

參照和指標的相同點:

雖然從語法角度來看參照是別名沒有額外開空間,但是底層角度來看他們是一樣的。

什麼是底層角度呢?就是通過編譯器處理的結果來看,以下是指標和參照經編譯器處理後的結果

我們會發現組合指令是一致的,這就說明了從底層角度看這兩個實現方式是一樣的

到此這篇關於C++深入探究參照的使用的文章就介紹到這了,更多相關C++參照內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


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