前言
上一節我們開始學習了C++,并且對C++有了初步的了解,這一節我們繼續學習C++的基礎,那么廢話不多說,我們正式進入今天的學習
C++中的引用
1.1引用的概念
引用不是新定義一個變量,而是給已存在變量取了一個別名,編譯器不會為引用變量開辟內存空 間,它和它引用的變量共用同一塊內存空間
引用的語法如下:
int a = 0;int& b = a;
第二行的代碼的意思是:給已創建的變量a取一個別名為b,注意這里的&符號不是取地址的意思,而是引用的意思。
當&符號放在一個類型和一個變量的中間位置才叫做引用,不然的話仍然起的是取地址的功能。
int main(void)
{int a = 0;int& b = a;cout << &a << endl;cout << &b << endl;return 0;
}
通過運行以上代碼我們可以知道:引用并沒有重新開辟一個空間,而是和原變量共用一個空間
我們對b變量執行++操作也相當于對a變量執行++操作,因為它們本質上就是同一個變量
引用的概念有點像我們在日常生活中給別人起的外號
學到這里可能有人會覺得引用沒有什么用,只是給變量取了一個“外號”罷了,其實并不是這樣的。我們在C語言中若是要完成交換兩個不同變量中的數據內容時,我們創建swap函數應該要傳入兩個變量的地址,如果是傳值調用則無法完成交換的功能,因為在出函數的時候臨時的變量被銷毀。但是有了引用就可以不用使用指針,引用傳參修改的值可以直接影響到原來的參數
void swap(int& x1, int& x2)
{int tmp = x1;x1 = x2;x2 = tmp;
}int main(void)
{int x = 1;int y = 2;swap(x, y);cout << "x = " << x << endl;cout << "y = " << y << endl;return 0;
}
1.2引用的注意事項
1. 引用在定義時必須初始化
int a = 10;// int& ra; // 該條語句編譯時會出錯
2. 一個變量可以有多個引用
int a = 0;
int& ra = a;
int& rra = a;
3. 引用一旦引用一個實體,再不能引用其他實體
int a = 0;
int& b = a;
int x = 1;
b = x;
//這種情況下并不是把b變成x的引用,而是給b中賦值為1,也就是a改為1
4.引用的類型必須和引用實體的類型相同
1.3傳引用返回
首先我們需要了解一個概念,叫做傳值返回:
int Count()
{int n = 0;n++;//……return n;
}int main(void)
{int ret = Count();return 0;
}
在出函數的時候變量n已經被銷毀了,所以函數并不是直接拿n作為返回值,此時編譯器用以下兩種方式返回
1.把n拷貝到一個寄存器中,讓寄存器充當返回值,該情況適用于返回值比較小的情況
2.當n的取值比較大的的時候,會在Count函數和main函數之間的空隙里面壓出一個空間,將值存入并將它作為一個返回值
隨后我們來了解一下引用返回:
int& Count()
{int n = 0;n++;//……return n;
}int main(void)
{int ret = Count();return 0;
}
傳引用返回相當于返回的是值n的別名,這個概念看起來有些奇怪,因為原來的變量n已經被銷毀了,而返回的值又是已經被銷毀的變量的別名,所以有的人可能認為這個概念的性質和野指針差不多 。首先我們需要知道的是,當一個變量或者一個空間被銷毀了,仍然可以返回這個已經被銷毀的空間里面的變量的別名,因為空間被銷毀并不是意味著這一塊空間已經沒有了。空間銷毀的概念有點類似于我們在日常生活中的“退房”,我們銷毀了的那塊空間并不是不能使用了,而是收回了對那塊空間的使用權。但是傳引用返回是比較危險的,傳引用返回一般都會報一個警告,返回來的值可能是正常值也有可能是一個隨機值,這取決于函數棧幀銷毀了以后原空間里面的內容會不會被重置為一個隨機值
int& Count()
{int n = 0;n++;//……return n;
}int main(void)
{int& ret = Count();cout << ret << endl;cout << ret << endl;return 0;
}
我們來看一下這樣的情況下為什么第二次打印的結果是一個隨機值:
我們首先需要了解,cout是一個函數調用,調用的是一個運算符重載的函數,cout是ostream類型
我們在第一次調用函數的時候并不會出現任何的問題,而當我們第二次調用函數的時候,創建棧幀的區域還是在第一次調用所創建的區域,此時區域內原先的數據就已經被覆蓋了,所以打印出來的是一個隨機值
我們來舉一個例子就能更好地理解這一點:
int& Add(int a, int b)
{int c = a + b;return c;
}int main(void)
{int& ret = Add(1, 2);Add(3, 4);cout << "Add(1,2) is :" << ret << endl;return 0;
}
因為本來執行Add操作計算出來的值是3,但是我們在第二次調用函數的時候算出來的值是7,第二次調用覆蓋了第一次調用,所以此時去打印算出來的值是7
所以:如果函數返回時,出了函數作用域,如果返回對象還在(還沒有還給系統),則可以使用引用返回,如果已經還給系統了,則必須使用傳值返回
我們在C語言中要完成順序表的第i個位置的數據和修改需要兩個步驟:
struct Seqlist
{int* a;int size;int capacity;
};
//讀取第i個位置的值
//修改第i個位置的值
int SLAT(struct Seqlist* ps, int i)
{assert(i < ps->size);//...return ps->a[i];
}int SLModify(struct Seqlist* ps, int i, int x)
{assert(i < ps->size);//...return ps->a[i] = x;
}
而在C++中使用引用返回就可以簡化這一過程:
struct Seqlist
{int* a;int size;int capacity;
};
//讀取&修改第i個位置的值
int& SLAT(struct Seqlist& ps, int i)
{assert(i < ps.size);//...return ps.a[i];
}
int main(void)
{struct Seqlist s;//...SLAT(s, i) = 1;cout << SLAT(s, i) << endl;return 0;
}
當出了作用域時,ps.a[i] 仍然存在,所以可以直接對其修改,修改過后的值仍然存在
怎么解釋這個現象呢?
因為傳值返回出函數的時候返回的是一份臨時的拷貝,是不可以直接修改的;
而傳引用返回返回的是別名,是可以被修改的
傳值、傳引用效率比較
以值作為參數或者返回值類型,在傳參和返回期間,函數不會直接傳遞實參或者將變量本身直接返回,而是傳遞實參或者返回變量的一份臨時的拷貝,因此用值作為參數或者返回值類型,效率是非常低下的,尤其是當參數或者返回值類型非常大時,效率就更低。如果是傳引用的話就能夠提高效率,當然,傳指針也可以
#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(void)
{TestRefAndValue();return 0;
}
由此我們可以看出:傳引用返回比傳值返回的效率要高
那么在什么情況下適合使用傳引用返回呢?
1.返回的是一個全局對象
2.返回的是一個靜態對象
3.在堆上動態申請的對象
因為這三個變量出了作用域仍然存在,使用傳引用返回就可以在確保沒有錯誤的情況下提高效率
此時我們就可以總結一下:
傳引用傳參的優勢(在任何的情況下都可以)
1.提高效率
2.輸出型參數(形參的修改可以影響實參)
傳引用返回的優勢(出了函數作用域對象還在才可以使用)
1.提高效率
2.修改返回的對象
1.4常引用
int main(void)
{const int a = 0;int& b = a;return 0;
}
在這種情況下,編譯是無法通過的,因為這是一種權限的放大。在引用的過程中權限是可以平移和縮小的,但是不能被放大
權限的平移:
const int a = 0;
//權限的平移
const int& c = a;
權限的縮小:
int x = 0;
//權限的縮小
const int& y = x;
注意:賦值是不受權限的影響的,因為賦值是一種拷貝,b的修改不影響a
const int a = 0;
//賦值
int b = a;
我們再來看一個有趣的現象:
我們這樣寫出的代碼編譯會失敗:
int i = 0;
double& d = i;
而這樣寫編譯就不會報錯:
int i = 0;
const double& d = i;
第一種情況其實是一種權限的放大,臨時變量具有常性
而第二種情況我們加入了const,此時就沒有權限的放大了
1.5引用與指針的區別
我們通過之前的學習可以知道:指針在創建的時候會在內存中開辟空間,而在語法上來看引用是不會額外的開辟空間的(其實在底層實現上引用也會占用空間,但是在語法的層面上不會占用空間)
引用和指針的不同點:
1.引用是定義一個變量的別名,而指針是存儲一個變量的地址
2.引用在定義的時候必須要初始化,而指針沒有要求
3.引用在初始化時引用一個實體以后,就不能再引用其他的實體,而指針可以在任何時候指向任何一個同類型實體
4.沒有NULL引用,但是有NULL指針
5.sizeof的含義不同:引用結果是引用類型的大小,但是指針始終是地址空間所占用的字節數(4或者8)
6. 引用自加即引用的實體增加1,指針自加即指針向后偏移一個類型的大小
7. 有多級指針,但是沒有多級引用
8. 訪問實體方式不同,指針需要顯式解引用,引用編譯器自己處理
9. 引用比指針使用起來相對更安全
結尾
引用是C++中的一個重要的概念,我們需要加深對引用的理解,有助于我們更好的學習C++,那么本節的內容到此就結束了,謝謝您的瀏覽!!!