【c++面向对象编程】第32篇:移动语义与右值引用:现代C++性能优化核心

目录

一、一个昂贵的拷贝

[二、左值 vs 右值:核心概念](#二、左值 vs 右值:核心概念)

[三、右值引用 T&&](#三、右值引用 T&&)

区分左值引用与右值引用

四、移动构造函数与移动赋值运算符

编写移动构造函数

使用示例

五、std::move:仅仅是转型

六、移动语义的收益示例

七、移动语义发生的典型场景

[返回值优化(RVO)vs 移动](#返回值优化(RVO)vs 移动)

八、完美转发初探

引用折叠规则

转发函数

[九、std::move 与 std::forward 的区别](#九、std::move 与 std::forward 的区别)

十、常见错误

[1. move 后继续使用对象](#1. move 后继续使用对象)

[2. 不必要的 move(阻止 RVO)](#2. 不必要的 move(阻止 RVO))

[3. const 对象不能移动](#3. const 对象不能移动)

[4. 忘记标记 noexcept](#4. 忘记标记 noexcept)

十一、这一篇的收获


一、一个昂贵的拷贝

cpp

复制代码
vector<string> createBigVector() {
    vector<string> v(1000000, "hello");
    return v;   // 返回时发生了什么?
}

vector<string> v = createBigVector();

C++98/03 时代,这个返回会触发一次或多次拷贝:构造临时对象、拷贝给接收变量,100 万个字符串被逐个复制,性能极差。

现代 C++ 的做法:移动,而不是拷贝。将内部指针直接"偷"过来,原对象置空------O(1) 操作,而不是 O(n)。


二、左值 vs 右值:核心概念

概念 特征 例子
左值 有名字、可取地址、持久存在 变量名 astd::cout*ptr
右值 临时值、无名、即将销毁 字面量 42、表达式 a+b、函数返回值

cpp

复制代码
int a = 42;    // a 是左值,42 是右值
int b = a;     // b 是左值,a 是左值(但可以读取)
int c = a + b; // a+b 是右值(临时结果)

简单判断 :能对表达式用 & 取地址的是左值,不能的是右值。

cpp

复制代码
int x = 5;
&x;      // ✅ 合法,x 是左值
&5;      // ❌ 非法,5 是右值

三、右值引用 T&&

右值引用专门绑定到右值,语法是 T&&

cpp

复制代码
int&& rref = 42;        // 右值引用绑定到临时值
// int&& rref2 = x;     // ❌ 不能绑定到左值(x 是左值)

区分左值引用与右值引用

cpp

复制代码
void process(int& lref) {
    cout << "左值引用版本" << endl;
}

void process(int&& rref) {
    cout << "右值引用版本" << endl;
}

int main() {
    int x = 10;
    process(x);    // 调用左值版本(x 是左值)
    process(10);   // 调用右值版本(10 是右值)
    process(move(x)); // 把 x 转为右值,调用右值版本
}

四、移动构造函数与移动赋值运算符

编写移动构造函数

cpp

复制代码
class Buffer {
private:
    int* data;
    size_t size;
public:
    // 普通构造
    Buffer(size_t n) : size(n), data(new int[n]) {}
    
    // 析构
    ~Buffer() { delete[] data; }
    
    // 拷贝构造(深拷贝)
    Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
        copy(other.data, other.data + size, data);
        cout << "拷贝构造" << endl;
    }
    
    // 移动构造(“偷”资源)
    Buffer(Buffer&& other) noexcept 
        : data(other.data), size(other.size) {
        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;
    }
    
    // 拷贝赋值...
};

关键点

  1. 参数是 T&&

  2. 将源对象的资源"偷"过来

  3. 将源对象置于"有效但未定义"状态(通常是 nullptr

  4. 标记 noexcept(移动操作不应抛异常,便于标准库优化)

  5. 移动后源对象仍然可以被析构(所以不能让它持有资源)

使用示例

cpp

复制代码
Buffer a(100);
Buffer b = move(a);   // 移动构造(a 不再拥有资源)
Buffer c;
c = move(b);          // 移动赋值

五、std::move:仅仅是转型

std::move 这个名字容易误导------它并不移动任何东西。它只是一个类型转换:将左值转换为右值引用。

cpp

复制代码
template<typename T>
decltype(auto) move(T&& arg) {
    return static_cast<remove_reference_t<T>&&>(arg);
}

cpp

复制代码
int x = 10;
int&& y = move(x);   // 将 x 转为右值引用(但 x 仍然是左值)
// move(x) 告诉编译器:请把我当作右值对待

重要move 只是"请求"移动,真正移动发生在移动构造/赋值中。如果类型没有移动构造,仍会调用拷贝。


六、移动语义的收益示例

cpp

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

class BigData {
    vector<int> data;
public:
    BigData(size_t n) : data(n, 0) {
        // cout << "构造" << endl;
    }
    
    // 拷贝构造(深拷贝)
    BigData(const BigData& other) : data(other.data) {
        cout << "拷贝构造(O(n))" << endl;
    }
    
    // 移动构造(O(1))
    BigData(BigData&& other) noexcept : data(move(other.data)) {
        cout << "移动构造(O(1))" << endl;
    }
};

int main() {
    cout << "=== 拷贝方式 ===" << endl;
    BigData d1(1000000);
    BigData d2 = d1;       // 拷贝:100万整数被复制
    
    cout << "\n=== 移动方式 ===" << endl;
    BigData d3(1000000);
    BigData d4 = move(d3); // 移动:只是交换指针,O(1)
    
    return 0;
}

输出:

text

复制代码
=== 拷贝方式 ===
拷贝构造(O(n))

=== 移动方式 ===
移动构造(O(1))

七、移动语义发生的典型场景

场景 说明 示例
函数返回局部变量 编译器自动移动 return vec;
std::move 显式转换 主动请求移动 move(obj)
标准库容器 push_back 的右值版本 vec.push_back(move(s))
算法 std::sort 等内部移动元素 swap 底层用移动

返回值优化(RVO)vs 移动

cpp

复制代码
vector<int> createVector() {
    vector<int> v(1000);
    return v;   // 编译器会使用 RVO(复制省略)或移动
}

现代编译器在返回局部变量时通常使用复制省略(Copy Elision),连移动都不需要,直接构造在目标位置。


八、完美转发初探

完美转发用于模板函数:将参数原封不动地转发给另一个函数,保持其左值/右值属性。

cpp

复制代码
template<typename T>
void wrapper(T&& arg) {
    // 想要把 arg 转发给 process,保持 arg 的原始类型
    process(forward<T>(arg));
}

引用折叠规则

类型 折叠结果
T& & T&
T& && T&
T&& & T&
T&& && T&&

转发函数

cpp

复制代码
void process(int& x) { cout << "左值" << endl; }
void process(int&& x) { cout << "左值" << endl; }

template<typename T>
void forwarder(T&& arg) {
    process(forward<T>(arg));   // 保持 arg 的左值/右值属性
}

int main() {
    int x = 10;
    forwarder(x);   // 转发左值 → 调用 process(int&)
    forwarder(20);  // 转发右值 → 调用 process(int&&)
}

九、std::move 与 std::forward 的区别

特性 std::move std::forward
作用 无条件转为右值引用 有条件地转为右值(仅当参数是右值时)
常用场景 明确要移动对象 完美转发模板参数
典型写法 move(obj) forward<T>(arg)

十、常见错误

1. move 后继续使用对象

cpp

复制代码
Buffer a(100);
Buffer b = move(a);
a.someMethod();   // ❌ 危险!a 处于未定义状态

2. 不必要的 move(阻止 RVO)

cpp

复制代码
vector<int> createVector() {
    vector<int> v(1000);
    return move(v);   // ❌ 阻止 RVO,反而可能变慢
}

3. const 对象不能移动

cpp

复制代码
const Buffer a(100);
Buffer b = move(a);   // ❌ 调用拷贝构造(const 不能绑定到 T&&)

4. 忘记标记 noexcept

移动操作不抛异常时应标记 noexcept,否则标准库(如 vector 扩容)可能选择拷贝而非移动。


十一、这一篇的收获

你现在应该理解:

  • 左值 :有地址,持久;右值:临时,即将销毁

  • 右值引用 T&&:绑定到右值,用于移动语义

  • 移动构造/赋值:转移资源所有权,O(1) 操作,源对象置空

  • std::move:只是转型(左值 → 右值引用),不移动任何东西

  • std::forward:完美转发,保持参数原始类型

  • 关键收益:避免深拷贝,尤其是容器、大对象

💡 小作业:实现一个 String 类(类似 std::string 的子集),包含普通构造、拷贝构造、移动构造、析构。测试 vector<String>push_back 在 C++11 前后的性能差异(模拟)。


下一篇预告:第33篇《C++异常处理:try/throw/catch的基本流程》------进入异常安全章节。异常是 C++ 的错误处理机制,但使用不当会导致资源泄漏。下篇讲清楚 try/throw/catch 的基本用法。

相关推荐
fish_xk5 小时前
c++11的初见
开发语言·c++·算法
JAVA面经实录9176 小时前
JVM高频面试总结(背诵完整版)
java·开发语言·jvm
ChoSeitaku6 小时前
11.异常_throws_try...catch_BigInteger_BigDecimal_Date_Calendar_LocalDate_Integer
java
胡志辉的博客6 小时前
完全开源、本地 SQLite 管理一切:我写了一个桌面邮件客户端 OneMail
java·sqlite·开源
沪漂阿龙6 小时前
Java JVM 面试题详解:JVM运行原理、内存模型、堆栈方法区、GC垃圾回收、JIT编译、类加载机制与线上调优全攻略
java·开发语言·jvm
小碗羊肉6 小时前
Maven高级
java·开发语言·maven
不知名的老吴6 小时前
C++ 中函数对象的形式概述
开发语言·c++
星秀日6 小时前
Spring Boot + Sa-Token 实时聊天系统:用户注册流程源码深度剖析
java·人工智能·spring·状态模式
Shan12056 小时前
C++中函数对象之重载 operator()
开发语言·c++·算法