前言:
?相信大家在學(xué)習(xí)C語言的時候,最頭疼的就是指針,經(jīng)常會碰到一級指針、二級指針,這些指針使用起來,稍有不慎就會等導(dǎo)致程序崩潰,為了讓廣大程序員少掉點(diǎn)頭發(fā),C++中提出了 引用這一概念。當(dāng)然,在C++的代碼中,仍然可以兼容C語言的指針。
一、引用的概念
?在語法上引用不是新定義一個變量,而是給已存在的變量取一個別名,編譯器不會為引用變量開辟內(nèi)存空間,它和它引用的變量共用同一塊空間。例如:魯迅和周樹人都是同一個人。
- 類型& 引用變量名(對象名)= 引用實(shí)體
int main()
{
int a = 0;
int& b = a;//定義引用類型,b是a的引用
int& c = a;
return 0;
?通過匯編指令實(shí)現(xiàn)的角度看,引用底層是類似指針的方式實(shí)現(xiàn)的,如下圖所示:
二、共用同一塊空間驗(yàn)證
int main()
{
int a = 0;
int& b = a;
int& c = a;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
?通過上面打印的結(jié)果可以看出,引用變量的地址和引用實(shí)體的地址是一樣的,說明引用其實(shí)就是給同一塊內(nèi)存空間上取別名。
int main()
{
int a = 10;
int& b = a;
int& c = a;
b++;
cout << a << endl;
cout << b << endl;
cout << c << endl;
return 0;
}
?上面代碼,對其中一個引用變量++,其他三個變量的值也發(fā)生了改變,這也能說明引用是給同一塊內(nèi)存空間取別名。
三、引用的特性
3.1 引用在定義時必須初始化
int main()
{
int a = 10;
int& d;//不能這樣
return 0;
}
?上面的代碼是不被允許的,因?yàn)樵诙x引用變量b
的時候,沒有初始化。
3.2 一個變量可以有多個引用
int main()
{
int a = 10;
int& b = a;
int& c = a;
return 0;
}
?上面代碼中,b
和c
都是a
變量的引用。這一點(diǎn)很容易理解,就像同一個人可以有多個外號一樣,一個變量也可以同時有多個引用。
3.3 引用不能改變
?引用一旦引用一個實(shí)體,不能再引用其它實(shí)體。
int main()
{
int a = 10;
int num = 100;
int& b = a;
int& c = a;
b = num;//這里是把num的值賦給a,并不是讓b變成num的引用
cout << "變量a的地址:" << &a << endl;
cout << "引用b的地址:" << &b << endl;
cout << "變量num的地址:" << &num << endl;
return 0;
}
?上面代碼中的b = num
,是把num
變量的值賦值給b
引用的實(shí)體,并不是讓b
變成num
的引用。通過打印地址可以證明這一點(diǎn),引用b
的地址和變量a
的地址一樣,說明b
任然是a
的引用。這類似于終身制,引用一旦和實(shí)體綁定就不能再分離。
?這里還需要注意一點(diǎn):給引用b
賦值,引用實(shí)體a
也是會跟著發(fā)生改變的,就像周樹人吃了午飯,那魯迅必定也吃了,不可能出現(xiàn)周樹人吃了午飯,而魯迅還餓肚子的情況。
四、引用的使用場景
4.1 做參數(shù)
?引用做參數(shù)有以下兩個方面的意義:
- 做輸出型參數(shù),即要求形參的改變可以影響實(shí)參
- 提高效率,自定義類型傳參,用引用可以避免拷貝構(gòu)造,尤其是大對象和深拷貝對象(后續(xù)文章會講到)
交換兩個整型變量:
void Swap(int& num1, int& num2)
{
int tmp = num1;
num1 = num2;
num2 = tmp;
}
int main()
{
int a = 10;
int b = 11;
cout << "a:" << a << " " << "b:" << b << endl;
Swap(a, b);
cout << "a:" << a << " " << "b:" << b << endl;
return 0;
}
?上面代碼,利用引用做參數(shù)實(shí)現(xiàn)了兩個數(shù)的交換,以前交換兩個數(shù)的值,需要進(jìn)行值傳遞,也就是實(shí)參去兩個數(shù)的地址,形參用指針來接受。而用引用做參數(shù)后,實(shí)參無需再傳遞地址,形參num1
是變量a
的引用,和a表示同一塊內(nèi)存空間,形參num2
是變量b
的引用,和b
表示同一塊空間,因此在函數(shù)體內(nèi)交換num1
和num2
實(shí)際上就是交換a
和b
。
交換兩個指針變量:
void Swap(int*& p1, int*& p2)
{
int* tmp = p1;
p1 = p2;
p2 = tmp;
}
int main()
{
int a = 10;
int b = 11;
int* pa = &a;
int* pb = &b;
cout << "pa:" << pa << " " << "pb:" << pb << endl;
Swap(pa, pb);
cout << "pa:" << pa << " " << "pb:" << pb << endl;
return 0;
}
?如果用C語言來實(shí)現(xiàn)交換兩個指針變量,實(shí)參需要傳遞指針變量的地址,那形參就需要用二級指針來接收,這顯然十分繁瑣,且容易出錯。有了引用之后,實(shí)參直接傳遞指針變量即可,形參用指針類型的引用(指針也是一種類型)。
鏈表新玩法:
?之前用C語言實(shí)現(xiàn)的鏈表,在進(jìn)行尾插、頭插、頭刪等操作的時候,因?yàn)榭赡軙婕暗綄︻^節(jié)點(diǎn)的修改,因此在傳參的時候,用的是指向頭節(jié)點(diǎn)的指針的地址,也就是一個二級指針,如下:
SLTNode* node;//創(chuàng)建一個鏈表,其實(shí)只是定義了一個頭節(jié)點(diǎn)
SLTPushBack(&node);//調(diào)用尾插函數(shù),傳遞的是頭指針的地址
void SLTPushBack(SLTNode** pphead,SLTDataType x)//函數(shù)原型
?在有了引用之后,可以對代碼做如下修改:
SLTNode* node;//創(chuàng)建一個鏈表,其實(shí)只是定義了一個頭節(jié)點(diǎn)
SLTPushBack(node);//調(diào)用尾插函數(shù),直接傳遞頭指針
void SLTPushBack(SLTNode*& pphead,SLTDataType x)//函數(shù)原型
?還可以對結(jié)構(gòu)體指針類型進(jìn)行重定義,像下面這樣:
typedef struct SListNode
{
SLTDataType data;//數(shù)據(jù)域
struct SListNode* next;//指針域
}SLTNode, *PSLTNode;//對結(jié)構(gòu)體指針類型進(jìn)行重定義
PSLTNode node;//創(chuàng)建一個鏈表,其實(shí)只是定義了一個頭節(jié)點(diǎn)
SLTPushBack(node);//調(diào)用尾插函數(shù),直接傳遞頭指針
void SLTPushBack(PSLTNode& pphead,SLTDataType x)//函數(shù)原型
?很多數(shù)據(jù)結(jié)構(gòu)書上都是采用第三種寫法,導(dǎo)致很多新手朋友學(xué)起來比較蒙,其本質(zhì)上就是利用了C++中引用這一概念。
4.2 做返回值
?先來回顧下,普通的傳值返回。
int add(int x, int y)
{
int sum = x + y;
return sum;
}
int main()
{
int a = 5;
int b = 4;
int ret = add(a, b);
return 0;
}
?上面代碼中的add
函數(shù),實(shí)現(xiàn)了一個簡單的兩數(shù)求和,要將求和結(jié)果sum
返回給調(diào)用它的地方,這里采用的是傳值返回,由于sum
是函數(shù)中的一個局部變量,存儲在當(dāng)前函數(shù)的棧幀中,隨著函數(shù)調(diào)用結(jié)束棧幀銷毀,sum也會隨之灰飛煙滅。因此,對于這種傳值返回,會生成一個臨時的中間變量,用來存儲返回值,在返回值比較小的情況下,這個臨時的中間變量一般就是寄存器,下面通過調(diào)試來驗(yàn)證:
?不僅函數(shù)中的普通局部變量在傳值返回的時候會創(chuàng)建臨時的中間變量,函數(shù)中static
修飾的靜態(tài)變量,雖然存儲在內(nèi)存中的靜態(tài)區(qū),不會隨著函數(shù)調(diào)用結(jié)束而銷毀,但是在傳值返回的時候,同樣會創(chuàng)建一個臨時的中間變量,以下面的代碼為例:
int Text()
{
static int a = 10;
return a;
}
int main()
{
int ret = Text();
return 0;
}
?盡管函數(shù)中的a
是一個靜態(tài)變量,沒有存儲在當(dāng)前函數(shù)調(diào)用的棧幀中,但是在返回a
的時候,還是創(chuàng)建了一個臨時的中間變量來存儲a
。因此可以得出結(jié)論:
- 只要是傳值返回,編譯器都會生成一個臨時的中間變量。
- 臨時的中間變量具有常性。
傳引用返回:
?和傳值返回不同,傳引用返回不需要創(chuàng)建臨時的中間變量,但前提是,在函數(shù)調(diào)用結(jié)束,函數(shù)棧幀銷毀后,返回的變量任然存在。換句話說就是,返回的變量不能存儲在函數(shù)調(diào)用所創(chuàng)建的棧幀中,即返回的變量,不能是普通的局部變量,而是存儲在靜態(tài)區(qū)的靜態(tài)變量,或是在堆上動態(tài)申請得到的變量。
局部變量傳引用返回存在的問題:
?引用即別名,傳引用返回,就是給一塊空間取了一個別名,再把這個別名返回。一個局部變量的空間,是函數(shù)棧幀的一部分,這塊空間會隨著函數(shù)調(diào)用結(jié)束,函數(shù)棧幀的銷毀而銷毀,因此給這塊空間取一個別名,再把這個別名返回給調(diào)用它的地方,這顯然是有問題的,因?yàn)檫@塊空間已經(jīng)被釋放了,歸還給了操作系統(tǒng)。
int& add(int x, int y)
{
int sum = x + y;
return sum;
}
int main()
{
int a = 5;
int b = 4;
int ret = add(a, b);
cout << ret << endl;
return 0;
}
?還是上面這個求和代碼,sum
是一個局部變量,但是傳引用返回,結(jié)果貌似沒有什么問題,這是為什么呢?其實(shí),sum
標(biāo)識的這塊空間在函數(shù)調(diào)用結(jié)束,確確實(shí)實(shí)是歸還給了操作系統(tǒng),但是操作系統(tǒng)并沒有將里面存儲的內(nèi)容清理,這就導(dǎo)致打印出來的結(jié)果貌似是正確的??梢詫ι厦娴拇a稍作修改,繼續(xù)驗(yàn)證:
int& add(int x, int y)
{
int sum = x + y;
return sum;
}
int main()
{
int a = 5;
int b = 4;
int& ret = add(a, b);
cout << ret << endl;
printf("hello\n");
cout << ret << endl;
return 0;
}
?這一次驗(yàn)證,最重要的變化是從int ret = add(a, b);
變成了int& ret = add(a, b);
,可不要小瞧了這一個&
,他讓ret
變成了引用,即ret從一個獨(dú)立的變量,變成了一塊空間的別名。原本調(diào)用add
函數(shù),返回sum
所標(biāo)識空間的一個別名,在把這塊空間里的內(nèi)容賦值給ret,而現(xiàn)在,ret也變成了sum所標(biāo)識空間的別名,為什么要這樣做?先看結(jié)果,兩次打印ret的結(jié)果并不相同,第一次僥幸是正確的,因?yàn)?code>sum標(biāo)識的空間在歸還給操作系統(tǒng)后,操作系統(tǒng)并沒有對這塊空間進(jìn)行清理,接著調(diào)用了printf
函數(shù),由于函數(shù)調(diào)用會創(chuàng)建棧幀,sum
標(biāo)識的空間在此次創(chuàng)建的函數(shù)棧幀中被重新使用,這就導(dǎo)致里面存儲的內(nèi)容一定會發(fā)生改變,此時再去打印ret,結(jié)果就是錯誤的。假如這里的ret不是引用,是無法驗(yàn)證出這個錯誤的,因?yàn)榇藭rret有自己單獨(dú)的空間,int ret = add(a, b);
就是一次賦值操作,在第一次賦值后,ret就不會再變化,因此兩次打印的結(jié)果可能僥幸都是正確的,所以需要讓ret變成引用。
?上面說了這么多就是想告訴大家,局部變量傳引用返回,你的結(jié)果可能僥幸是正確的。所以對于局部變量,大家還是老老實(shí)實(shí)的用傳值返回。
引用做返回值的優(yōu)勢:
- 減少拷貝,提高效率。
- 可以同時讀取和修改返回值(重載[ ]就是利用這個優(yōu)勢)
五、傳值、傳引用效率比較
?以值作為參數(shù)或返回值類型,在傳參和返回期間,函數(shù)不會直接傳遞實(shí)參或者將變量本身直接返回,而是傳遞實(shí)參或返回變量的一份臨時拷貝,因此用值作為參數(shù)或者返回值類型,效率是非常地下的,尤其是當(dāng)參數(shù)或者返回值類型非常大時,效率就更低。
參數(shù)對比:
struct A
{
int a[100000];
};
void TestFunc1(A a)
{
;
}
void TestFunc2(A& a)
{
;
}
void TestFunc3(A* a)
{
;
}
//引用傳參————可以提高效率(大對象或者深拷貝的類對象)
void TestRefAndValue()
{
A a;
// 以值作為函數(shù)參數(shù)
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)//就是單純的調(diào)用一萬次這個函數(shù)傳一萬次參
TestFunc1(a);
size_t end1 = clock();
// 以引用作為函數(shù)參數(shù)
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);//這里直接傳的是變量名
size_t end2 = clock();
//以指針作為函數(shù)參數(shù)
size_t begin3 = clock();
for (int i = 0; i < 10000; i++)
{
TestFunc3(&a);
}
size_t end3 = clock();
// 分別計(jì)算兩個函數(shù)運(yùn)行結(jié)束后的時間
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
cout << "TestFunc3(A*)-time:" << end3 - begin3 << endl;
}
?其中,A
類型里面有一個四十萬字節(jié)的數(shù)組,TestFunc1
是值傳遞,TestFunc2
是傳引用,TestFunc3
是傳地址,分別把這三個函數(shù)調(diào)用一萬次,通過結(jié)果可以看出,值傳遞花費(fèi)的時間最長,并且也是最占用空間的,每次調(diào)用TestFunc1函數(shù),都會重新創(chuàng)建一個四十萬字節(jié)的A類型的變量,來存儲實(shí)參,而傳引用,形參只是給實(shí)參所標(biāo)識的內(nèi)存空間取了一個別名,并沒有創(chuàng)建新的空間,傳地址,只會創(chuàng)建一塊空間來存儲實(shí)參的地址,這塊空間在32位機(jī)下是4字節(jié),在64位機(jī)下是8字節(jié)。
返回值對比:
struct A
{
int a[100000];
};
A a;//全局的,函數(shù)棧幀銷毀后還在
// 值返回
A TestFunc1()
{
return a;
}
// 引用返回
A& TestFunc2()
{
return a;
}
void TestReturnByRefOrValue()
{
// 以值作為函數(shù)的返回值類型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();//就讓他返回不接收
size_t end1 = clock();
// 以引用作為函數(shù)的返回值類型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 計(jì)算兩個函數(shù)運(yùn)算完成之后的時間
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
?值返回每次都要創(chuàng)建臨時的中間變量,這就導(dǎo)致效率下降和空間上的浪費(fèi)。
六、常引用
6.1 權(quán)限放大——不被允許
int main()
{
const int a = 10;
int& b = a;//權(quán)限放大
return 0;
}
?上面代碼中,用const
定義了一個常變量a
,接著想給a取一個別名b
,但是編譯的過程中報(bào)錯了:無法從const int 轉(zhuǎn)換為int &。為什么會這樣呢?原因是:權(quán)限可以平移、縮小,但是不能放大。
?a
最初是一個常變量,意味著a一旦定義就不能再修改,而此時引用b
出現(xiàn)了,它是a的一個別名,但是它沒有加const修飾,意味著可以對b進(jìn)行修改,這時就相當(dāng)于權(quán)限的放大,這種情況是不允許的。正確的做法是,給引用b加上const進(jìn)行修飾,即:cconst int& b = a;
,此時屬于權(quán)限的平移。
6.2 權(quán)限平移
int main()
{
const int a = 10;
const int& b = a;//權(quán)限平移
return 0;
}
?上面代碼中的,給常變量a取一個別名b,這里的b
其實(shí)就是一個常引用。
6.3 權(quán)限縮小
int main()
{
int a = 10;
const int& b = a;//錯誤:權(quán)限縮小
int& c = 10//錯誤:權(quán)限縮小
return 0;
}
?上面代碼中,給一個普通的變量a
取了一個別名b
,這個b是一個常引用。這意味著,可以通過a
變量去對內(nèi)存中存儲的數(shù)據(jù)進(jìn)行修改,但是不能通過引用b
去修改內(nèi)存中存儲的數(shù)據(jù)。這就有點(diǎn)類似于:李逵在梁山泊的時候可以喝酒,下了梁山泊以后就叫黑旋風(fēng),不能喝酒一樣。但是李逵在了梁山泊喝了酒,黑旋風(fēng)的肚子里一定也是有酒的(黑旋風(fēng)不能喝酒,管我李逵什么事dog)。這就意味著:通過a
去對內(nèi)存中存儲的數(shù)據(jù)進(jìn)行修改,b
也會跟著變,雖然不能通過b
去修改,但是它會跟著變。
int main()
{
int a = 10;
const int& b = a;
cout << "修改前:" << endl;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
a++;
//b++;//b是一個常引用,不能通過b去修改
cout << "修改后:" << endl;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
return 0;
}
6.4 賦值拷貝不涉及權(quán)限問腿
?上面說到的權(quán)限放大,縮小問題,都是在取別名的時候發(fā)生的,即在定義一個引用變量的時候。權(quán)限的放大、縮小等,針對的必定是同一塊空間,不同的空間有自己的權(quán)限,不存在權(quán)限變更的問題。而引用恰恰就是對同一塊空間取了一個別名而已,賦值則是重新創(chuàng)建了一塊空間。
int main()
{
const int a = 10;
const int b = a;
int c = a;
return 0;
}
?如上面的代碼,可以把常變量a
重新賦值給常變量b
或者普通變量c
,都是可以的,因?yàn)檫@三個變量標(biāo)識了三個獨(dú)立的內(nèi)存空間,它們之間互不影響。
七、臨時變量
?在4.2小節(jié)中提到,函數(shù)的傳值返回,會創(chuàng)建一個臨時變量,在最后的總結(jié)中還寫到:臨時的中間變量具有常性。這條性質(zhì)適用于所有的臨時變量,不只是傳值返回產(chǎn)生的臨時變量具有常性,在類型轉(zhuǎn)換(包括但不限于:強(qiáng)制類型轉(zhuǎn)換、隱式類型轉(zhuǎn)換、整型提升、截?cái)啵┻^程中,也會產(chǎn)生臨時變量,并且這個臨時變量也具有常性。為什么要提這個?
?因?yàn)橐檬轻槍ν粔K空間的操作,引用就是給同一塊空間取別名,既然是同一塊空間,就逃不了會涉及到權(quán)限變化問題,又因?yàn)榕R時變量不經(jīng)過調(diào)試,我們是很難發(fā)現(xiàn)的它的存在,并且臨時變量很特殊,具有常性,所以,我們需要特別注意哪些可能會產(chǎn)生臨時變量的操作。下面舉一些可能會產(chǎn)生臨時變量的例子:
7.1 傳值返回
int Text()
{
int a = 99;
return a;
}
int main()
{
//int& ret = Text();//函數(shù)返回會創(chuàng)建臨時變量
const int& ret = Text();//用引用接收必須要加const修飾
return 0;
}
7.2 類型轉(zhuǎn)換
int main()
{
double a = 3.14;
//int& b = a;//錯誤的類型轉(zhuǎn)換,產(chǎn)生臨時變量
const int& b = a;//正確
return 0;
}
7.3 傳參
void Text1(int& y)
{
cout << y << endl;
}
void Text(const int& y)
{
cout << y << endl;
}
int main()
{
//Text1(1 + 3);//錯誤
Text(1 + 3);//正確
return 0;
}
?上面代碼中的函數(shù)調(diào)用Text1(1 + 3);
是錯誤的,因?yàn)?code>1 + 3的結(jié)果會保存在一個臨時變量里面,同時形參是一個引用,相當(dāng)于要給這個臨時變量取一個別名,但這個臨時變量具有常性,而這里的引用只是一個普通引用,不是常引用,所以就會涉及權(quán)限的放大,導(dǎo)致函數(shù)調(diào)用出錯。Text
函數(shù)的形參是一個常引用,在調(diào)用的時候就不會出錯。文章來源:http://www.zghlxwxcb.cn/news/detail-538158.html
八、引用與指針的區(qū)別
- 引用在概念上定義一個變量的別名,指針存儲一個變量的地址。
- 引用在定義時必須初化,指針沒有要求。
- 引用在初始化時引用一個一個實(shí)體后,就不能再引用其他實(shí)體,而指針可以在任何時候指向任何一個同類型實(shí)體。
- 沒有NULL引用,但有NULL空指針。
- 在
sizeof
中的含義不同,引用結(jié)果為引用類型的大小,但指針始終是地址空間所占字節(jié)個數(shù)(32位機(jī)下占四個字節(jié),64位機(jī)下占八個字節(jié))。 - 引用自加即引用的實(shí)體增加1,指針自加即指針向后偏移一個類型的大小。
- 有多級指針,但是沒多級引用。
- 訪問實(shí)體方式不同。指針顯式解引用,引用編譯器自己做處理。
- 引用比指針使用起來相對更安全。
?今天的分享到這里就結(jié)束啦!如果覺得文章還不錯的話,可以三連支持一下,您的支持就是春人前進(jìn)的動力!文章來源地址http://www.zghlxwxcb.cn/news/detail-538158.html
到了這里,關(guān)于【C++初階】C++入門——引用的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!