廣州開發(fā)區(qū)建設(shè)和環(huán)境保護網(wǎng)站網(wǎng)站seo運營
目錄
一.?什么是引用
1.1?引用的概念
1.2?引用的定義
二.?引用的性質(zhì)和用途
2.1?引用的三大主要性質(zhì)
2.2?引用的主要應(yīng)用?
三. 引用的效率測試
3.1?傳值調(diào)用和傳引用調(diào)用的效率對比
3.2?值返回和引用返回的效率對比
四.?常引用?
4.1 權(quán)限放大和權(quán)限縮小問題
4.2?跨數(shù)據(jù)類型的引用問題
五.?引用和指針的區(qū)別
一.?什么是引用
1.1?引用的概念
引用,通俗的講,就是給已經(jīng)存在的變量取一個別名,而不是創(chuàng)建一個新的變量。引用和被引用對象共同使用一塊內(nèi)存空間。
引用就好比一個人的大名和小名,大名和小名都是一個人。再比如,李逵外號黑旋風(fēng),叫李逵和黑旋風(fēng)表示同一個人。
1.2?引用的定義
引用定義的語法格式:類型&?引用的名稱 =?被引用實體
如,定義int a = 10,希望再定義一個引用b,來表示整形變量a的別名,語法為:int& b = a。演示代碼1.1展示了引用的定義過程,對原變量a和引用b的其中任意一個賦值,都會使a和b的值均發(fā)生改變,這是因為a和b共用一塊內(nèi)存空間。
演示代碼1.1:
int main()
{int a = 10;int& b = a; //b是a的引用(別名)printf("a = %d, b = %d\n", a, b); //10,10a = 20; //對a賦值,同時改變a和bprintf("a = %d, b = %d\n", a, b); //20,20b = 30; //對b賦值,同時改變a和bprintf("a = %d, b = %d\n", a, b); //30,30return 0;
}

二.?引用的性質(zhì)和用途
2.1?引用的三大主要性質(zhì)
1、引用在定義時必須初始化
定義引用時必須給出這個引用的被引用實體是誰,如:int &b; -- 是非法的。
演示代碼2.1:
int main()
{int a = 10;int& b; //報錯int& c = a; //初始化引用return 0;
}

?2、一個變量可以有多個引用
我們可以為一個變量取多個別名。如演示代碼2.2所示,給a變量取b、c、d三個別名是可行的。對a、b、c、d中的任意一個賦值,都會使a、b、c、d的值均發(fā)生改變。a、b、c、d共用一塊內(nèi)存空間。
演示代碼2.2:
int main()
{int a = 10;int& b = a;int& c = a;int& d = a; //為a取b、c、d三個別名printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d); //10,10,10,10c = 20;printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d); //20,20,20,20return 0;
}

?3、一個引用一旦引用了某個實體,就不能再引用其他實體
演示代碼2.3中的b = c并不是將b變?yōu)樽兞縞的引用,而是將變量c的值賦給b,通過打印b和c的地址,我們可以發(fā)現(xiàn)b和c并不共用一塊內(nèi)存空間,而賦值之后,a和b的值都變?yōu)榱?0。
演示代碼2.3:
int main()
{int a = 10;int& b = a;int c = 20;b = c; //將c的值賦給b,而不是讓b變?yōu)閏的引用printf("&b = %p, &c = %p\n", &b, &c); //b和c的地址不一致printf("a = %d, b = %d\n", a, b); //a、b都變?yōu)榱薱的值20return 0;
}

正是因為引用一旦引用了某個實體之后就不能再引用其他實體,所以引用無法替代指針來實現(xiàn)鏈表數(shù)據(jù)結(jié)構(gòu)。否則就無法實現(xiàn)鏈表的增、刪等操作,鏈表的增刪操作需要改變指針的指向。
2.2?引用的主要應(yīng)用?
1、引用做函數(shù)參數(shù)
要寫一個swap函數(shù),實現(xiàn)兩個整形數(shù)據(jù)的交換,如果用C語言來寫這個函數(shù),就必須使用指針來作為函數(shù)的參數(shù),即:void swap(int* px, int* py)。但是,如果使用C++來寫,則可以用引用傳參來替代指針傳參,因為引用和被引用實體共用一塊內(nèi)存空間,引用傳參使得函數(shù)內(nèi)部可以控制實參所占用的內(nèi)存空間,這是,swap可以聲明為:void swap(int& rx, int& ry)。
演示代碼2.4:
void swap(int& rx, int& ry)
{int tmp = rx;rx = ry;ry = tmp;
}int main()
{int x = 10, y = 20;printf("交換前:x = %d,y = %d\n", x, y); //10,20swap(x, y);printf("交換后:x = %d,y = %d\n", x, y); //20,10return 0;
}

