一、基本概念
-
左值(lvalue)和右值(rvalue)
-
左值指的是有確定存儲位置(地址)的對象,通常可以出現在賦值語句左側。例如:變量名、解引用指針得到的對象、數組元素等都屬于左值。
-
右值一般指臨時對象或字面常量,通常沒有固定的存儲地址,只能出現在賦值語句右側。例如:字面量(
42
、"hello"
)、表達式求值產生的臨時結果(a + b
、函數返回的非引用類型)等都屬于右值。
-
-
左值引用(lvalue reference)
T&
-
語法:
T&
-
含義:引用一個左值,必須綁定到一個具有名字且可尋址的對象上。
-
作用:可以通過引用直接操作原對象,不產生拷貝;常用于函數參數(接收可修改的實參)或延長臨時對象的生命周期(使用
const T&
)。
-
-
右值引用(rvalue reference)
T&&
(C++11 引入)-
語法:
T&&
-
含義:引用一個右值,只能綁定到臨時對象(比如函數返回的非引用類型對象、字面量或者
std::move
之后的結果)。 -
作用:為“移動語義(move semantics)”和“完美轉發(perfect forwarding)”提供基礎,通過接收將要被銷毀的臨時對象,可以“竊取”其內部資源而不是拷貝。
-
二、左值引用與右值引用的區別
特性 | 左值引用 T& | 右值引用 T&& |
---|---|---|
可綁定的對象 | 只能綁定到 左值(命名變量等) | 只能綁定到 右值(臨時對象、字面常量、std::move 產生的中間值) |
是否可修改 | 可修改所引用的對象 | 可以修改所引用的臨時對象(臨時對象本來就要銷毀) |
延長生命周期 | const T& 可延長臨時對象生命周期T& 不能綁定臨時對象 | 綁定臨時對象后,可操作臨時,直到其生命周期結束 |
主要用途 | 傳遞可修改的已有對象,避免拷貝 | 實現移動語義、完美轉發,減少不必要的深度拷貝 |
-
左值引用
T&
-
只能引用已有的命名對象。
-
用途:
-
函數參數接收時,可以直接修改傳入的實參(例如
void foo(int& x)
)。 -
避免拷貝開銷(例如
void print(const std::string& s)
)。 -
const T&
可以綁定到右值,用于只讀訪問且延長臨時對象生命周期。
-
-
-
右值引用
T&&
-
只能引用臨時對象(右值)。
-
用途:
-
移動構造/移動賦值:從臨時對象“竊取”內部資源,而不做深拷貝。
-
完美轉發:在模板中,通過
T&&
(與std::forward<T>(…)
)保持函數參數的值類別(左值/右值)不變。 -
禁止綁定左值:直接傳遞命名對象到
T&&
參數會編譯錯誤,除非顯式使用std::move
。
-
-
三、典型示例
下面通過幾個示例來直觀說明它們的使用場景和區別。
1. 直接綁定示例
#include <iostream>
#include <string>int main() {int a = 10; // a 是左值int& lref = a; // 左值引用只能綁定到左值// int& lref2 = 20; // 錯誤:不能把 int& 綁定到字面量 20(右值)上int&& rref = 20; // 右值引用只能綁定到右值// int&& rref2 = a; // 錯誤:不能把 int&& 綁定到左值 a 上std::string s = "Hello";std::string& ls = s; // 合法,引用已有 std::string 對象const std::string& lsc = "World"; // const std::string& 可以綁定到右值 "World",臨時字符串的生命周期延長至 lsc 作用域結束std::string&& rs = std::string("Temp"); // 右值引用綁定到了臨時 std::string("Temp"),可在后續對 rs 進行修改std::cout << "a: " << a << "\n"; // a: 10std::cout << "lref: " << lref << "\n"; // lref: 10std::cout << "rref: " << rref << "\n"; // rref: 20std::cout << "ls: " << ls << "\n"; // ls: Hellostd::cout << "lsc: " << lsc << "\n"; // lsc: Worldstd::cout << "rs: " << rs << "\n"; // rs: Tempreturn 0;
}
-
int& lref = a;
:左值引用lref
綁定到左值a
。 -
int&& rref = 20;
:右值引用rref
綁定到字面量20
(右值)。 -
const std::string& lsc = "World";
:const T&
可以綁定到右值(臨時字符串),并將其生命周期延長至lsc
的作用域結束。 -
std::string&& rs = std::string("Temp");
:右值引用rs
綁定到臨時字符串對象,此時可以對該臨時對象進行修改(不過它最終會析構)。
2. 作為函數參數的區別
通常我們會看到如下重載示例,用以區分傳入的是左值還是右值:
#include <iostream>// 重載 1:接受左值引用
void process(int& x) {std::cout << "process(int&): 接收左值引用, x = " << x << "\n";
}// 重載 2:接受右值引用
void process(int&& x) {std::cout << "process(int&&): 接收右值引用, x = " << x << "\n";
}int main() {int a = 5;process(a); // 調用 process(int&), 因為 a 是左值process(10); // 調用 process(int&&), 因為 10 是右值process(std::move(a)); // std::move(a) 將 a 轉換成右值, 因此調用 process(int&&)return 0;
}
-
當實參是一個命名變量(
a
),它是左值,因此會匹配void process(int&)
。 -
當實參是一個字面量或臨時表達式(如
10
或std::move(a)
),它們是右值,因此會匹配void process(int&&)
。
3. 移動構造 vs 拷貝構造
以一個簡單的自定義類 MyString
為例,演示移動構造函數與拷貝構造函數的區別。
#include <iostream>
#include <cstring>class MyString {
public:char* data;// 構造函數:從 C 風格字符串構造MyString(const char* s) {size_t len = std::strlen(s);data = new char[len + 1];std::strcpy(data, s);std::cout << "構造 MyString(\"" << data << "\")\n";}// 拷貝構造:深拷貝MyString(const MyString& other) {size_t len = std::strlen(other.data);data = new char[len + 1];std::strcpy(data, other.data);std::cout << "調用 拷貝構造, 源 = \"" << other.data << "\"\n";}// 移動構造:竊取指針,避免拷貝MyString(MyString&& other) noexcept {data = other.data; // 直接竊取資源指針other.data = nullptr; // 將源置空,避免析構時釋放兩次std::cout << "調用 移動構造\n";}// 析構函數~MyString() {if (data) {std::cout << "析構 MyString(\"" << data << "\")\n";delete[] data;} else {std::cout << "析構 MyString(nullptr)\n";}}
};MyString makeString() {MyString temp("臨時");return temp; // C++11 以后可進行移動構造而非拷貝
}int main() {MyString s1("Hello"); // 普通構造MyString s2 = s1; // 拷貝構造MyString s3 = makeString(); // 由于 makeString 返回臨時對象,可觸發移動構造(或編譯器優化)// 可以強制使用移動構造:MyString s4 = std::move(s1);return 0;
}
-
調用
MyString s2 = s1;
時,實參s1
是一個左值,因此調用的是 拷貝構造,會分配新內存并拷貝字符串內容。 -
調用
MyString s3 = makeString();
時,makeString()
返回的臨時對象是一個右值,因此會優先調用 移動構造(如果編譯器沒有做完全優化)。移動構造只會“竊取”臨時對象的內部char*
指針,而不會再做深拷貝。 -
對
s1
應用std::move(s1)
,即把左值s1
強制轉換為右值,再傳遞給MyString s4 = std::move(s1);
,也會調用移動構造,將s1
的內部指針轉移給s4
,此時s1.data
置為nullptr
。
4. 完美轉發(Perfect Forwarding)示例
在模板中,如果想讓函數“如實”地將傳入參數的值類別(左值/右值)傳給另一個函數,可以使用右值引用和 std::forward
。示例:
#include <iostream>
#include <utility> // std::forward, std::movevoid target(int& x) {std::cout << "target(int&) 被調用,x = " << x << "\n";
}void target(int&& x) {std::cout << "target(int&&) 被調用,x = " << x << "\n";
}// 通用轉發函數模板
template <typename T>
void wrapper(T&& param) {// 直接傳給 target,但保留 param 本身的值類別// 如果原參數是左值,就調用 target(int&); 如果是右值,就調用 target(int&&)target(std::forward<T>(param));
}int main() {int a = 100;wrapper(a); // a 是左值 -> 調用 target(int&)wrapper(200); // 200 是右值 -> 調用 target(int&&)int&& rr = 300;wrapper(rr); // rr 本身雖然聲明為右值引用,但 rr 名稱是左值 -> 調用 target(int&)wrapper(std::move(rr)); // std::move(rr) 是右值 -> 調用 target(int&&)return 0;
}
-
wrapper(a)
:a
是左值,模板參數T
被推斷為int&
,T&&
變為int& &&
,折疊后仍為int&
,所以param
是左值引用,std::forward<T>(param)
仍是左值,調用target(int&)
。 -
wrapper(200)
:200
是右值,T
被推斷為int
,T&&
為int&&
,param
是右值引用。std::forward<int>(param)
是右值,調用target(int&&)
。 -
int&& rr = 300;
聲明了一個右值引用rr
,但rr
本身是一個命名變量(左值)。所以:-
wrapper(rr)
:傳入rr
(是左值),和wrapper(a)
類似,依然調用target(int&)
。 -
wrapper(std::move(rr))
:std::move(rr)
將rr
強制轉換為右值,調用target(int&&)
。
-
四、總結
-
綁定限制
-
T&
只能綁定到左值,不能綁定到右值(除非加上const
,即const T&
可以綁定到右值,用于只讀)。 -
T&&
只能綁定到右值,不能直接綁定到左值(除非對左值使用std::move
或std::forward
強制轉換為右值)。
-
-
主要用途
-
左值引用
T&
:主要用于接收已有對象并進行讀取/修改,避免拷貝開銷。 -
右值引用
T&&
:主要用于實現“移動語義”,在可以銷毀的臨時對象上直接竊取資源;同時在模板中用于“完美轉發”以保留參數的值類別。
-
-
性能意義
-
使用
T&&
實現移動構造/移動賦值,可以顯著減少對大對象(如容器、字符串等)的深拷貝,從而提升性能。 -
std::forward<T>
與T&&
配合可以讓模板函數在轉發參數時不丟失值類別,避免不必要的拷貝或移動。
-
-
注意事項
-
雖然
T&&
只能綁定右值,但當T
已推斷為引用類型(如int&
)時,T&&
會出現“引用折疊”(reference collapsing)現象:-
如果
T
是U&
,那么T&&
等價于U&
。 -
如果
T
是U
,那么T&&
就是U&&
。
-
-
在函數重載中,若同時存在
void f(int&)
和void f(int&&)
,傳入的實參的值類別會直接影響調用哪個重載。
-