左值和右值
左值(lvalue):在表達式結束后仍然存在,可以取地址。簡單理解:有名字、有存儲位置。
比如變量、數組元素、對象等。
右值(rvalue):臨時值,表達式結束后就消失,不能取地址。
比如字面量、表達式的臨時結果。
int x = 10; // x 是左值
int y = x; // x 是左值,10 是右值
int z = x + y; // (x + y) 是右值,z 是左值
左值引用
左值引用就是對 左值的引用。
語法:T& ref = var;
int a = 5;
int& ref = a; // ref 是 a 的別名
ref = 10; // 改變 ref 其實就是改變 a
?? 注意:左值引用不能直接綁定到右值上。
int& r = 5; // ? 錯誤,5 是右值
右值引用
右值引用是 C++11 引入的,允許綁定到 右值。
語法:T&& ref = expr;
int&& r = 5; // ? r 引用了一個臨時右值
int&& r2 = a + 3; // ? a + 3 是右值
右值引用的意義:
可以延長臨時值的生命周期。
用于 移動語義(move semantics) 和 完美轉發(perfect forwarding)。
左值引用 vs 右值引用的函數參數示例
void f(int& x) { // 接受左值std::cout << "左值引用\n";
}
void f(int&& x) { // 接受右值std::cout << "右值引用\n";
}int main() {int a = 10;f(a); // 傳左值,調用 f(int&)f(20); // 傳右值,調用 f(int&&)
}
輸出:左值引用
右值引用
應用場景:移動語義
右值引用最重要的用途是避免不必要的拷貝,提高效率。
#include <iostream>
#include <vector>
using namespace std;int main() {vector<int> v1 = {1,2,3,4,5};vector<int> v2 = std::move(v1); // 使用右值引用轉移資源cout << "v1 size: " << v1.size() << endl;cout << "v2 size: " << v2.size() << endl;
}
結果:v1 size: 0
v2 size: 5
這里 std::move 把 v1 轉換成右值引用,避免了數據拷貝,而是直接轉移所有權。
移動構造時發生了什么?
以 std::vector 為例,里面有三個關鍵成員:
指向堆區的指針 ptr
當前元素個數 size
容量 capacity
拷貝構造
會重新分配一塊堆內存,把 v1 的數據拷貝過去。
移動構造
直接把 v1 的內部指針 ptr 交給 v2。
然后把 v1 的 ptr 置空,size 和 capacity 設為 0。
所以:
v1 變成一個 空的 vector(但依然是合法對象)。
v2 擁有了原來 v1 的那塊內存。
v2 用完之后,內存會銷毀嗎?
是的。
當 v2 生命周期結束時,它會調用析構函數,釋放那塊堆內存。
而 v1 在 std::move 之后,它的 ptr 已經被清空,所以它的析構函數不會釋放任何東西(避免二次釋放)。
直觀理解(類比搬家 🚚)
v1 原來有一套家具(堆內存)。
std::move(v1) 把家具的所有權交給了 v2。
v1 自己變成了一個空房子(里面啥都沒有,但房子還在)。
當 v2 銷毀時,它負責把家具丟掉。
示例
- 問題:沒有移動語義時的性能問題
假設我們有一個BigData類,它內部持有一個很大的動態數組(這里用std::vector模擬)。拷貝這個類的對象會非常昂貴,因為它需要分配新的內存并復制所有數據。
#include <iostream>
#include <vector>
#include <chrono>class BigData {
private:std::vector<int> m_data; // 模擬一個很大的數據塊public:// 構造函數,分配大量數據BigData(size_t size) : m_data(size) {std::cout << "Constructor called. Allocated " << size << " elements." << std::endl;}// 拷貝構造函數(深拷貝)- 性能瓶頸!BigData(const BigData& other) : m_data(other.m_data) {std::cout << "Copy Constructor called. Expensive deep copy!" << std::endl;}// 拷貝賦值運算符(深拷貝)BigData& operator=(const BigData& other) {if (this != &other) {m_data = other.m_data; // 又一次昂貴的拷貝!}std::cout << "Copy Assignment called. Expensive deep copy!" << std::endl;return *this;}// 析構函數~BigData() {std::cout << "Destructor called." << std::endl;}
};// 一個函數,返回一個BigData對象
BigData createBigData() {BigData data(1000000); // 在函數內部創建一個大對象return data; // 傳統C++03中,這里可能會觸發拷貝
}int main() {BigData my_data = createBigData(); // 這里期望得到函數內部創建的對象return 0;
}
- 解決方案:實現移動語義
移動語義允許我們“竊取”即將被銷毀的對象的資源,而不是進行昂貴的拷貝。我們通過定義移動構造函數和移動賦值運算符來實現這一點。
#include <iostream>
#include <vector>
#include <utility> // for std::moveclass BigData {
private:std::vector<int> m_data;public:BigData(size_t size) : m_data(size) {std::cout << "Constructor called. Allocated " << size << " elements." << std::endl;}// 1. 移動構造函數 (參數是非常量右值引用 BigData&&)BigData(BigData&& other) noexcept // noexcept 很重要,用于標準庫優化: m_data(std::move(other.m_data)) // 關鍵:使用std::move移動內部的vector{std::cout << "Move Constructor called. Efficient move!" << std::endl;// 移動后,源對象‘other’的m_data現在處于有效但未狀態(通常是空)}// 2. 移動賦值運算符BigData& operator=(BigData&& other) noexcept {if (this != &other) {m_data = std::move(other.m_data); // 關鍵:移動賦值}std::cout << "Move Assignment called. Efficient move!" << std::endl;return *this;}// 保留拷貝構造和拷貝賦值,實現“Rule of Five”BigData(const BigData& other) = default;BigData& operator=(const BigData& other) = default;~BigData() = default;
};// 工廠函數
BigData createBigData() {BigData data(1000000);return data; // 編譯器意識到‘data’是局部對象,是“將亡值”// 優先選擇移動構造函數,即使沒有移動構造也會嘗試拷貝
}int main() {std::cout << "--- Scenario 1: Return from function ---" << std::endl;BigData my_data = createBigData(); // 調用移動構造函數(如果優化不掉)std::cout << "\n--- Scenario 2: Explicit std::move ---" << std::endl;BigData data1(1000);BigData data2 = std::move(data1); // 使用std::move將左值強制轉換為右值,// 從而調用移動構造函數。// data1此后不應再被使用!return 0;
}
代碼關鍵點解釋:
移動構造函數 BigData(BigData&& other):
參數類型是BigData&&,這是一個右值引用,它只能綁定到臨時對象或即將被銷毀的對象(“將亡值”)。
它的作用是“竊取”源對象other的資源。在這里,我們使用std::move(other.m_data)來移動其內部的vector。std::move的本質是一個static_cast,它將變量強制轉換為右值,從而觸發vector自身的移動構造函數。
noexcept關鍵字向標準庫承諾這個操作不會拋出異常,這很重要,因為標準庫容器(如std::vector)在重新分配內存時會優先使用noexcept的移動操作而不是拷貝操作來轉移元素,性能更高。
移動賦值運算符 operator=(BigData&& other):
原理同移動構造函數,用于在賦值時移動資源。
std::move:
它本身不移動任何東西!它只是一個轉換工具,告訴編譯器:“我知道這個左值對象我不再需要了,請把它當作一個右值來處理”。
真正的移動操作是在類的移動構造函數或移動賦值運算符中完成的。
Rule of Five:
如果你定義了析構函數、拷貝構造函數、拷貝賦值運算符中的任何一個,那么你應該定義全部五個(加上移動構造函數和移動賦值運算符)來精確管理資源。在上面的例子中,我們顯式定義了移動操作,并用= default保留了默認的拷貝操作。
現代編譯器非常智能,通常會直接進行“返回值優化”,連移動構造都省略了。但在更復雜的返回路徑中,移動語義是重要的保障。
實際應用:編譯器的“神來之筆”與程序員的“安全網”
想象一下你要從A城市(函數內)運送一批貴重家具(大數據)到B城市(函數外)。
沒有優化(C++03時代):你得先在A城找個倉庫(函數棧幀)放家具,然后雇輛卡車,把家具一件件搬上卡車(拷貝),運到B城后,再一件件卸下來放到新家。費時費力!
有移動語義(C++11基礎):你發現這些家具到了B城后,A城的倉庫就要拆了。所以你很聰明,只把倉庫的所有權轉讓給B城的人。你只需要把倉庫地址告訴他(移動,即交換指針),他自己去取。省去了搬運的體力活!
返回值優化RVO(返回值優化)/NRVO(編譯器優化):編譯器這個“上帝”看到了你的整個計劃。它直接說:“別在A城建倉庫了,我直接在B城給你建好,你一開始就把家具放那里!”它完全消除了“移動”或“拷貝”這個動作本身。這是最極致的效率。
- 編譯器的“神來之筆”:返回值優化
返回值優化是編譯器被標準允許的一種優化,它可以直接在函數外部(調用者的棧幀上)構造本應在函數內部返回的對象,從而完全避免任何拷貝或移動操作。
代碼示例:
BigData createBigData() {BigData data(1000000); // 理論上,這里在函數內部構造`data`// ... 一些對data的操作return data; // 理論上,這里需要將`data`返回出去
}int main() {BigData my_data = createBigData(); // 理論上,這里需要接收返回的`data`return 0;
}
未優化時的邏輯路徑:
在createBigData函數內部調用構造函數BigData(1000000),創建data。
函數返回時,調用拷貝/移動構造函數,用data構造一個臨時對象。
在main函數中,調用拷貝/移動構造函數,用臨時對象構造my_data。
銷毀臨時對象。
函數結束,銷毀data。
啟用RVO/NRVO后的實際路徑:
編譯器會偷偷重寫你的代碼,變成類似這樣:
void createBigData(BigData& hidden_obj) { // 編譯器偷偷傳進來一個引用hidden_obj.BigData(1000000); // 直接在目標位置構造!// ... 一些對hidden_obj的操作return; // 直接返回,沒有任何拷貝!
}int main() {BigData my_data; // 只分配空間,未初始化createBigData(my_data); // 編譯器偷偷把my_data的引用傳進去構造return 0;
}
你看,my_data其實就是函數里的data,它們根本就是同一個對象! 這就是所謂的“連移動構造都省略了”,因為移動都不需要了。
“通常”這個詞的含義:在現代編譯器中,對于這種簡單的返回局部對象的場景,RVO優化非常強大且幾乎總是會發生(尤其是在Release模式下)。所以你可能在實際運行中看不到移動構造函數被調用。
- 程序員的“安全網”:移動語義的保障
那么問題來了,既然編譯器這么聰明,我們為什么還要費心寫移動語義呢?
因為編譯器不是萬能的上帝,它只能在簡單的、確定的代碼路徑中進行這種優化。一旦代碼變得復雜,優化就可能失敗。
BigData createBigData(bool flag) {BigData data1(1000000);BigData data2(1000000);if (flag) {return data1; // 可能返回這個分支} else {return data2; // 也可能返回那個分支}// 編譯器懵了:我該在調用者那里預先構造data1還是data2?
}int main() {BigData my_data = createBigData(true); // RVO優化可能失敗!return 0;
}
特性 | RVO(返回值優化) | 移動語義 |
---|---|---|
實施者 | 編譯器 | 程序員 (通過編寫移動構造函數) |
發生階段 | 編譯時 | 運行時 |
實現原理 | 重新解釋代碼邏輯,直接在目標內存地址構造對象。 | 資源所有權的轉移(例如,交換指針)。 |
所需條件 | 代碼路徑簡單,符合標準要求。 | 對象必須實現了移動構造函數/賦值運算符。 |
本質 | 消除“拷貝/移動”這個操作本身。 | 將“昂貴的拷貝”操作替換為“廉價的移動”操作。 |
代碼表現 | 看不見任何調用(構造函數、移動構造都沒有)。 | 能看到移動構造函數被調用。 |