目錄
引用
常引用
指針與引用的關系
小拓展
引用的價值
做形參
?傳值、傳引用的效率比較
做返回值
函數傳值返回
函數傳引用返回(錯誤示范)
野引用(錯誤示范)
引用的正常應用
值和引用作為返回值類型的性能比較
引用和指針的區別
語法上
底層(匯編)上
引用
基本概念:引用是給已存在變量取了一個別名
李逵,在家稱為“鐵牛”,江湖上人稱“黑旋風”
特點:和原變量共用同一塊內存空間(地址相同,且牽一發而動全身),同一變量可以有多個別名
格式:類型& 引用變量名(對象名) = 引用實體
注意事項:
1、C++中的&依然可以表示取地址運算符和按位與運算符
2、引用類型必須和引用實體是同種類型的
3、當引用實體和它的別名屬于不同的域時,別名和引用實體的名字可以相同但是不建議這樣做
#include <iostream>
using namespace std;int main()
{int a = 10;int& b = a;cout << &a << endl;cout << &b << endl << endl;a++;b++;cout << a << endl;cout << b << endl << endl;return 0;
}
4、 引用必須在開始就初始化(說明自己是誰的別名)
int a = 0;//wrong
int& b;
b = a; //right
int &b = a;
5、引用定義后,不能改變指向(別名與綁定后無法修改,對別名的任何操作就是對實體的操作)
int a = 0;
int& b = a;
int c = 1;
b = c; // 這里不是讓b指向c,而是將1賦值給了引用實體a
常引用
定義:使用?const
?修飾的引用
特點:被const修飾的引用不能被修改
void printValue(const int& value) {// 這里無法通過value來修改原始值,如果輸入value += 10;編譯器就會報錯cout << "Value: " << value << endl;
}int main() {int num = 10;// 使用常量引?來傳遞numprintValue(num);return 0;
}
指針與引用的關系
基本概念:指針和引用是類似的,指針找到并修改你,別名直接修改你(二者存在依賴關系)
????????雖然C++的引用可以對使用指針后比較復雜的場景進行一些替換,讓代碼更簡單易懂,但是因
為引用在定義后不能改變指向(刪除雙向鏈表的一個結點時,?需要用改變前去指針和后繼指針,
用不能做到這一點),而指針可以,所以在C++中引用并不能替代指針:
struct Node
{struct Node* next;struct Node* prev;int val;
};
??????之前我們在寫單鏈表時,用二級指針pphead接收頭指針的地址,而當我們有了引用的概念后,
可以以一種更好理解的方式實現下列單鏈表的(偽)代碼:
#include <stdio.h>
struct Node
{struct Node* next;struct Node* prev;int val;
};//二級指針版本
void PushBack(struct Node** pphead,int x)
{*pphead = newnode;
}//引用版本
void PushBack(struct Node*& phead, int x)//為指針取別名,plist指針的別名是phead
{phead = newnode;
}int main()
{struct Node* plist = NULL;PushBack(plist,0);return 0;
}
????????我們將原來的二級指針pphead換為struct Node*& phead,phead是plist的別名,對phead的操
作就是對plist的操作,這使得代碼看起來更加簡單。單鏈表使用二級指針的原因就是為了能夠向
頭指針中存入新節點的值(不用的話對于值得修改不能被帶出尾插函數),這里直接通過引用就實
現了這一目的所以不需要再去考慮傳值調用的問題
關于單鏈表的內容可以查看:單鏈表的實現(全注釋promax版)
小拓展
#include <stdio.h> typedef struct Node
{struct Node* next;struct Node* prev;int val;
}LNode,*PNode;//引用版本
void PushBack(PNode& phead, int x)//為指針取別名,plist指針的別名是phead
{phead = newnode;
}int main()
{PNode plist = NULL;PushBack(plist,0);return 0;
}
-
LNode
: 是?struct Node
?的別名(當你使用?LNode
?時,就相當于使用了?struct Node)
-
PNode
: 是結構體類型的指針struct Node*的別名
C++百分之八十的場景都在使用引用,剩下的才會用指針
引用的價值
做形參
????????原來我們在交換兩個變量的時候,向Swap函數中傳遞的是地址,形參是實參的拷貝。實參必
須傳遞的是地址,否則交換后的結果無法傳遞回去,有了別名后就不需要傳遞地址了,形參可以直
接寫成實參的別名即可,對于別名的修改就相當于原來的實參的修改:
#include <iostream>
using namespace std;//原版本,傳遞地址
void Swap(int* left, int* right)
{int tmp;tmp = *left;*left = *right;*right = tmp;
}int main()
{int i = 10;int j = 20;Swap(&i, &j);cout << i << endl;cout << j << endl;return 0;
}//現版本,引用
void Swap(int& left, int& right)
{int tmp = left;left = right;right = tmp;
}int main()
{int i = 10;int j = 20;Swap(i, j);cout << i << endl;cout << j << endl;return 0;
}
從語法上來講,left和right是i和j的拷貝,int& left和int& right是i和j的別名
🤡從底層上來講,這里先不講,我不會🤡?
?傳值、傳引用的效率比較
#include <stdio.h>
#include <time.h>
#include <iostream>
using namespace std;struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}int main()
{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;return 0;
}
結論:引用做形參時是輸出型參數,且當對象較大時,減少拷貝提高效率
這些效果指針也可以實現,但是沒引用方便?
輸出型參數:傳遞給函數的參數數據,函數可以使用且能修改這些數據
輸入型參數:傳遞給函數的參數數據,函數可以使用但不能修改這些數據
做返回值
函數傳值返回
? ? ? ? main函數開辟了一塊棧幀,接著又調用了func函數,再開辟一塊幀棧,然后func返回a的
值并結束,然后main函數又想接收func函數的返回值a,但是返回值a已經在func函數結束后被銷
毀了,a空間中所存放的值已經不再被保證有效,所以此時ret得到是隨機值(也有可能是原來的
值,這相當于你雞蛋碎在大街上了,你記著位置回去找,還能在地上找到蛋黃。如果等久一點,再
回去找,那就不一定能找到啥了),但是在大多數情況下,編譯器在棧幀銷毀時會將返回值存儲在
寄存器中(對象比較小時)或者其他適當位置(除了函數返回值之外,編譯器還可以選擇使用寄存
器來保存一些重要的局部變量或者臨時變量,以減少對內存的讀寫作),最后寄存器中存放的原來返回值的值會交給ret
????????在函數結束后硬要把局部變量搞成一個隨機值是一件沒有意義的事情。只能說函數結束后局部變量的值【不再保證有效】
????????銷毀是需要花點力氣的,函數銷毀后,原來函數的那一片空間變成空閑區域,隨時會再次被使用。大部分情況下,你還能輸出,是因為沒人用到了那片空閑區域!然而機器也懶得去銷毀,重置內存也得費電。
函數傳引用返回(錯誤示范)
????????func函數的返回值是一個int&類型的引用,即返回值是局部變量a的引用(別名),而當函數
返回該引用時,a的值已經不再保證有效了(隨機值或者原值)
結論:函數傳值返回的是返回變量的拷貝,函數傳引用返回的是返回變量的引用(別名)
野引用(錯誤示范)
????????func函數返回的是a的引用(別名),那么ret就是a的引用的引用,而a的那片空間在func函
數銷毀時已經不再保證有效了,所以ret就是一個野引用(在程序運行過程中無法保證該內存空間
仍然有效或包含原始值(因為它可能已被其他數據覆蓋),因此稱之為野引用)
結論:返回變量(局部變量)出了函數作用域就被銷毀時,不能用引用返回(薛定諤的🐱)
引用的正常應用
全局變量、靜態變量、堆上分配對象等內容可以用引用返回:
- 全局變量:全局變量在程序運行期間始終存在,因此可以安全地通過引用返回
- 靜態變量:靜態變量也類似于全局常駐內存,在程序整個執行周期中都存在
- 堆上分配對象:如果一個對象是通過?
new
?運算符在堆上動態分配內存創建的,則其生命周期由?new
?和?delete
?控制,并不受限于函數作用域
①int a = 10;
int& func()
{②static int a = 0;return a;
}int main()
{int& ret = func();cout << ret << endl;return 0;
}
堆上分配對象的例子我不會🤡
這是C語言(參雜了一點C++)實現順序表的簡化代碼:
#include <iostream>
#include <assert.h>
#include <stdio.h>using namespace std;struct SeqList
{int* a;int size;int capacity;
};//初始化
void SLInit(SeqList& sl)//利用引用了
{sl.a = (int*)malloc(sizeof(int) * 4);//... sl.size = 0;sl.capacity = 4;
}void SLPushBack(SeqList& sl, int x)
{//...(擴容)sl.a[sl.size++] = x;
}//修改
void SLModity(SeqList& sl, int pos, int x)
{assert(pos >= 0);assert(pos <= sl.size);sl.a[pos] = x;
}//獲取pos位置的值
int SLGet(SeqList& sl, int pos)
{assert(pos >= 0);assert(pos <= sl.size);return sl.a[pos];
}int main()
{SeqList s;//C++將stryct SeqList變為了類,所以可以直接用SeqListSLInit(s);SLPushBack(s, 1);SLPushBack(s, 2);SLPushBack(s, 3);SLPushBack(s, 4);for (int i = 0; i < s.size; i++){cout << SLGet(s,i) << " ";}cout << endl;for (int i = 0; i < s.size; i++){int val = SLGet(s,i);if (val % 2 == 0){SLModity(s, i, val * 2);}}cout << endl;for (int i = 0; i < s.size; i++){cout << SLGet(s, i) << " ";}cout << endl;return 0;
}
這是完全使用C++語法寫出的順序表代碼:
#include <iostream>
#include <assert.h>
#include <stdio.h>using namespace std;struct SeqList
{//成員變量int* a;int size;int capacity;//成員函數//C++的結構體(類)除了可以定義變量還可以定義函數void Init()//可以不寫前綴{a = (int*)malloc(sizeof(int) * 4);//... size = 0;capacity = 4;}void PushBack(int x){//...(擴容)a[size++] = x;}int& Get(int pos)//加上一個引用就可以代替原來的SLModify和SLGet兩個函數的作用{assert(pos >= 0);assert(pos <= size);return a[pos];//由于數組中pos位置的空間是由malloc開辟的,如果不主動釋放它就一直都在,所以即使函數銷毀,該空間也不會銷毀,該空間中的值也會被保留?}
};int main()
{SeqList s;//C++將stryct SeqList變為了類,所以可以直接用SeqLists.Init();s.PushBack(1);s.PushBack(2);s.PushBack(3);s.PushBack(4);for (int i = 0; i < s.size; i++){cout << s.Get(i) << " ";}cout << endl;//將滿足條件的數組pos位置的值進行修改for (int i = 0; i < s.size; i++){if (s.Get(i) % 2 == 0){s.Get(i) *= 2;}}cout << endl;for (int i = 0; i < s.size; i++){cout << SLGet(s, i) << " ";}cout << endl;return 0;
}
- C語言:數據與函數分離,想要訪問數據就要將數據作為參數傳遞給函數
- C++:數據和函數不分離,都處于一個類中(實際上也傳了編譯器做的但是現在沒學到)可以直接用(不需要再傳一個結構體類型的指針將存儲在結構體中的數據傳遞)
????????在這段代碼中我們不僅僅需要關注的是將原本放在外部的尾插、初始化等函數放在了結構體
中,還需要注意在這里我們用Get一個函數就可以實現原來SLGet和SLModify兩個函數的作用(讀
寫pos位置的數據)這是因為:C++規定臨時變量具有常性(特指存儲在寄存器中的變量,雖然
pos位置的值除非主動釋放否則不會銷毀,但是可以將該值拷貝一份放入寄存器中作為函數的返回
值),臨時變量默認被const修飾無法修改,如果在試圖修改pos位置的值時就會出現不可修改的
左值的報錯,但是即使C++沒有做出這一規定,我們仍然不能做到對數組pos位置的值進行修改,
因為臨時變量只是原來該位置數組的一個拷貝,對拷貝內容的修改無法影響到原來位置的數值,這
兩種情況都只能對pos位置的值進行讀取而不能進行修改,而當我們加上了一個引用,就可以實現
對pos位置的值的修改,此時Get函數返回的就不是一個臨時變量(函數返回引用不會產生臨時變
量),而是a[pos]的別名,至于誰的別名作者不知道就不做解釋,只需要此時我們既可以讀取到
pos位置的值也可以對該值進行修改?
????????完成對數組pos位置的值的修改還有一個前提就是數組pos位置存放值的空間在函數結束后仍然可以保證值得有效,否則就會出現傳引用返回(錯誤示范)中出現得值不保證有效的問題,對于這一點由于數組空間是由mallo開辟的,除非主動銷毀否則不會釋放,即使函數結束數組pos位置存放值得空間仍然存在,值仍然保證有效,所以可以放心引用該實體對象
值和引用作為返回值類型的性能比較
#include <iostream>
#include <time.h>
using namespace std;struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }int main()
{// 以值作為函數的返回值類型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;//引用語法上不開空間ra = 20;int* pa = &a;//指針語法上開空間*pa = 20;return 0;
}
②、引用必須初始化,指針可以初始胡也可以不初始化(所以指針更容易出現野的情況)
③、引用不可以改變指向,指針可以改變指向
④、引用相對安全,沒有空引用,但是有空指針,容易出現野指針,但是不容易出現野引用
⑤、在sizeof中含義不同:引用結果為引用類型的大小,但指針始終是地址空間所占字節個數
⑥、引用自加即引用的實體增加1,指針自加即指針向后偏移一個類型的大小
⑦、有多級指針,但是沒有多級引用
⑧、訪問實體方式不同,指針需要顯式解引用,引用編譯器自己處理
⑨、引用比指針使用起來相對更安全
底層(匯編)上
對于①中的引用,在底層(匯編)上的情況是這樣的:
????????“003F2026 lea eax,[a]”:取a的地址放在eax寄存器,故引用在底層層面需要開空間
結論:
- 引用底層是用指針實現的
- 語法含義和底層實現是背離的(魚香肉絲沒有🐟)
匯編層面上,沒有引用,都是指針,引用編譯后也轉換成指針了
~over~