CPP学习笔记—右值引用和移动语义
右值引用(Rvalue Reference)和移动语义(Move Semantics)是 C++11 中引入的最重要的特性之一,它极大地提升了 C++ 的性能,并使得一些新的编程范式(如资源所有权的唯一性)成为可能。
1. 背景知识:左值(Lvalue)与右值(Rvalue)
在 C++ 中,每一个表达式都有两个属性:类型(Type) 和 值类别(Value Category)。值类别中最基本的就是左值和右值。
左值 (Lvalue - Locator Value)
可以把它粗略地理解为 “有固定内存地址、可以被赋值” 的表达式。它就像一个有名字、有固定住址的“居民”。
- 特征:
- 可以取地址(使用
&运算符)。 - 通常出现在赋值运算符
=的左边。 - 在表达式结束后依然存在。
- 可以取地址(使用
- 例子:
- 变量名:
int x = 10;(x是一个左值)。 - 数组元素:
arr[0]。 - 解引用的指针:
*p。 - 返回左值引用的函数调用:
get_string_ref()。
- 变量名:
右值 (Rvalue - Read Value)
可以把它粗略地理解为 “临时的、即将被销毁” 的值。它就像一个信件里的内容,读完就扔了,没有固定的“家”。
- 特征:
- 不可以取地址。
- 不能出现在赋值运算符
=的左边。 - 生命周期很短,通常在表达式结束时就被销毁。
- 例子:
- 字面量:
10,true,"hello"。 - 算术表达式的结果:
x + y。 - 按值返回的函数调用:
get_string()。 this指针。
- 字面量:
一个简单的区分方法:能对表达式取地址的就是左值,不能的就是右值。
1 | int a = 42; |
2. C++11 前的困境:不必要的深拷贝
考虑一个管理动态内存的类,比如一个简单的字符串类 MyString。
1 | class MyString { |
现在看一个场景:
1 | MyString make_string() { |
make_string() 函数返回了一个临时的 MyString 对象。这个临时对象是一个右值。为了用这个临时对象初始化 s2,编译器会调用拷贝构造函数:
- 在
make_string内部,构造一个MyString("world")对象。 make_string返回时,创建一个临时的MyString对象(右值)。- 调用
MyString的拷贝构造函数,将这个临时对象的内容深拷贝到s2中。这涉及:- 为
s2._data分配新内存。 - 将临时对象的数据逐字节拷贝到新内存中。
- 为
- 临时对象被销毁,其内部的
_data被delete[]。
问题:那个临时对象马上就要被销毁了,它里面的资源(_data 指向的内存)完全可以直接“偷”过来给 s2 用,而不是花大力气去重新分配内存并拷贝一遍内容。这种不必要的深拷贝在处理大型对象(如容器、矩阵、文件流等)时会造成巨大的性能浪费。
3. 核心概念:右值引用(Rvalue Reference)
为了解决上述问题,C++11 引入了右值引用,专门用来“绑定”右值。
- 语法:
T&&(类型 + 双与号) - 作用:
- 识别右值:通过函数重载,我们可以为左值和右值提供不同的实现。
- 延长生命周期:虽然右值引用本身可以延长临时对象的生命周期(不常用),但其主要目的是在函数参数中识别出右值。
1 | int x = 10; // x 是左值 |
特例:
const左值引用 (const T&) 是一个“万能引用”,它可以绑定到左值、右值、const对象和非const对象。这是 C++11 之前能够接收临时对象作为参数的原因。但它只提供了只读访问,我们无法修改它,也就无法“偷”走它的资源。
有了右值引用,我们就可以通过函数重载来区分传入的是左值还是右值:
1 | void process(int& x) { |
4. 核心应用:移动语义(Move Semantics)
移动语义的核心思想是:对于即将消亡的对象(右值),我们不进行内容的“拷贝”,而是进行资源的“转移”或“窃取”。
这是通过重载移动构造函数和移动赋值运算符来实现的,它们的参数都是右值引用。
移动构造函数 (Move Constructor)
- 签名:
ClassName(ClassName&& other) - 实现:
- 从
other对象中“窃取”资源指针。 - 将
other对象置于一个有效但无资源的状态(例如,将其内部指针设为nullptr)。这至关重要,因为other的析构函数马上会被调用,我们不希望它释放我们刚刚偷走的资源。
- 从
我们来为 MyString 添加移动构造函数:
1 | class MyString { |
现在再看之前的例子:
1 | MyString s2 = make_string(); |
make_string()返回一个临时对象(右值)。- 编译器发现有一个参数为
MyString&&的构造函数,并且传入的实参是右值,完美匹配。 - 调用移动构造函数。
s2直接接管了临时对象的_data指针,几乎没有成本。 - 临时对象的析构函数被调用,但此时它的
_data已经是nullptr,delete[] nullptr是安全无害的操作。
结果:没有了昂贵的内存分配和数据拷贝,性能大幅提升!
移动赋值运算符 (Move Assignment Operator)
- 签名:
ClassName& operator=(ClassName&& other) - 实现:逻辑与移动构造函数类似,但需要先释放自己已有的资源。
1 | class MyString { |
使用场景:
1 | MyString s1("old"); |
noexcept 的重要性
标准库中的许多容器(如 std::vector)在进行扩容等操作时,会把旧内存的元素移动到新内存。如果移动构造函数可能抛出异常,容器为了保证强异常安全(要么操作成功,要么对象状态回滚到操作前),就不敢使用移动操作,而会退回到更安全的拷贝操作。将移动操作标记为 noexcept 是在向编译器承诺“我的移动绝不抛异常”,这样容器等才能放心地使用移动语义进行优化。
5. 关键工具:std::move
我们已经知道,右值引用不能绑定到左值。但有时我们有一个左值,并且确定它之后不再需要了,希望也能对它执行移动操作以提高性能。这时就需要 std::move。
std::move不做任何移动操作!std::move的唯一功能是:将一个左值强制转换为右值引用。
它就像一个“强转”工具,它告诉编译器:“请把这个左值当作一个右值来处理,我保证它之后没用了,你可以放心地移动它的资源。”
1 | MyString s1("hello"); |
警告:使用 std::move 之后,原对象(上例中的 s1)的状态是“被掏空”了。虽然它仍然是一个合法的对象(可以被析构、被重新赋值),但你不应该再对其原始值做任何假设。访问一个被移动过的对象(除了赋值和析构)是未定义行为。
6. 延伸话题:完美转发(Perfect Forwarding)
这是一个更高级的应用,主要用在泛型编程(模板)中。
问题:假设我们要写一个工厂函数,它接收一些参数,然后用这些参数去构造一个对象。
1 | template<typename... Args> |
我们希望:
- 如果传给
create_string的是左值,那么MyString的构造函数也接收到左值。 - 如果传给
create_string的是右值,那么MyString的构造函数也接收到右值(从而触发移动构造)。
如果像上面那样写,args 在 create_string 函数内部本身是一个左值(因为它有名字),直接传递会导致右值变成左值,移动语义就失效了。
为了解决这个问题,C++11 提供了完美转发机制,它由两部分组成:
转发引用(Forwarding Reference,也叫万能引用 Universal Reference)
- 当模板参数
T以T&&的形式出现在函数参数中时,它就是一个转发引用。 - 它既可以接收左值,也可以接收右值。
- 引用折叠规则:
- 传左值
L给T&&,T被推导为L&,参数类型为L& &&->L&。 - 传右值
R给T&&,T被推导为R,参数类型为R&&->R&&。
- 传左值
- 当模板参数
std::forward- 一个条件转换工具,配合转发引用使用。
- 如果传递给转发引用的原始实参是右值,
std::forward就将其转换为右值引用。 - 如果传递给转发引用的原始实参是左值,
std::forward就什么都不做,保持其左值引用属性。
正确的工厂函数写法:
1 | template<typename... Args> |
通过 T&& 和 std::forward 的组合,我们实现了参数值类别的完美传递,确保了无论传递何种参数,都能调用到最合适的函数版本,充分利用移动语义。
7. 总结与实践
带来的好处
- 性能提升:极大地减少了临时对象和大型对象拷贝带来的开销,尤其是在函数返回值、容器操作(如
push_back)等场景。 - 实现移动专有类型:一些资源是不能被复制,只能被转移的,例如文件句柄、网络套接字、线程所有权。移动语义使得
std::unique_ptr,std::thread,std::fstream等“移动专有”(Move-only)类型成为可能。
实际应用场景
- STL 容器:
std::vector的push_back有两个重载版本,一个接收const T&(拷贝),一个接收T&&(移动)。当你push_back一个右值时,会触发移动操作。emplace_back则利用完美转发,直接在容器内部构造元素,避免了任何拷贝和移动。 - 智能指针:
std::unique_ptr禁止拷贝,但允许移动,清晰地表达了对所管理资源的“唯一所有权”的转移。 - 函数返回值:现代 C++ 中,按值返回大型对象(如
std::vector)不再是性能杀手,因为编译器会利用移动语义(或返回值优化 RVO)来避免拷贝。










