C++11移动语义:右值引用与高效资源转移

引言

上一篇我们学习了左值和右值的基本概念。今天我们进入 C++11 最重要的特性之一:右值引用和移动语义

在没有移动语义之前,C++ 中临时对象的传递只能靠拷贝------把数据原封不动复制一份。对于 intchar 这种小类型还好,但对于 stringvector 这种管理堆内存的对象,拷贝意味着申请新内存 + 逐元素复制 + 释放旧内存,代价极高。

移动语义的核心思想是:与其深拷贝,不如直接把资源"偷"过来。就像你把一份纸质文件从 A 文件夹移到 B 文件夹------不需要复印一份,直接拿过去就行。

第一部分:移动构造与移动赋值

一、右值引用 T&&

cpp 复制代码
int a = 10;
int& lref = a;       // 左值引用:绑定左值
int&& rref = 10;     // 右值引用:绑定右值
int&& rref2 = a + 1; // 右值引用:绑定临时结果

右值引用 T&& 只能绑定到右值,它的出现是为了区分"这个参数是临时对象,可以偷它的资源"。

二、移动构造函数

参数是右值引用的构造函数,用来"偷"资源

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

class MyString {
private:
    char* data;
    size_t len;
public:
    // 普通构造
    MyString(const char* str) {
        len = strlen(str);
        data = new char[len + 1];
        strcpy(data, str);
        cout << "构造:" << data << endl;
    }

    // 拷贝构造(深拷贝)
    MyString(const MyString& other) {
        len = other.len;
        data = new char[len + 1];
        strcpy(data, other.data);
        cout << "拷贝构造:" << data << endl;
    }

    // 移动构造(偷资源)
    MyString(MyString&& other) noexcept {
        len = other.len;
        data = other.data;         // 直接接管指针!
        other.data = nullptr;      // 源对象置空
        other.len = 0;
        cout << "移动构造:" << data << endl;
    }

    ~MyString() {
        if (data) {
            cout << "析构:" << data << endl;
            delete[] data;
        }
    }

    const char* c_str() const { return data; }
};

移动构造做了什么?

三、移动赋值运算符

cpp 复制代码
// 移动赋值
MyString& operator=(MyString&& other) noexcept {
    if (this != &other) {
        delete[] data;             // 释放自己的旧资源
        data = other.data;         // 接管 other 的资源
        len = other.len;
        other.data = nullptr;      // other 置空
        other.len = 0;
    }
    cout << "移动赋值:" << data << endl;
    return *this;
}

四、触发移动的场景

cpp 复制代码
MyString createString() {
    return MyString("临时对象");  // 返回临时对象
}

int main() {
    MyString s1("hello");
    
    // 场景1:用临时对象构造 → 调用移动构造
    MyString s2 = createString();
    
    // 场景2:用 std::move 强制移动
    MyString s3 = std::move(s1);
    // s1 现在被掏空了!不要再使用
    
    // 场景3:临时对象赋值 → 调用移动赋值
    s2 = MyString("world");
    
    // 场景4:std::move 强制移动赋值
    s2 = std::move(s3);
}

第二部分:std::move

一、std::move 的本质

std::move 不移动任何东西 ,它只是做了一个类型转换------把左值转成右值引用。

cpp 复制代码
// std::move 的简化实现
template<typename T>
typename remove_reference<T>::type&& move(T&& t) noexcept {
    return static_cast<typename remove_reference<T>::type&&>(t);
}

// 本质上就是:
int x = 10;
int&& rref = static_cast<int&&>(x);  // 等价于 std::move(x)

std::move = 一个类型转换 + 一个语义标记。它告诉编译器:"请把 x 当作右值处理,可以偷它的资源"。

二、move 之后的对象

cpp 复制代码
string s1 = "hello";
string s2 = std::move(s1);  // s1 被移动

cout << s1 << endl;   // 可能输出空字符串,也可能是其他值
cout << s1.size();    // 未定义(取决于实现)

// ★ 唯一保证的是:s1 处于"有效但未指定"状态
// 可以安全析构
// 可以赋予新值:s1 = "new value";
// 但不应该读取它的值

三、什么时候用 std::move

cpp 复制代码
// 1. 要把一个左值传给接受右值引用的函数
vector<int> v1 = {1, 2, 3};
vector<int> v2 = std::move(v1);  // 明确说:v1 我不要了,资源给 v2

// 2. 函数返回时(return 局部变量不需要 move!)
vector<int> createVector() {
    vector<int> v(10000);
    return v;              // ✅ 编译器自动优化,不需要 std::move
    // return std::move(v); // ❌ 反而会阻止 RVO 优化!
}

// 3. 往容器中放入会销毁的对象
vector<string> vec;
string s = "hello";
vec.push_back(std::move(s));  // s 之后不再使用

第三部分:noexcept 与移动

一、为什么移动构造要加 noexcept

cpp 复制代码
// ✅ 推荐
MyString(MyString&& other) noexcept { ... }

// ❌ 不推荐
MyString(MyString&& other) { ... }

原因:vector 扩容时,如果移动构造是 noexcept,就用移动;否则用拷贝

