左值和右值
左值(lvalue):在表达式结束后仍然存在,可以取地址。简单理解:有名字、有存储位置。
比如变量、数组元素、对象等。
右值(rvalue):临时值,表达式结束后就消失,不能取地址。
比如字面量、表达式的临时结果。
int x = 10; // x 是左值
int y = x; // x 是左值,10 是右值
int z = x + y; // (x + y) 是右值,z 是左值
左值引用
左值引用就是对 左值的引用。
语法:T& ref = var;
int a = 5;
int& ref = a; // ref 是 a 的别名
ref = 10; // 改变 ref 其实就是改变 a
⚠️ 注意:左值引用不能直接绑定到右值上。
int& r = 5; // ❌ 错误,5 是右值
右值引用
右值引用是 C++11 引入的,允许绑定到 右值。
语法:T&& ref = expr;
int&& r = 5; // ✅ r 引用了一个临时右值
int&& r2 = a + 3; // ✅ a + 3 是右值
右值引用的意义:
可以延长临时值的生命周期。
用于 移动语义(move semantics) 和 完美转发(perfect forwarding)。
左值引用 vs 右值引用的函数参数示例
void f(int& x) { // 接受左值std::cout << "左值引用\n";
}
void f(int&& x) { // 接受右值std::cout << "右值引用\n";
}int main() {int a = 10;f(a); // 传左值,调用 f(int&)f(20); // 传右值,调用 f(int&&)
}
输出:左值引用
右值引用
应用场景:移动语义
右值引用最重要的用途是避免不必要的拷贝,提高效率。
#include <iostream>
#include <vector>
using namespace std;int main() {vector<int> v1 = {1,2,3,4,5};vector<int> v2 = std::move(v1); // 使用右值引用转移资源cout << "v1 size: " << v1.size() << endl;cout << "v2 size: " << v2.size() << endl;
}
结果:v1 size: 0
v2 size: 5
这里 std::move 把 v1 转换成右值引用,避免了数据拷贝,而是直接转移所有权。
移动构造时发生了什么?
以 std::vector 为例,里面有三个关键成员:
指向堆区的指针 ptr
当前元素个数 size
容量 capacity
拷贝构造
会重新分配一块堆内存,把 v1 的数据拷贝过去。
移动构造
直接把 v1 的内部指针 ptr 交给 v2。
然后把 v1 的 ptr 置空,size 和 capacity 设为 0。
所以:
v1 变成一个 空的 vector(但依然是合法对象)。
v2 拥有了原来 v1 的那块内存。
v2 用完之后,内存会销毁吗?
是的。
当 v2 生命周期结束时,它会调用析构函数,释放那块堆内存。
而 v1 在 std::move 之后,它的 ptr 已经被清空,所以它的析构函数不会释放任何东西(避免二次释放)。
直观理解(类比搬家 🚚)
v1 原来有一套家具(堆内存)。
std::move(v1) 把家具的所有权交给了 v2。
v1 自己变成了一个空房子(里面啥都没有,但房子还在)。
当 v2 销毁时,它负责把家具丢掉。
示例
- 问题:没有移动语义时的性能问题
假设我们有一个BigData类,它内部持有一个很大的动态数组(这里用std::vector模拟)。拷贝这个类的对象会非常昂贵,因为它需要分配新的内存并复制所有数据。
#include <iostream>
#include <vector>
#include <chrono>class BigData {
private:std::vector<int> m_data; // 模拟一个很大的数据块public:// 构造函数,分配大量数据BigData(size_t size) : m_data(size) {std::cout << "Constructor called. Allocated " << size << " elements." << std::endl;}// 拷贝构造函数(深拷贝)- 性能瓶颈!BigData(const BigData& other) : m_data(other.m_data) {std::cout << "Copy Constructor called. Expensive deep copy!" << std::endl;}// 拷贝赋值运算符(深拷贝)BigData& operator=(const BigData& other) {if (this != &other) {m_data = other.m_data; // 又一次昂贵的拷贝!}std::cout << "Copy Assignment called. Expensive deep copy!" << std::endl;return *this;}// 析构函数~BigData() {std::cout << "Destructor called." << std::endl;}
};// 一个函数,返回一个BigData对象
BigData createBigData() {BigData data(1000000); // 在函数内部创建一个大对象return data; // 传统C++03中,这里可能会触发拷贝
}int main() {BigData my_data = createBigData(); // 这里期望得到函数内部创建的对象return 0;
}
- 解决方案:实现移动语义
移动语义允许我们“窃取”即将被销毁的对象的资源,而不是进行昂贵的拷贝。我们通过定义移动构造函数和移动赋值运算符来实现这一点。
#include <iostream>
#include <vector>
#include <utility> // for std::moveclass BigData {
private:std::vector<int> m_data;public:BigData(size_t size) : m_data(size) {std::cout << "Constructor called. Allocated " << size << " elements." << std::endl;}// 1. 移动构造函数 (参数是非常量右值引用 BigData&&)BigData(BigData&& other) noexcept // noexcept 很重要,用于标准库优化: m_data(std::move(other.m_data)) // 关键:使用std::move移动内部的vector{std::cout << "Move Constructor called. Efficient move!" << std::endl;// 移动后,源对象‘other’的m_data现在处于有效但未状态(通常是空)}// 2. 移动赋值运算符BigData& operator=(BigData&& other) noexcept {if (this != &other) {m_data = std::move(other.m_data); // 关键:移动赋值}std::cout << "Move Assignment called. Efficient move!" << std::endl;return *this;}// 保留拷贝构造和拷贝赋值,实现“Rule of Five”BigData(const BigData& other) = default;BigData& operator=(const BigData& other) = default;~BigData() = default;
};// 工厂函数
BigData createBigData() {BigData data(1000000);return data; // 编译器意识到‘data’是局部对象,是“将亡值”// 优先选择移动构造函数,即使没有移动构造也会尝试拷贝
}int main() {std::cout << "--- Scenario 1: Return from function ---" << std::endl;BigData my_data = createBigData(); // 调用移动构造函数(如果优化不掉)std::cout << "\n--- Scenario 2: Explicit std::move ---" << std::endl;BigData data1(1000);BigData data2 = std::move(data1); // 使用std::move将左值强制转换为右值,// 从而调用移动构造函数。// data1此后不应再被使用!return 0;
}
代码关键点解释:
移动构造函数 BigData(BigData&& other):
参数类型是BigData&&,这是一个右值引用,它只能绑定到临时对象或即将被销毁的对象(“将亡值”)。
它的作用是“窃取”源对象other的资源。在这里,我们使用std::move(other.m_data)来移动其内部的vector。std::move的本质是一个static_cast,它将变量强制转换为右值,从而触发vector自身的移动构造函数。
noexcept关键字向标准库承诺这个操作不会抛出异常,这很重要,因为标准库容器(如std::vector)在重新分配内存时会优先使用noexcept的移动操作而不是拷贝操作来转移元素,性能更高。
移动赋值运算符 operator=(BigData&& other):
原理同移动构造函数,用于在赋值时移动资源。
std::move:
它本身不移动任何东西!它只是一个转换工具,告诉编译器:“我知道这个左值对象我不再需要了,请把它当作一个右值来处理”。
真正的移动操作是在类的移动构造函数或移动赋值运算符中完成的。
Rule of Five:
如果你定义了析构函数、拷贝构造函数、拷贝赋值运算符中的任何一个,那么你应该定义全部五个(加上移动构造函数和移动赋值运算符)来精确管理资源。在上面的例子中,我们显式定义了移动操作,并用= default保留了默认的拷贝操作。
现代编译器非常智能,通常会直接进行“返回值优化”,连移动构造都省略了。但在更复杂的返回路径中,移动语义是重要的保障。
实际应用:编译器的“神来之笔”与程序员的“安全网”
想象一下你要从A城市(函数内)运送一批贵重家具(大数据)到B城市(函数外)。
没有优化(C++03时代):你得先在A城找个仓库(函数栈帧)放家具,然后雇辆卡车,把家具一件件搬上卡车(拷贝),运到B城后,再一件件卸下来放到新家。费时费力!
有移动语义(C++11基础):你发现这些家具到了B城后,A城的仓库就要拆了。所以你很聪明,只把仓库的所有权转让给B城的人。你只需要把仓库地址告诉他(移动,即交换指针),他自己去取。省去了搬运的体力活!
返回值优化RVO(返回值优化)/NRVO(编译器优化):编译器这个“上帝”看到了你的整个计划。它直接说:“别在A城建仓库了,我直接在B城给你建好,你一开始就把家具放那里!”它完全消除了“移动”或“拷贝”这个动作本身。这是最极致的效率。
- 编译器的“神来之笔”:返回值优化
返回值优化是编译器被标准允许的一种优化,它可以直接在函数外部(调用者的栈帧上)构造本应在函数内部返回的对象,从而完全避免任何拷贝或移动操作。
代码示例:
BigData createBigData() {BigData data(1000000); // 理论上,这里在函数内部构造`data`// ... 一些对data的操作return data; // 理论上,这里需要将`data`返回出去
}int main() {BigData my_data = createBigData(); // 理论上,这里需要接收返回的`data`return 0;
}
未优化时的逻辑路径:
在createBigData函数内部调用构造函数BigData(1000000),创建data。
函数返回时,调用拷贝/移动构造函数,用data构造一个临时对象。
在main函数中,调用拷贝/移动构造函数,用临时对象构造my_data。
销毁临时对象。
函数结束,销毁data。
启用RVO/NRVO后的实际路径:
编译器会偷偷重写你的代码,变成类似这样:
void createBigData(BigData& hidden_obj) { // 编译器偷偷传进来一个引用hidden_obj.BigData(1000000); // 直接在目标位置构造!// ... 一些对hidden_obj的操作return; // 直接返回,没有任何拷贝!
}int main() {BigData my_data; // 只分配空间,未初始化createBigData(my_data); // 编译器偷偷把my_data的引用传进去构造return 0;
}
你看,my_data其实就是函数里的data,它们根本就是同一个对象! 这就是所谓的“连移动构造都省略了”,因为移动都不需要了。
“通常”这个词的含义:在现代编译器中,对于这种简单的返回局部对象的场景,RVO优化非常强大且几乎总是会发生(尤其是在Release模式下)。所以你可能在实际运行中看不到移动构造函数被调用。
- 程序员的“安全网”:移动语义的保障
那么问题来了,既然编译器这么聪明,我们为什么还要费心写移动语义呢?
因为编译器不是万能的上帝,它只能在简单的、确定的代码路径中进行这种优化。一旦代码变得复杂,优化就可能失败。
BigData createBigData(bool flag) {BigData data1(1000000);BigData data2(1000000);if (flag) {return data1; // 可能返回这个分支} else {return data2; // 也可能返回那个分支}// 编译器懵了:我该在调用者那里预先构造data1还是data2?
}int main() {BigData my_data = createBigData(true); // RVO优化可能失败!return 0;
}
特性 | RVO(返回值优化) | 移动语义 |
---|---|---|
实施者 | 编译器 | 程序员 (通过编写移动构造函数) |
发生阶段 | 编译时 | 运行时 |
实现原理 | 重新解释代码逻辑,直接在目标内存地址构造对象。 | 资源所有权的转移(例如,交换指针)。 |
所需条件 | 代码路径简单,符合标准要求。 | 对象必须实现了移动构造函数/赋值运算符。 |
本质 | 消除“拷贝/移动”这个操作本身。 | 将“昂贵的拷贝”操作替换为“廉价的移动”操作。 |
代码表现 | 看不见任何调用(构造函数、移动构造都没有)。 | 能看到移动构造函数被调用。 |