至此,可以總結(jié)出函數(shù)的三種調(diào)用方法:?
- 傳值調(diào)用。
- 傳地址調(diào)用。
- 傳引用調(diào)用。
?問題:void swap(int x, int y)和void swap(int& x, int& y)能否構(gòu)成函數(shù)重載?
答案是可以的。因為其滿足構(gòu)成函數(shù)重載的條件之一 :參數(shù)的類型不同。
但是,在調(diào)用這兩個swap函數(shù)時,會存在歧義。通過語句swap(x,y)調(diào)用,無法確定是調(diào)用swap(int x, int y)還是swap(int&x, int& y)。
2、引用做函數(shù)的返回值
在演示代碼2.5中,定義函數(shù)int& Add(int x, int y),函數(shù)返回z的別名。我們希望這個函數(shù)能夠?qū)+y進行計算。但是顯然,這段代碼是有潛在問題的,因為在add函數(shù)調(diào)用結(jié)束后,為add函數(shù)創(chuàng)建的棧幀會被銷毀,這塊棧空間會還給操作系統(tǒng)。此時再使用add函數(shù)的返回值,就會造成對內(nèi)存空間的非法訪問,而大部分情況下,編譯器不會對非法訪問內(nèi)存報錯。
演示代碼2.5:
int& add(int x, int y)
{int z = x + y;return z;
}int main()
{int& ret = add(1, 2);printf("ret = %d\n", ret);return 0;
}
對于演示代碼2.5的運行結(jié)果,可以分為兩種情況討論:
- 函數(shù)棧幀銷毀后,編譯器不對被銷毀的??臻g進行清理,打印函數(shù)的返回值,結(jié)果依舊為x + y的值。
- 函數(shù)棧幀銷毀后,編譯器對被銷毀的??臻g進行清理,函數(shù)的返回值為隨機值。
在VS2019 編譯環(huán)境下,演示代碼2.5的運行結(jié)果為3,說明VS編譯器不會清理被銷毀的函數(shù)棧幀空間中內(nèi)容。

既然VS編譯器不會對被銷毀的函數(shù)棧幀進行清理,那么是否在VS編譯環(huán)境下,可以正常使用演示代碼2.5中的add函數(shù)呢?答案顯然是否定的,這可以從以下兩個方面解釋:
- 如果在其他編譯環(huán)境下進行編譯,則被銷毀的函數(shù)空間可能會被清理,這樣會降低代碼的可移植性。
- 即使函數(shù)棧幀空間不被清理,但這塊空間已經(jīng)換給了操作系統(tǒng),如果調(diào)用完add函數(shù)后再調(diào)用其他函數(shù),那么原本為z開辟的空間可能會被覆蓋,從而改變ret的值。
如演示代碼2.6所示,第一次調(diào)用add函數(shù)使用ret來接收返回值,第二次調(diào)用add函數(shù)不接收返回值。但是第二次調(diào)用add函數(shù)之后,ret的值卻變?yōu)榱?0,這是因為第二次調(diào)用add函數(shù)覆蓋了第一次調(diào)用時創(chuàng)建的函數(shù)棧幀,原來第一次調(diào)用存放變量z的內(nèi)存空間的內(nèi)容由3變?yōu)榱?0,因此,程序運行的結(jié)果為30。這段代碼在運行過程中棧幀的創(chuàng)建和銷毀情況見圖2.7。
演示代碼2.6:
int& add(int x, int y)
{int z = x + y;return z;
}int main()
{int& ret = add(1, 2);cout << ret << endl;add(10, 20);cout << ret << endl;return 0;
}


總結(jié)(什么時候可以用引用返回,什么時候不可以):
- 如果出了函數(shù)作用域,函數(shù)返回的對象被銷毀了,則不能使用引用類型作為返回值。
- 如果出了函數(shù)作用域,函數(shù)的返回對象還沒有被銷毀(存儲返回對象的內(nèi)存還沒有還給操作系統(tǒng)),則可以使用引用作為返回值。
演示代碼2.7給出了兩種可以使用引用作為返回的情況,一種是以靜態(tài)變量作為返回對象,另一種是返回對象為調(diào)用函數(shù)中開辟的一塊內(nèi)存空間中的內(nèi)容(調(diào)用函數(shù)中開辟的數(shù)組)。
演示代碼2.7:
int& func1()
{static int n = 0;++n;return n;
}char& func2(char* str, int i)
{return str[i];
}int main()
{cout << func1() << endl; //1cout << func1() << endl; //2char ch[] = "abcdef";for (int i = 0; i < strlen(ch); ++i){func2(ch, i) = '0' + i;}cout << ch << endl; //012345return 0;
}

