【C++系列】引用与临时变量
文章目錄
- ??引用的概念
- ??引用的作用
- ??1. 作為函數參數,更加簡潔,能減少內存的消耗。
- ??編譯器是如何檢查越界訪問的
- ??2. 引用作為返回值
前言
- 🔖 分享每一次的學習,期待你我都有收獲。
- 🎇 歡迎🔎關注👍點贊??收藏?評論,共同進步!
- 🌊 “要足夠優秀,才能接住上天給的驚喜和機會”
- 💬 博主水平有限,如若有誤,請多指正,萬分感謝!
??引用的概念
引用不是新定義一個變量,
而是給已存在變量取了一個別名。
編譯器不會為引用變量開辟內存空間,
它和它引用的變量共用同一塊內存空間。
說人話就是,引用就是取小名,平常說的二狗子啥的都是小名。
就比如這個人,他的伙伴喜歡親切地叫他綠藻頭,當然也可以直接叫他索隆,但無論是叫他綠藻頭還是索隆,叫的都是這個人,兩種稱呼,一個身體。引用也是這樣的,在語法上,雖然稱呼不同,但是它們都是指同一塊內存空間
我們來觀察一下這一段代碼
#include<iostream> using namespace std;int main() {int a = 10;int& b = a;return 0; }可以看到,我們定義的b與a具有相同的值,且他們的地址也是相同的,再次印證了b只是a的別名,b就是a。
引用特性:
常引用
int main() {const int a = 10;int& x = a; // ① 錯誤 正確寫法:const int& x = a;int b = 10;const int& y = b; // ② 正確int c = 0;double z = c //不同類型之間的計算——>隱式轉換,正確int c = 0;double& z = c; // ③ 錯誤int d = 0;const double& s = d;// ④ 正確return 0; }① a為const修飾的常變量,x為a的別名卻沒有用const加以修飾,也就是說,a不能改變自己的值,但是用它的別名卻能改變它的值,屬于權限的放大,因此該語句無法通過編譯。
②b為變量,可讀可寫的權限,它的別名只使用了讀的權限,屬于權限的縮小,這是允許的,就好比權利與義務的關系,有些權利我們雖然有,但是我們可以選擇放棄,前提是我們擁有這種權利。 ①則體現了沒有的權利不能隨意擴大。
③與④是一組對比
我們先來看這樣一段代碼
int main() {int a = 10;double b = 0;double c = 0;b = a; //int ->double 隱式轉換c = (double)a; //強制類型轉換return 0; }不同類型之間的計算會發生隱式轉換,一個變量可以進行強制類型轉換,但無論是這兩種中的哪一種,都是借由臨時變量進行的,不會改變變量本身。
臨時變量具有常性,其值無法被修改。
同理
int c = 0;double& z = c; // ③ 錯誤這里也是先將c的值交給臨時變量存儲,再給z,也就是說z不是c的別名,而是臨時變量的別名,而臨時變量無法被修改,因此z必須用const修飾。
int c = 0;const double& z = c; // 正確我們不妨再看一下c的地址和z的地址。
再次印證了轉換的過程產生了臨時變量,z是臨時變量的別名,而不是c的別名,因此z的地址是臨時變量的地址。
??引用的作用
??1. 作為函數參數,更加簡潔,能減少內存的消耗。
在c語言中,我們學習了指針作為函數參數,其實引用效果就類似于指針。
C語言版
#include<stdio.h>void Swap(int* a, int* b) {int tmp = *a;*a = *b;*b = tmp; }int main() {int a = 10;int b = 20;Swap(&a, &b);return 0; }C++版
void Swap(int&a,int &b) {int tmp = a;a = b;b = tmp; }int main() {int a = 10;int& b = a;return 0; }- 函數傳值和傳引用的效率比較
我們知道,引用只是一塊內存空間的別名,
因此函數傳引用,它并不會耗費額外的內存空間。
而實參傳值時,形參只是實參的一份臨時拷貝,它會在內存中開辟一塊臨時空間,然后拷貝實參的內容。
并且實參所占空間越大,則開辟的臨時空間就越大,對性能的消耗也就越大。
#include<iostream> #include<time.h> using namespace std;struct Node {int a[10000]; };void Func1(Node a) //傳值 {; }void Func2(Node& a) //傳引用 {}void TestEffic() {Node a; int begin1 = clock();for (int i = 0; i < 100000; i++){Func1(a); //傳值}int end1 = clock();int begin2 = clock();for (int i = 0; i < 100000; i++){Func2(a); //傳引用}int end2 = clock();cout << "Func1(Node) = " << end1 - begin1 << endl;cout << "Func2(Node&) = " << end2 - begin2 << endl;}int main() {TestEffic();return 0; }從數字上就可以清晰看到他們運行所花費的時間的差距了。
這就是由于每次引用并不需要再開空間對A進行拷貝,而傳值時每次都要建立臨時空間對A進行拷貝。
需要開的數組越大,則他們的差距會越大。
在講第二個作用前,要先講一件事——
??編譯器是如何檢查越界訪問的
編譯器對越界訪問的檢查是——抽查。
我們看這么一段代碼:
int main() {int a[6] = { 1,2,3,4};a[4] = 10;return 0; }這段代碼很明顯越界訪問了,編譯器也確實檢查到了,于是程序就崩了。但是如果我們再往更后面一點的內存訪問又會發生什么呢?
int main() {int a[4] = {1,2,3,4}; //數組下標范圍 0~3//a[4] = 10;a[6] = 16; //這次往更后面訪問內存塊,訪問a[6]printf("a[6] = %d\n", a[6]);return 0; }我們看到,程序非但沒有崩,我們不但越界將內存空間中的值更改,甚至還將它打印出來了,編譯器也只是給了我們一個警告,并沒有報錯。因此,編譯器對于內存的越界訪問檢查,確實是采取抽查的方式。
編譯器通常只會在合法內存塊后面,一兩個內存塊的位置設立檢查,因為正常情況下,這里是最容易發生越界訪問的地方,如果在每個內存空間都設立檢查,那樣對性能的消耗太大,不現實。
就好比交警檢查酒駕,不可能一天24小時設立檢查點,那樣對于社會的運轉也有很大的消耗和不便,不科學。
因此通常會在半夜,凌晨,或是利用酒駕人的心理,在小路上設立檢查點,針對這些最容易查到酒駕的情況,設立檢查點。
我們繼續講引用的第二個作用。
??2. 引用作為返回值
如果內存空間在函數結束時不會被銷毀,才可以作為返回值引用返回這塊內存空間本身
我們看這么一段代碼
int Add(int a, int b) {int c = a + b;return c; }int main() {int a = 1;int b = 2;int& ret = Add(a, b); //如果返回的是c,那我們直接引用別名接收ccout << "ret = " << ret << endl;return 0; }可能有些同學會存在這樣的誤區,當我們調用Add函數后,返回的是c這個變量。
如果這樣的結論成立,那么這段代碼就應該是能通過編譯的,但其實它不能,編譯器給我們報了錯誤
如果我們借助前面介紹的常引用,稍微把代碼改一下
//int& ret = Add(a, b);const int& ret = Add(a,b);編譯就通過了,這說明返回的不是c本身,而是一個臨時變量。
實際在內存中是這么個過程:
有這么兩塊棧幀。
當我們調用完函數時,main函數上開辟了一塊臨時空間,用來接收c返回來的值,然后c內存空間被銷毀。
被銷毀后,c的內存空間會被賦上隨機值,如果這時候系統還沒來得及給c一個隨機值,那c就還保留著原來的值。
看看這些代碼的運行結果是什么
int& Add(int a, int b) //注意:這里我們的返回值用了引用,因此返回的是c的內存空間 {int c = a + b;return c; //返回c本身 }int main() {int a = 1;int b = 2;int& ret = Add(a, b); // c的別名。cout << "ret = " << ret << endl;return 0; }雖然c已經被回收,但其中的值還未被賦上隨機值
int& Add(int a, int b) //注意:這里我們的返回值用了引用,因此返回的是c的內存空間 {int c = a + b;return c; //返回c本身 }int main() {int a = 1;int b = 2;int& ret = Add(a, b); // c的別名。Add(6, 8);cout << "ret = " << ret << endl;return 0; }
再次調用Add函數,c的內存空間中所保留的值被改變,且出函數后c的內存空間還未被賦上隨機值。
這里要解釋一下,為什么第二次調用函數,還是用原來c的那塊內存空間,而不是去用其他的內存空間。
因為銷毀后,沒有進行其他需要創建棧幀執行代碼的行為,所以第二次調Add函數的時候,調用的還是同一塊棧幀,所以改變的還是原來c那塊空間的內容。
int& Add(int a, int b) //注意:這里我們的返回值用了引用,因此返回的是c的內存空間 {int c = a + b;return c; //返回c本身 }int main() {int a = 1;int b = 2;int& ret = Add(a, b); // c的別名。Add(6, 8);cout << "hello world" << endl; //至此,原來調用Add的那塊棧幀被修改cout << "ret = " << ret << endl;return 0; }
上述幾種情況都涉及到了越界訪問,但都可以運行,這是因為越界訪問其實是不容易被檢查出來的,因為是抽查。
實際上,在上述幾種情況中,c所在的棧幀在函數調用完成時就已經被回收了,不應該引用返回,再次印證,符合引用返回的條件是函數調用完成時,返回對象還未還給系統,才可以引用返回。
因此,如果用static修飾變量c——c被轉移到靜態區,Add函數調用完后,c的內存空間還在,我們在調用其它函數,開辟棧幀時,才不會改變c的數據,滿足引用返回的使用條件,這時候才可以使用引用返回。
總結
以上是生活随笔為你收集整理的【C++系列】引用与临时变量的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: matlab无功仿真,第八章MATLAB
- 下一篇: 巧妙地用继电器实现直流电机正反转