第 18.10 節

右值引用和移動語義

0瀏覽次數0訪問次數--跳出率--平均停留

本節解決什麼問題

考慮這樣的場景:

std::vector<int> create_large_vector()
{
    std::vector<int> v(1000000);
    // ... 填充数据 ...
    return v;  // C++98 中会拷贝整个 vector!(非常慢)
}

在 C++98 中,函數返回大的對象時會發生深拷貝,性能很差。C++11 引入了移動語義,把"拷貝別人的數據"變成"偷走別人的數據",大幅提高性能。

這個特性是什麼

  • 左值:有名字、可以取地址的對象。如變量 xarr[0]
  • 右值:臨時的、即將銷燬的對象。如 42x + ystd::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

示例 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=0move 後源對象被"掏空",變成有效但未指定狀態move 後不要再使用源對象(除非重新賦值)
示例 2自定義移動構造Buffer(Buffer&&)noexceptother.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 等容器在擴容時退化為拷貝。

使用建議

  1. return 時不要加 std::move:讓編譯器自動優化(RVO/NRVO)。
  2. 賦值給已有對象時用 std::move(source):用 target = std::move(source) 避免拷貝。
  3. move 後不要再用源對象(除非重新賦值或重置)。
  4. 移動構造函數必須標記 noexcept:否則 STL 容器在擴容時不會調用它。
  5. std::move 不移動任何東西:它只是類型轉換,把左值變成右值引用。真正的移動發生在移動構造函數或移動賦值運算符中。

小結

  • 左值有名字、可取地址;右值是臨時的(字面量、臨時對象)。
  • 右值引用 T&& 綁定到右值。
  • 移動語義"偷"右值的資源,避免深拷貝。
  • std::move(x) 把左值轉為右值,告訴編譯器"x 可以被偷"。
  • return 局部變量時不要加 std::move(讓編譯器自己優化)。
  • 移動構造函數必須標記 noexcept

工程拓展

在 ROS2 中,發佈消息、傳輸大量傳感器數據(如點雲、圖像)時,移動語義能避免大數據拷貝。在 Boost.Asio 中,異步操作的回調參數大量使用移動語義來高效傳遞數據緩衝區。

音乐页