思考問題:既然函數(shù)完成調(diào)用時才會返回,而調(diào)用完成時函數(shù)棧幀又會被銷毀。那么,以值作為函數(shù)返回類型時,時如何從函數(shù)中接收返回值的呢?
就比如演示代碼2.8中的add函數(shù),函數(shù)返回值時add函數(shù)中的臨時變量z的值,在主函數(shù)中的ret如何從add函數(shù)中接收z值。
演示代碼2.8:?
int add(int x, int y)
{int z = x + y;return z;
}int main()
{int ret = add(2, 3);return 0;
}
答案其實很簡單,ret并不是直接從add函數(shù)棧幀的空間中接收返回值,而是在add函數(shù)完成調(diào)用、函數(shù)棧幀銷毀之前,存儲一個臨時變量用于接收函數(shù)的返回值,然后在將臨時變量的值賦給ret。
那么,這個臨時變量存儲在什么位置呢?分兩種情況討論:
- 如果返回值比較小,則使用寄存器充當(dāng)臨時變量。
- 如果返回值比較大,則將臨時變量放在調(diào)用add函數(shù)的函數(shù)內(nèi)部,在調(diào)用add函數(shù)之前在調(diào)用add的函數(shù)的棧幀中預(yù)先開辟一塊空間用于存儲臨時變量。

三. 引用的效率測試
3.1?傳值調(diào)用和傳引用調(diào)用的效率對比
演示代碼3.1分別執(zhí)行100000次傳值調(diào)用和100000次傳引用調(diào)用,每次傳值調(diào)用傳給函數(shù)的形參的大小為40000bytes,記錄傳值調(diào)用和傳引用調(diào)用消耗的時間。
程序運行結(jié)果顯示,10000次傳值調(diào)用耗時71ms,100000次傳引用調(diào)用耗時2ms,傳引用調(diào)用的效率遠高于傳值調(diào)用。這是因為傳引用調(diào)用不用再為形參開辟一塊內(nèi)存空間,而為形參開辟空間存在一定的時間消耗。
演示代碼3.1:
#include<iostream>
#include<time.h>
using namespace std;//大小為40000bytes的結(jié)構(gòu)體
typedef struct A
{int arr[10000];
}A;void Testvaluefunc(A a) { }; //傳值調(diào)用測試函數(shù)
void TestReffunc(A& a) { }; //傳引用調(diào)用測試函數(shù)void TestRefAndValue1()
{A a;int i = 0;size_t begin1 = clock(); //記錄開始傳值調(diào)用的時間(傳值調(diào)用100000次)for (i = 0; i < 100000; ++i){Testvaluefunc(a);}size_t end1 = clock(); //記錄結(jié)束傳值調(diào)用的時間size_t begin2 = clock(); //記錄開始傳引用調(diào)用的時間(調(diào)用100000次)for (i = 0; i < 100000; ++i){TestReffunc(a);}size_t end2 = clock();cout << "傳值調(diào)用10000次耗費時間:" << end1 - begin1 << endl;cout << "傳引用調(diào)用10000次耗費時間:" << end2 - begin2 << endl;
}

3.2?值返回和引用返回的效率對比
演示代碼3.2分別執(zhí)行100000次值返回函數(shù)和100000次引用返回函數(shù),記錄調(diào)用值返回函數(shù)和調(diào)用引用返回函數(shù)消耗的時間。程序運行結(jié)果表明,調(diào)用100000次值返回函數(shù)耗時136ms,調(diào)用100000次引用返回函數(shù)耗時2ms,引用返回的效率遠高于值返回。
演示代碼3.2:
#include<iostream>
#include<time.h>
using namespace std;typedef struct A
{int arr[10000];
}A;A a;A TestValuefunc2()
{return a;
}A& TestReffunc2()
{return a;
}void TestRefAndValue2()
{int i = 0;size_t begin1 = clock(); //記錄開始時間(調(diào)用100000次)for (i = 0; i < 100000; ++i){TestValuefunc2();}size_t end1 = clock(); //記錄結(jié)束時間size_t begin2 = clock(); //記錄開始的時間(調(diào)用100000次)for (i = 0; i < 100000; ++i){TestReffunc2();}size_t end2 = clock(); //記錄結(jié)束時間cout << "以值作為返回:" << end1 - begin1 << "ms" << endl;cout << "以引用作為返回:" << end2 - begin2 << "ms" << endl;
}int main()
{TestRefAndValue2(); //引用作為返回和值作為返回的效率測試return 0;
}

