右值引用和移動語義
本節解決什麼問題
考慮這樣的場景:
std::vector<int> create_large_vector()
{
std::vector<int> v(1000000);
// ... 填充数据 ...
return v; // C++98 中会拷贝整个 vector!(非常慢)
}
在 C++98 中,函數返回大的對象時會發生深拷貝,性能很差。C++11 引入了移動語義,把"拷貝別人的數據"變成"偷走別人的數據",大幅提高性能。
這個特性是什麼
- 左值:有名字、可以取地址的對象。如變量
x、arr[0]。 - 右值:臨時的、即將銷燬的對象。如
42、x + y、std::move(x)。 - 右值引用
T&&:綁定到右值的引用。 - 移動語義:通過"偷"右值的資源而不是拷貝,來避免不必要的深拷貝。
std::move:將左值轉爲右值引用,告訴編譯器"這個對象我以後不再用了,你可以偷它的資源"。
C++ 標準版本
C++11
需要的頭文件
#include <utility> // for std::move, std::forward
基本語法
int&& rref = 42; // 右值引用绑定到临时值
std::string&& sr = s1 + s2; // 绑定到表达式结果
// std::move:把左值转为右值
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // v1 的数据被"偷"到 v2,v1 变成空
左值 vs 右值速查表
| 表達式 | 左值/右值 | 說明 |
|---|---|---|
int x = 5; | x 是左值 | 有名字,可取地址 |
42 | 右值 | 字面量 |
x + y | 右值 | 臨時結果 |
f() 返回非引用 | 右值 | 臨時對象 |
f() 返回引用 | 左值 | 引用是別名 |
std::move(x) | 右值(將亡值) | 強制轉爲右值引用 |
示例代碼
示例 1:拷貝 vs 移動——爲什麼需要移動語義
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v1 = {1, 2, 3, 4, 5};
// 拷贝(两份独立的数据)
std::vector<int> v2 = v1;
std::cout << "after copy:\n";
std::cout << " v1 size = " << v1.size() << "\n"; // 5
std::cout << " v2 size = " << v2.size() << "\n"; // 5
// 移动(v1 的数据被"偷"到 v3,v1 变成空)
std::vector<int> v3 = std::move(v1);
std::cout << "after move:\n";
std::cout << " v1 size = " << v1.size() << "\n"; // 0
std::cout << " v3 size = " << v3.size() << "\n"; // 5
return 0;
}
運行結果:
after copy:
v1 size = 5
v2 size = 5
after move:
v1 size = 0
v3 size = 5
示例 2:在示例 1 基礎上,自定義類的移動構造函數
#include <algorithm> // for std::copy
#include <iostream>
#include <string>
#include <utility> // for std::move
class Buffer
{
int* data;
size_t size;
public:
// 构造函数
Buffer(size_t n) : data(new int[n]), size(n)
{
std::cout << "Constructor: allocated " << n << " ints\n";
}
// 拷贝构造函数(深拷贝)
Buffer(const Buffer& other) : data(new int[other.size]), size(other.size)
{
std::copy(other.data, other.data + size, data);
std::cout << "Copy constructor: deep copied " << size << " ints\n";
}
// 移动构造函数(偷数据)
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size)
{
other.data = nullptr; // 让原对象安全析构
other.size = 0;
std::cout << "Move constructor: stole " << size << " ints\n";
}
~Buffer()
{
delete[] data;
std::cout << "Destructor\n";
}
size_t get_size() const { return size; }
};
int main()
{
Buffer buf1(1000);
// 拷贝:会触发深拷贝
Buffer buf2 = buf1;
std::cout << "buf1 size after copy: " << buf1.get_size() << "\n";
// 移动:数据被偷走,没有深拷贝!
Buffer buf3 = std::move(buf2);
std::cout << "buf2 size after move: " << buf2.get_size() << "\n";
std::cout << "buf3 size after move: " << buf3.get_size() << "\n";
return 0;
}
運行結果:
Constructor: allocated 1000 ints
Copy constructor: deep copied 1000 ints
buf1 size after copy: 1000
Move constructor: stole 1000 ints
buf2 size after move: 0
buf3 size after move: 1000
Destructor
Destructor
Destructor
Buffer 示例詳解:std::copy、std::move 與對象初始化
示例 2 中最容易混淆的地方主要有三個:
std::copy到底複製了什麼;std::move到底做了什麼;- 爲什麼
Buffer b = a;和Buffer b(a);都會調用拷貝構造函數。
下面結合 Buffer 類逐一說明。
std::copy:真正複製一段元素
std::copy 定義在 <algorithm> 中,常見形式可以簡化寫成:
template<class InputIt, class OutputIt>
OutputIt std::copy(InputIt first, InputIt last, OutputIt destination);
三個參數的含義如下:
| 參數 | 含義 |
|---|---|
first | 源數據的起始位置 |
last | 源數據的結束位置,但不包含該位置 |
destination | 目標區域的起始位置 |
示例中的代碼是:
std::copy(other.data, other.data + size, data);
可以理解爲:
從
other.data開始,複製到other.data + size之前爲止,並把結果寫入data指向的內存。
假設 size == 3,它大致等價於:
data[0] = other.data[0];
data[1] = other.data[1];
data[2] = other.data[2];
也可以手寫成循環:
for (size_t i = 0; i < size; ++i)
{
data[i] = other.data[i];
}
std::copy 的返回值是目標區域複製結束後的下一個位置。這裏複製了 size 個元素,因此:
int* end = std::copy(other.data, other.data + size, data);
// 此时 end 等于 data + size
本例不需要繼續使用這個位置,所以直接忽略了返回值。
需要注意,std::copy 不會自動爲目標區域申請內存。調用它之前,data 必須已經指向足夠大的有效內存。本例在成員初始化列表中已經完成了申請:
data(new int[other.size])
std::move:本身並不移動數據
std::move 定義在 <utility> 中。它的作用不是複製數據,也不是搬運數據,而是把一個表達式轉換成可以綁定到右值引用的形式。
它的函數模板可以簡化理解爲:
template<class T>
std::remove_reference_t<T>&& std::move(T&& value) noexcept;
對於下面這句代碼:
std::move(buf2)
參數是 buf2,返回結果可以簡單理解爲一個 Buffer&&。它相當於告訴編譯器:
buf2原來的資源可以被轉移,不必繼續保留原來的內容。
但 std::move(buf2) 這一表達式本身不會修改 buf2。真正轉移資源的是隨後被調用的移動構造函數或移動賦值運算符。
例如:
Buffer buf3 = std::move(buf2);
std::move(buf2) 使右側表達式可以匹配:
Buffer(Buffer&& other) noexcept;
於是編譯器調用移動構造函數,由移動構造函數真正接管指針:
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size)
{
other.data = nullptr;
other.size = 0;
}
因此,可以把它們的職責記成:
std::copy:真正复制元素
std::move:把对象转换成“允许被移动”的表达式
移动构造函数:真正负责转移资源
單獨寫下面這句通常沒有實際效果:
std::move(buf2);
因爲返回的右值引用沒有被用於構造、賦值或函數傳參,移動構造函數也就不會被調用。
拷貝構造爲什麼是“深拷貝”
下面這句會調用拷貝構造函數:
Buffer buf2 = buf1;
對應的構造函數是:
Buffer(const Buffer& other)
: data(new int[other.size]), size(other.size)
{
std::copy(other.data, other.data + size, data);
}
它做了兩件事:
- 爲
buf2重新申請一塊獨立的數組內存; - 把
buf1數組中的每個元素複製到新內存中。
拷貝完成後,兩者保存的內容相同,但管理的是不同的內存:
buf1.data ──> [buf1 自己的 1000 个 int]
buf2.data ──> [buf2 自己的 1000 个 int]
所以銷燬或修改其中一個對象,不會直接影響另一個對象,這就是深拷貝。
移動構造爲什麼要清空原對象
移動構造函數沒有重新申請數組,也沒有複製 1000 個元素,而是直接接管指針:
data(other.data), size(other.size)
接管後必須執行:
other.data = nullptr;
other.size = 0;
否則新對象和原對象會同時保存同一個指針:
buf2.data ─┐
├──> [同一块数组内存]
buf3.data ─┘
兩個對象析構時都會執行:
delete[] data;
這樣就可能對同一塊內存釋放兩次,產生未定義行爲。
把原對象的指針設爲 nullptr 後,所有權關係變成:
buf2.data = nullptr
buf3.data ──> [原来由 buf2 管理的数组内存]
而下面的操作是安全的:
delete[] nullptr;
所以移動後的 buf2 仍然可以正常析構。在這個自定義實現中,我們還明確把它的 size 設成了 0。
爲什麼 Buffer b = a; 等同於 Buffer b(a);
下面兩種寫法都在創建一個新的對象 b:
Buffer b = a; // 拷贝初始化
Buffer b(a); // 直接初始化
雖然第一種寫法中出現了等號,但這裏的 = 不是賦值運算符,因爲 b 在這條語句執行前還不存在,它正在被創建。
兩種寫法都會尋找一個能夠使用 a 來構造 b 的構造函數。由於 a 的類型是 Buffer,所以都會匹配:
Buffer(const Buffer& other);
也就是拷貝構造函數。
可以把它們理解成:
Buffer b = a; // 创建 b,并用 a 初始化
Buffer b(a); // 创建 b,并用 a 初始化
對於本例,這兩種寫法的效果相同。
構造和賦值的判斷方法
判斷調用的是構造函數還是賦值運算符,關鍵不在於有沒有等號,而在於左側對象是否正在被創建。
Buffer a(1000);
Buffer b = a; // b 正在创建:调用拷贝构造函数
Buffer c(a); // c 正在创建:调用拷贝构造函数
Buffer d = std::move(a); // d 正在创建:调用移动构造函数
Buffer e(std::move(b)); // e 正在创建:调用移动构造函数
如果對象已經存在,再使用等號,纔是賦值:
Buffer x(100);
Buffer y(200);
x = y; // x 已存在:调用拷贝赋值运算符
x = std::move(y); // x 已存在:调用移动赋值运算符
可以用一句話記憶:
對象正在“出生”時調用構造函數;對象已經存在,只是更換內容時調用賦值運算符。
對應關係如下:
| 寫法 | 對象狀態 | 調用的函數 |
|---|---|---|
Buffer b = a; | b 正在創建 | 拷貝構造函數 |
Buffer b(a); | b 正在創建 | 拷貝構造函數 |
Buffer b = std::move(a); | b 正在創建 | 移動構造函數 |
Buffer b(std::move(a)); | b 正在創建 | 移動構造函數 |
b = a; | b 已經存在 | 拷貝賦值運算符 |
b = std::move(a); | b 已經存在 | 移動賦值運算符 |
拷貝初始化和直接初始化並非永遠相同
從語法分類上看:
Buffer b = a; // copy-initialization,拷贝初始化
Buffer b(a); // direct-initialization,直接初始化
在當前 Buffer 示例中,它們都會調用普通的拷貝構造函數,因此效果相同。但面對帶有 explicit 的轉換構造函數時,兩者會出現區別。
例如:
class Number
{
public:
explicit Number(int value)
{
}
};
直接初始化可以顯式調用這個構造函數:
Number n1(10); // 正确
拷貝初始化則不允許隱式使用 explicit 構造函數:
Number n2 = 10; // 错误
因此,更準確的說法是:
對於本例中的同類型對象拷貝,
Buffer b = a;和Buffer b(a);都調用拷貝構造函數;但“拷貝初始化”和“直接初始化”在所有場景下並不完全等價。
補充: 當前
Buffer類手動管理動態內存,但只實現了析構函數、拷貝構造函數和移動構造函數。若還要安全支持b = a和b = std::move(a),還應實現拷貝賦值運算符與移動賦值運算符,這通常稱爲 Rule of Five(五法則)。
示例 3:在示例 2 基礎上,移動語義讓函數返回大對象高效
#include <iostream>
#include <vector>
#include <string>
// 返回大 vector(C++11 起自动启用移动语义,不需要手动 std::move)
std::vector<int> make_data(int n)
{
std::vector<int> v(n);
for (int i = 0; i < n; ++i)
{
v[i] = i * 10;
}
return v; // ✅ 编译器自动移动(或 RVO 优化),不拷贝
}
int main()
{
auto data = make_data(5);
std::cout << "data: ";
for (int n : data)
{
std::cout << n << " ";
}
std::cout << "\n";
std::cout << "size = " << data.size() << "\n";
// ⚠️ 错误做法:不要对返回值用 std::move!
// auto data2 = std::move(make_data(5)); // ❌ 不要这样写!破坏 RVO 优化
// ✅ 正确做法:赋值给已有变量时用 = std::move(source)
std::vector<int> old = {100, 200};
std::vector<int> fresh = std::move(old); // 把 old 的内容移给 fresh
std::cout << "old size = " << old.size() << "\n"; // 0
std::cout << "fresh size = " << fresh.size() << "\n"; // 2
return 0;
}
運行結果:
data: 0 10 20 30 40
size = 5
old size = 0
fresh size = 2
運行結果
見上方每個示例的"運行結果"。
示例中的關鍵語法解釋
| 示例 | 講了什麼 | 新出現的語法 | 爲什麼這樣寫 | 注意事項 |
|---|---|---|---|---|
| 示例 1 | 拷貝 vs 移動對比 | std::move()、移動後 size=0 | move 後源對象被"掏空",變成有效但未指定狀態 | move 後不要再使用源對象(除非重新賦值) |
| 示例 2 | 自定義移動構造 | Buffer(Buffer&&)、noexcept、other.data=nullptr | 移動構造直接偷指針,不分配新內存 | 移動後必須把 other.data 置 null,否則重複 delete |
| 示例 3 | 返回值和賦值優化 | RVO、return v 不加 move | 編譯器會優化返回值爲移動/RVO,加了 move 反而阻止 RVO | 局部變量 return 時不要加 std::move |
爲什麼移動構造要寫 noexcept
std::vector 擴容時要把舊元素搬到新內存。如果元素類型的移動構造可能拋異常,vector 爲了保證異常安全,可能寧願走拷貝構造。也就是說:你明明寫了移動構造,但如果沒寫 noexcept,容器擴容時不一定用它。
class Buffer
{
public:
Buffer(Buffer&& other) noexcept
{
// 偷资源,不抛异常
}
};
這個差異在單獨移動一個對象時看不明顯;當 vector<Buffer> 大量 push_back 觸發擴容時,是否 noexcept 纔會影響容器選擇移動還是拷貝。
std::move 什麼時候該用,什麼時候不該用
| 場景 | 是否用 std::move | 原因 |
|---|---|---|
| 把已有對象的資源轉給另一個對象 | ✅ | 明確轉移所有權或內容 |
把 unique_ptr 傳給接收所有權的函數 | ✅ | unique_ptr 不能拷貝,只能移動 |
return 局部變量 | ❌ | 讓編譯器做 RVO / NRVO |
對 const 對象 | ❌ | const 對象不能被真正移動,通常變成拷貝 |
| move 後還想繼續讀原對象內容 | ❌ | moved-from 對象只保證可析構、可重新賦值 |
常見錯誤
錯誤 1:move 之後繼續使用源對象
std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a);
std::cout << a[0]; // ❌ a 已经被"掏空",行为未定义
正確做法:move 後不要再使用源對象,或者先檢查是否爲空。
錯誤 2:對 const 對象用 move
const std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a); // ❌ 实际执行的是拷贝!const 对象不能移动
正確做法:move 不要用於 const 對象。
錯誤 3:給返回值加 std::move
std::vector<int> func()
{
std::vector<int> v(1000);
return std::move(v); // ❌ 阻止了 RVO 优化!
}
正確做法:直接 return v;,編譯器會自動優化。
錯誤 4:移動構造函數沒有標記 noexcept
MyClass(MyClass&& other) { ... } // 缺少 noexcept
正確做法:MyClass(MyClass&& other) noexcept { ... } — 不標記 noexcept 會導致 vector 等容器在擴容時退化爲拷貝。
使用建議
- return 時不要加 std::move:讓編譯器自動優化(RVO/NRVO)。
- 賦值給已有對象時用
std::move(source):用target = std::move(source)避免拷貝。 - move 後不要再用源對象(除非重新賦值或重置)。
- 移動構造函數必須標記
noexcept:否則 STL 容器在擴容時不會調用它。 std::move不移動任何東西:它只是類型轉換,把左值變成右值引用。真正的移動發生在移動構造函數或移動賦值運算符中。
小結
- 左值有名字、可取地址;右值是臨時的(字面量、臨時對象)。
- 右值引用
T&&綁定到右值。 - 移動語義"偷"右值的資源,避免深拷貝。
std::move(x)把左值轉爲右值,告訴編譯器"x 可以被偷"。- return 局部變量時不要加 std::move(讓編譯器自己優化)。
- 移動構造函數必須標記
noexcept。
工程拓展
在 ROS2 中,發佈消息、傳輸大量傳感器數據(如點雲、圖像)時,移動語義能避免大數據拷貝。在 Boost.Asio 中,異步操作的回調參數大量使用移動語義來高效傳遞數據緩衝區。