cpp 复制代码
// vector 扩容时的内部逻辑
if (移动构造是 noexcept) {
    移动所有元素到新空间;    // 快,O(n) 时间,但安全
} else {
    拷贝所有元素到新空间;    // 慢,O(n) 时间 + O(n) 内存分配
}

为什么 vector 这么做? 因为如果移动构造可能抛异常,扩容中途出错时,旧数据已经被部分移动了,无法恢复。而拷贝不会破坏旧数据,是安全的。noexcept 保证移动构造不抛异常,vector 才能放心使用移动。

二、什么情况移动构造是 noexcept

cpp 复制代码
// 基本类型、指针 → 天然 noexcept
// string、vector → 移动构造是 noexcept
// 自定义类型 → 手动加 noexcept

class MyClass {
public:
    MyClass(MyClass&&) noexcept = default;  // 如果所有成员移动都是 noexcept
};

第四部分:编译器自动生成的规则

如果定义了 编译器是否自动生成移动构造
什么都没定义 ✅ 自动生成
自定义了拷贝构造 ❌ 不生成移动构造
自定义了移动构造 ❌ 不生成拷贝构造(标记为 delete)
自定义了析构函数 ❌ 不生成移动构造
自定义了移动赋值 类似规则

经验法则:如果你需要自定义析构函数(管理资源),大概率也需要自定义移动构造和移动赋值。

cpp 复制代码
class MyClass {
public:
    // 如果你写了这些...
    ~MyClass() { ... }                    // 自定义析构
    MyClass(const MyClass&) = default;    // 显式要求拷贝
    MyClass& operator=(const MyClass&) = default;

    // 那就也应该写移动
    MyClass(MyClass&&) noexcept = default;
    MyClass& operator=(MyClass&&) noexcept = default;
};

第五部分:完整示例

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Buffer {
private:
    int* data;
    size_t size;
public:
    // 普通构造
    Buffer(size_t n) : size(n), data(new int[n]) {
        cout << "构造:分配 " << n << " 个 int" << endl;
    }

    // 拷贝构造(深拷贝)
    Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
        copy(other.data, other.data + size, data);
        cout << "拷贝构造" << endl;
    }

    // 移动构造(偷资源)★ noexcept 很重要
    Buffer(Buffer&& other) noexcept : size(other.size), data(other.data) {
        other.data = nullptr;
        other.size = 0;
        cout << "移动构造" << endl;
    }

    // 移动赋值
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        cout << "移动赋值" << endl;
        return *this;
    }

    ~Buffer() { delete[] data; }
    
    size_t getSize() const { return size; }
};

int main() {
    // 触发移动构造
    Buffer b1 = Buffer(1000);       // 临时对象 → 移动构造
    Buffer b2 = std::move(b1);      // 强制移动
    
    // vector 扩容时会移动元素(因为移动构造是 noexcept)
    vector<Buffer> buffers;
    buffers.reserve(3);
    buffers.emplace_back(100);      // 原地构造
    buffers.emplace_back(200);      // 可能触发扩容 + 移动
    buffers.emplace_back(300);      // 可能触发扩容 + 移动
    
    return 0;
}

第六部分:移动语义 vs 拷贝语义

对比项 拷贝 移动
触发方式 T a = b;(b 是左值) T a = std::move(b);T a = T();
内存操作 申请新内存 + 复制数据 直接接管指针
时间复杂度 O(n) O(1)
源对象状态 不变 被掏空(有效但未指定)
适用场景 需要保留源对象 源对象不再使用

总结

一、核心要点

要点 内容
右值引用 T&& 只能绑定右值,用于区分"可以偷资源"的参数
移动构造 参数是 T&&,接管资源、源置空
移动赋值 释放自己的旧资源,接管新资源
std::move 把左值转成右值引用(只是一个类型转换)
noexcept 移动构造必须加,否则 vector 扩容不用移动

二、一句话记忆

移动语义通过右值引用 T&& 区分"可以偷"的临时对象,移动构造直接接管资源指针并将源置空,std::move 只是把左值转成右值引用。移动构造必须加 noexcept,否则 vector 扩容时宁愿拷贝也不用移动。

相关推荐
我不是懒洋洋2 小时前
从零实现WebSocket:实时通信的核心协议
c++
Hello:CodeWorld2 小时前
深入浅出 C++:静态多态与动态多态的业务应用场景与源码级实战
开发语言·c++·架构
星恒随风2 小时前
C++入门(一):第一个 C++ 程序、命名空间、输入输出和缺省参数
开发语言·c++·笔记·学习
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第94题】【Mysql篇】第24题:什么是单路排序?什么是双路排序??
java·开发语言·数据库·mysql·面试·排序算法
于先生吖2 小时前
Java分账体系设计,网约车行程计费与到店线下结账一体化后端开发实战
java·开发语言
thisiszdy2 小时前
<C++&C#> lambda表达式
java·c++·c#
晚风叙码2 小时前
C++类和对象(中)| 深挖四大默认成员函数:构造/析构/拷贝/赋值重载原理全解
c++
Cloud_Shy6182 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第三章 Item 17 - 20)
开发语言·笔记·python
三品吉他手会点灯2 小时前
C语言学习笔记 - 42.数据类型 - scanf函数深度解析
c语言·开发语言·笔记·学习