四.?常引用?
4.1 權(quán)限放大和權(quán)限縮小問題
如果int& b = a,而a是整形常量,被const關(guān)鍵字修飾,那么b就不能作為a的別,因為a變量是只讀的,而將b定義為int&類型,則表明b是可讀可寫的類型,b對a存在權(quán)限放大問題。
對于int a = 10,使用const?int& b = a來表示a的別名是可以編譯通過的。因為a為讀寫類型,而b為只讀類型,b相對于a權(quán)限縮小,C++允許權(quán)限縮小。
總結(jié):C++允許權(quán)限縮小,不允許權(quán)限放大。
演示代碼4.1:
int main()
{//權(quán)限放大問題const int a = 10;//int& b = a; //報錯const int& b = a; //編譯通過//權(quán)限縮小int c = 10;const int& d = c; //能夠編譯通過return 0;
}
4.2?跨數(shù)據(jù)類型的引用問題
看一個很詭異的問題。在演示代碼4.2中,定義一個雙精度浮點型數(shù)據(jù)double d = 1.1,編譯程序,出現(xiàn)下面的現(xiàn)象:
- 將d賦給int型數(shù)據(jù)i1,編譯通過。
- 用int&?i2 = d來作為d的引用(別名),編譯報錯。
- 但是,使用const int& i3 = d來作為d的引言,編譯通過。
演示代碼4.2:
int main()
{double d = 11.11;int i1 = d; //強轉(zhuǎn),編譯通過//int& i2 = d; //編譯報錯const int& i3 = d; //編譯通過printf("&d = %p\n", &d);printf("&i3 = %p\n", &i3);return 0;
}
那么,為什么const?int& i3類型的可以作為d的引用,而int& i2卻不行?問題出在強制類型轉(zhuǎn)換上。要理解這個問題,首先要清楚強制類型轉(zhuǎn)換的過程,強制類型轉(zhuǎn)換(int i1 = d),并不是將d強轉(zhuǎn)后的數(shù)據(jù)直接賦給i1,而是先將d強轉(zhuǎn)為int類型數(shù)據(jù)的值存儲在一個臨時變量中,然后再將臨時變量的值傳給i1,詳見圖4.1。

臨時變量具有常性,只可讀不可改。因此,int& i2 = d就存在權(quán)限放大的問題,編譯無法通過,而const?int& i3 = d不會存在權(quán)限放大的問題,可以編譯通過。但是,這里的i3就不再是d的別名,而是存儲d強轉(zhuǎn)為int類型數(shù)據(jù)值的臨時變量的別名,因此i3和d的地址也就不同。演示代碼4.2打印了i3和d的地址,表面他們不同,i3其實并不是d的別名。
提示:一定要弄清楚強轉(zhuǎn)類型轉(zhuǎn)換時臨時變量做中間值的問題!

五.?引用和指針的區(qū)別
- 引用是定義一個變量的別名,而指針存儲一個地址。
- 引用不占用額外的內(nèi)存空間,而指針要占用4bytes或8bytes的內(nèi)存空間。
- 引用在定義時必須初始化,而指針可以不初始化。(建議指針在定義時避免不初始化)。
- 引用一旦引用了某個實體,便不能更改被引用實體,而指針可以更改指向。
- 對引用自加,即對被引用的實體+1,指針自加,向后偏移一個指針類型的大小(bytes)。
- 沒有多級引用,有多級指針。
- 訪問實體時,引用直接由編譯器處理即可,指針需要解應(yīng)用。
- 沒有空引用,但有空指針NULL。
- 引用相對于指針更加安全。
因為指針存在野指針、空指針等問題,造成指針過于靈活,所以指針的安全性不如引用。
引用的底層是通過指針來實現(xiàn)的。
引用最大的局限性在于不能更改引用實體,因此雖然引用的底層是通過指針實現(xiàn)的,但引用不能替代指針來實現(xiàn)鏈表數(shù)據(jù)結(jié)構(gòu)。因為鏈表的操作需要更改指針的指向。