目錄
一、引用的概念
二、引用的特性
1、定義時必須初始化
2、一個變量可以有多個引用
3、引用一旦綁定實體就不能更改
三、const引用(常引用)
1、const引用的基本特性
2、臨時對象與const引用
3、臨時對象的特性
4、const?引用作為函數形參
1. 基本示例(類型不匹配時創建臨時對象)
2. 數值類型轉換(臨時對象存儲轉換后的值)
3. 如果形參是?非 const 引用,則不允許綁定臨時對象
4. 類對象的隱式轉換(臨時對象 +?const?引用)
總結
四、引用的使用場景
1、引用作為函數參數
引用傳參的工程價值
2、引用作為函數返回值
返回引用最佳實踐
注意:
五、引用與指針的區別(超重要!!!)
引用與指針的工程選擇準則
C++引用與其他語言引用的本質區別
關鍵差異點
六、引用的優點
七、數據結構中的引用實踐
1、C風格二級指針實現解析
關鍵點解析
調用方式
2、C++引用風格實現解析
關鍵改進
調用方式
3、兩種實現的底層對比
內存布局示例
八、現代C++中的引用演進(后面會學到,現在先了解)
1、右值引用(C++11引入)
2、完美轉發
3、結構化綁定(C++17)
九、引用使用的注意事項
1、避免懸垂引用
2、接口設計原則
3、多線程環境
一、引用的概念
????????引用(Reference)是C++中一種重要的復合類型,它不是定義一個新的變量,而是為已存在的變量提供一個別名。編譯器不會為引用變量單獨分配內存空間,引用變量與其引用的實體共享同一塊內存空間。
????????C++中為了避免引入太多的運算符,會復用C語言的一些符號,比如前面的>>和<<,這里引用也和取地址使用了同?個符號&,大家注意使用方法角度區分就可以。(吐槽一下,這個問題其實挺坑的,個人覺得用更多符號反而更好,不容易混淆)
基本語法形式:
類型& 引用變量名 = 引用實體;
重要說明:引用類型必須與引用實體是同種類型。?
示例代碼:
#include <iostream>
using namespace std;int main() {int a = 10;int& b = a; // 給變量a取一個別名bcout << "a = " << a << endl; // 輸出10cout << "b = " << b << endl; // 輸出10b = 20; // 通過引用修改變量值cout << "a = " << a << endl; // 輸出20cout << "b = " << b << endl; // 輸出20return 0;
}
二、引用的特性
1、定義時必須初始化
-
引用必須在聲明時進行初始化,不能先聲明后賦值
-
正確示例:
int a = 10; int& b = a; // 正確:定義時初始化
-
錯誤示例:
int c = 10; int &d; // 錯誤:未初始化 d = c;
2、一個變量可以有多個引用
int a = 10;
int& b = a;
int& c = a;
int& d = a;
此時,b、c、d都是變量a的別名。
3、引用一旦綁定實體就不能更改
-
引用在初始化后不能改為指向其他實體
-
示例:
int a = 10; int& b = a; int c = 20; b = c; // 這不是改變引用指向,而是將a的值改為20
三、const引用(常引用)
????????上面提到,引用類型必須和引用實體是同種類型的。但是僅僅是同種類型,還不能保證能夠引用成功,我們若用一個普通引用類型去引用其對應的類型,但該類型被const所修飾,那么引用將不會成功。
常引用(const reference)用于引用常量或臨時對象,具有以下特點:
-
可以引用常量
-
可以延長臨時對象的生命周期
-
不能通過常引用修改被引用的對象
示例:
int main() {const int a = 10;// int& ra = a; // 錯誤:不能用普通引用引用常量const int& ra = a; // 正確// int& b = 10; // 錯誤:不能引用字面常量const int& b = 10; // 正確double pi = 3.14159;// int& rpi = pi; // 錯誤:類型不匹配const int& rpi = pi; // 正確:會發生隱式轉換return 0;
}
????????我們可以將被const修飾了的類型理解為安全的類型,因為其不能被修改。我們若將一個安全的類型交給一個不安全的類型(可被修改),那么將不會成功。?
1、const引用的基本特性
const引用是C++中一種特殊的引用類型,具有以下重要特性:
-
引用const對象:必須使用const引用來引用const對象
const int a = 10; const int& ra = a; // 正確 int& rb = a; // 錯誤:不能使用非const引用引用const對象,這是權限的放大
-
引用普通對象:const引用可以引用普通對象,這是權限的縮小
int b = 20; const int& rb = b; // 正確:權限縮小
-
權限規則:
-
權限可以縮小(從可修改到只讀)
-
但不能放大(從只讀到可修改)
-
2、臨時對象與const引用
????????C++中有幾種常見情況會產生臨時對象(以下各點都同理):臨時對象具有常性!!!(重要!!!)
-
表達式結果:
int a = 5; const int& rb = a * 3; // 正確:a*3的結果存儲在臨時對象中 int& rc = a * 3; // 錯誤:臨時對象具有常性
-
類型轉換:
double d = 12.34; const int& rd = d; // 正確:類型轉換產生臨時int對象 int& re = d; // 錯誤:臨時對象具有常性
3、臨時對象的特性
????????臨時對象(temporary object)是編譯器在需要暫存表達式求值結果時自動創建的未命名對象,具有以下特點:
-
常性:臨時對象默認具有const屬性(常性)
int a = 5; const int& r1 = a * 2; // 正確:臨時對象具有常性,可以用 const 引用綁定 int& r2 = a * 2; // 錯誤:臨時對象是 const 的,不能綁定非 const 引用
-
生命周期:臨時對象通常會在表達式結束時銷毀,但如果綁定到?
const
?引用,其生命周期會延長至該引用的作用域結束。#include <iostream>int main() {// 臨時int直接使用 - 表達式結束就"消失"std::cout << "臨時int值: " << 42 << std::endl;// 綁定到const引用 - 生命周期延長const int& ref = 123; // 臨時123會一直存在直到main結束std::cout << "通過引用訪問: " << ref << std::endl;return 0; }
說明:第一個42是純臨時值,用完即"消失";第二個123因為綁定到const引用ref,所以會一直存在直到main函數結束
-
隱式創建:編譯器自動生成臨時對象的情況:(超重要!!!)
-
表達式求值結果
int x = 10, y = 20; const int& sum = x + y; // x + y 的結果存儲在臨時對象中
當表達式的結果需要存儲時,編譯器會生成臨時對象。
-
類型轉換中間結果
double d = 3.14; const int& intVal = d; // 生成臨時 int 對象存儲截斷后的值(3)
當隱式類型轉換發生時,編譯器會生成臨時對象存儲轉換后的值。
-
函數返回值(未使用移動語義時)
std::string createString() {return "Temporary"; // 返回臨時對象 }int main() {const std::string& s = createString(); // 臨時對象的生命周期延長std::cout << s << std::endl; // 正確:"Temporary"return 0; }
當函數返回一個臨時對象時,如果沒有優化(如 RVO/NRVO),編譯器會生成臨時對象。
-
4、const
?引用作為函數形參
????????當?const
?引用作為函數形參?時,如果傳入的實參類型不匹配(但可以隱式轉換),編譯器會自動創建臨時對象來存儲轉換后的值,并讓?const
?引用綁定到這個臨時對象。(學到后面再回看)
1. 基本示例(類型不匹配時創建臨時對象)
void print(const std::string& str) {std::cout << str << std::endl;
}int main() {print("Hello"); // "Hello" 是 const char[6],編譯器生成臨時 std::string 對象return 0;
}
發生了什么?
-
"Hello"
?的類型是?const char[6]
,而?print
?的參數是?const std::string&
。 -
編譯器隱式調用?
std::string
?的構造函數,生成一個臨時?std::string
?對象。 -
const std::string& str
?綁定到這個臨時對象,臨時對象的生命周期延長至?print
?函數結束。
2. 數值類型轉換(臨時對象存儲轉換后的值)
void printInt(const int& num) {std::cout << num << std::endl;
}int main() {double d = 3.14;printInt(d); // 生成臨時 int 對象存儲截斷后的值(3)return 0;
}
發生了什么?
-
d
?是?double
?類型,而?printInt
?的參數是?const int&
。 -
編譯器生成一個臨時?
int
?對象,存儲?d
?截斷后的值(3
)。 -
const int& num
?綁定到這個臨時?int
?對象。
3. 如果形參是?非 const 引用
,則不允許綁定臨時對象
void modify(int& num) { // 非 const 引用num = 100;
}int main() {modify(42); // 錯誤!臨時對象不能綁定到非 const 引用return 0;
}
為什么不行?
-
生成臨時對象,但是它不能綁定到非const引用中。
-
臨時對象是?
const
?的,不能通過?非 const 引用
?修改。 -
C++ 禁止這種行為,避免邏輯錯誤(修改一個即將銷毀的臨時對象沒有意義)。
4. 類對象的隱式轉換(臨時對象 +?const
?引用)
class MyString {
public:MyString(const char* s) { std::cout << "構造臨時 MyString\n"; }
};void printStr(const MyString& s) {std::cout << "使用 MyString\n";
}int main() {printStr("Hello"); // 生成臨時 MyString 對象return 0;
}
輸出:
說明:
-
"Hello"
?觸發?MyString
?的構造函數,生成臨時對象。 -
const MyString& s
?綁定到這個臨時對象,生命周期延長至?printStr
?結束。
總結
情況 | 是否生成臨時對象? | 是否合法? |
---|---|---|
const T& ?形參 +?可隱式轉換的實參 | ?? 生成 | ?? 合法 |
const T& ?形參 +?完全匹配的實參 | ? 不生成 | ?? 合法 |
T& ?形參(非?const ?引用) +?臨時對象 | ?? 生成 | ? 非法 |
關鍵點:
-
const
?引用可以延長臨時對象的生命周期,避免懸垂引用。 -
非?
const
?引用不能綁定臨時對象,因為臨時對象是只讀的。 -
這種機制使得?
const
?引用在接受字面量、表達式結果、類型轉換結果時更加靈活和安全。
這在 C++ 的函數參數傳遞、返回值優化(RVO)、隱式轉換等場景中非常重要!
四、引用的使用場景
1、引用作為函數參數
引用作為函數參數可以實現高效傳參,避免拷貝開銷,同時可以修改實參的值。
示例(交換函數)
????????還記得C語言中的交換函數,學習C語言的時候經常用交換函數來說明傳值和傳址的區別。現在我們學習了引用,可以不用指針作為形參了:
void Swap(int& a, int& b) {int tmp = a;a = b;b = tmp;
}
因為在這里a和b是傳入實參的引用,我們將a和b的值交換,就相當于將傳入的兩個實參交換了。?
引用傳參的工程價值
引用傳參在C++中主要有兩大優勢:
-
性能優化:避免大型對象拷貝帶來的性能損耗
-
語義明確:通過引用明確表達函數可能修改參數值的意圖
// 性能敏感場景:傳遞大型結構體
void ProcessLargeData(const BigData& data) { // const引用避免拷貝// 只讀操作...
}// 需要修改參數的場景
void TransformData(Matrix& matrix) { // 非const引用表明會修改參數matrix.invert();
}
2、引用作為函數返回值
引用可以作為函數返回值,但需要注意:
-
不能返回局部變量的引用(除非是static局部變量)(因為在函數內部定義的普通的局部變量會隨著函數調用的結束而被銷毀)
-
可以返回類成員變量、全局變量或動態分配內存的引用(不會隨著函數調用的結束而被銷毀的數據)
-
可以返回函數參數中的引用
引用返回值的使用需要特別注意生命周期管理:
// 安全示例:返回靜態變量或成員變量的引用
int& GetStaticValue() {static int value = 0; // 靜態變量生命周期與程序相同return value;
}// 危險示例:返回局部變量的引用
int& DangerousFunction() {int local = 42; // 局部變量將在函數返回后被銷毀return local; // 編譯器警告:返回局部變量的引用
}
返回引用最佳實踐
-
返回對象成員變量時
-
返回函數內靜態變量時
-
返回動態分配的對象時(需配合智能指針)
-
返回參數中傳入的引用時
注意:
????????如果函數返回時,出了函數作用域,返回對象還未還給系統,則可以使用引用返回;如果已經還給系統了,則必須使用傳值返回。?
五、引用與指針的區別(超重要!!!)
????????在C++中,指針和引用就像一對性格迥異的孿生兄弟。指針如同大哥,引用則像小弟,二者在實踐中相得益彰。雖然功能有所重疊,但各自擁有獨特的特點,彼此不可替代。
雖然引用在底層實現上通常是通過指針實現的,但在語法和使用上有顯著區別:
特性 | 引用 | 指針 |
---|---|---|
開辟內存空間 | 不開辟內存空間(引用只是一個變量的取別名) | 要開辟內存空間(指針存儲一個變量的地址) |
初始化要求 | 必須初始化 | 可以不初始化 |
可修改性 | 一旦綁定實體就不能更改 | 可以隨時改變指向 |
NULL值 | 不能為NULL | 可以為NULL |
sizeof結果 | 引用類型的大小 | 指針的大小始終是地址空間所占字節個數(32位平臺下占4個字節,64位下是8byte) |
自增操作 | 實體值增加1 | 指向下一個同類型對象 |
多級間接訪問 | 不支持多級引用 | 支持多級指針 |
訪問方式 | 自動解引用 | 需要顯式解引用 |
安全性 | 更高(引用很少出現空指針和野指針的問題) | 相對較低(指針很容易出現空指針和野指針的問題) |
在語法概念上,引用就是一個別名,沒有獨立的空間,其和引用實體共用同一塊空間。?
int main() {int a = 10;int& ra = a; // 底層通常實現為指針ra = 20; // 自動解引用int* pa = &a; // 顯式指針*pa = 20; // 顯式解引用return 0;
}
但是在底層實現上,引用實際是有空間的,底層實現示例(匯編層面):
從匯編角度來看,引用的底層實現也是類似指針存地址的方式來處理的。?
引用與指針的工程選擇準則
場景 | 推薦選擇 | 理由 |
---|---|---|
必須重新綁定 | 指針 | 引用一旦綁定不可更改 |
可能為nullptr | 指針 | 引用不能為null |
容器存儲 | 指針 | 引用不是對象,不能直接存儲 |
函數參數 | 常引用優先 | 更清晰的語義,更安全的const保證 |
操作符重載 | 引用 | 更自然的語法 |
多態操作 | 指針或引用 | 根據是否需要重新綁定決定 |
C++引用與其他語言引用的本質區別
// C++引用示例
int a = 10;
int& ref = a; // 永久綁定到a
int b = 20;
// ref = b; // 不是重新綁定,而是賦值操作
// Java"引用"示例
Integer a = new Integer(10);
Integer ref = a; // 可以重新指向其他對象
Integer b = new Integer(20);
ref = b; // 合法操作,ref現在指向b
關鍵差異點
-
綁定靈活性:C++引用是永久綁定,Java引用可重新指向
-
空值處理:C++引用不能為null,Java引用可以為null
-
內存管理:C++引用不涉及內存管理,Java引用與GC機制緊密相關
六、引用的優點
-
更清晰的語法:引用使代碼更易讀,避免了指針的復雜語法
-
更安全:引用必須初始化且不能為NULL,減少了空指針風險
-
更高效:避免了值傳遞的拷貝開銷
-
支持運算符重載:使自定義類型的運算符重載更自然
七、數據結構中的引用實踐
????????在部分采用C代碼實現的數據結構教材中,作者會使用C++的引用替代指針傳參來簡化程序,避免復雜的指針操作。但由于許多學生尚未掌握引用這一概念,反而增加了理解難度。
在傳統C風格數據結構改造中,同時引用也可以顯著提升代碼可讀性,例子如下:
1、C風格二級指針實現解析
在傳統C語言中,由于沒有引用概念,修改鏈表頭指針需要使用二級指針:
// 節點結構定義
typedef struct Node {int value;struct Node* next;
} Node;// 創建新節點
Node* createNode(int value) {Node* newNode = (Node*)malloc(sizeof(Node));newNode->value = value;newNode->next = nullptr;return newNode;
}// 在鏈表頭部插入節點
void insertNode(Node** head, int value) {Node* newNode = createNode(value);newNode->next = *head; // 新節點指向原頭節點*head = newNode; // 修改頭指針指向新節點
}
關鍵點解析
-
二級指針的必要性:為了修改調用方的頭指針,需要傳遞指針的地址
-
解引用操作:通過
*head
訪問實際的頭指針 -
操作順序:必須先設置新節點的next指針,再更新頭指針
調用方式
Node* head = nullptr; // 空鏈表
insertNode(&head, 10); // 必須傳遞頭指針的地址
insertNode(&head, 20);
2、C++引用風格實現解析
C++引用提供了更直觀的語法來修改指針:
struct Node {int value;Node* next;
};Node* createNode(int value) {Node* newNode = new Node;newNode->value = value;newNode->next = nullptr;return newNode;
}// 使用引用簡化鏈表操作
void insertNode(Node*& head, int value) {Node* newNode = createNode(value);newNode->next = head; // 直接使用head引用head = newNode; // 直接修改head引用
}
關鍵改進
-
語法簡化:
Node*&
表示對指針的引用,無需二級指針 -
直觀操作:直接操作
head
就像操作原始指針一樣 -
類型安全:引用必須初始化,避免了空指針風險
調用方式
Node* head = nullptr; // 空鏈表
insertNode(head, 10); // 直接傳遞指針,無需取地址
insertNode(head, 20);
3、兩種實現的底層對比
內存布局示例
調用方:[head指針] -> [節點A] -> [節點B] -> nullptr
C風格:調用棧保存head指針的地址,然后函數內通過解引用修改head指針
C++風格:調用棧保存head指針的引用(編譯器通常用指針實現),然后函數內直接操作引用
八、現代C++中的引用演進(后面會學到,現在先了解)
1、右值引用(C++11引入)
void process(std::string&& str) { // 移動語義支持// 可以安全"竊取"str的資源
}
2、完美轉發
template<typename T>
void relay(T&& arg) { // 通用引用process(std::forward<T>(arg)); // 保持值類別
}
3、結構化綁定(C++17)
std::map<int, std::string> m;
for (auto& [key, value] : m) { // 引用綁定到map元素// 直接修改map中的值
}
九、引用使用的注意事項
1、避免懸垂引用
-
不要返回局部變量的引用
-
不要返回臨時對象的引用
-
注意lambda捕獲引用時的生命周期
2、接口設計原則
-
輸入參數:const引用優先
-
輸出參數:非const引用明確修改意圖
-
返回值:值返回優先,必要時返回引用
3、多線程環境
-
共享數據的引用訪問需要同步
-
避免跨線程傳遞局部變量的引用
????????引用作為C++的核心特性,其正確使用需要開發者深入理解其語義和限制。在性能關鍵代碼中合理使用引用,可以顯著提升程序效率,同時保持代碼的清晰性和安全性。