[C++11] 右值引用、移动语义与std::move
C++11 右值引用、移动语义与 std::move 详解
头文件:<utility>
一、什么是:左值 & 右值
在 C++ 中,表达式根据能否取地址、是否持久存在,分为左值和右值,这是理解后续知识点的基础。
1. 左值(lvalue)
- 定义:有名字、可以取地址的表达式,程序运行中内存持久存在。
- 常见例子:变量、数组元素、引用。
int a = 10; // a 是左值,有变量名,可以 &a 取地址
int b = a; // a 依旧是左值
int arr[3] = {1,2,3};
arr[0]; // 数组元素也是左值
- 特点:能出现在赋值运算符
=左边,所以叫左值。a = 20;合法,a是左值。
2. 右值(rvalue)
- 定义:没有名字、不能取地址的临时数据,用完就销毁,生命周期极短。
- 常见例子:字面量、表达式运算结果、函数返回的临时对象。
100; // 字面量,右值,无法 &100 取地址
a + b; // 加法运算结果,临时值,右值
std::string("test"); // 临时字符串对象,右值
- 特点:只能出现在赋值运算符
=右边,不能取地址。100 = a;编译报错,右值不能被赋值。
简易记忆口诀
有名字、能取地址 → 左值
无名字、临时值、不能取地址 → 右值
二、左值引用 & C++11 新增:右值引用
引用就是给变量起别名,C++98 只有左值引用,C++11 补充了右值引用。
1. 回顾:C++98 左值引用 &
语法:类型 & 引用名 = 左值;
- 只能绑定左值,不能直接绑定纯右值。
int a = 10;
int &ra = a; // 合法:左值引用 绑定 左值
// int &rb = 20; // 编译报错!左值引用不能直接绑定字面量(右值)
const int &rc = 20; // 特殊:const 左值引用可以绑定右值(C++98 特性)//这么做会延长右值的生命周期
2. C++11 右值引用 &&
语法:类型 && 引用名 = 右值;
- 作用:专门用来绑定右值(临时对象)
- 符号:两个
&&,和左值引用&区分开
基础示例
// 1. 绑定字面量(右值)
int &&r1 = 100;
r1 = 200; // 右值引用本质也是别名,可以修改
cout << r1; // 输出 200
//普通右值引用 T&& 可以修改;const T&& 不能修改(几乎不用)。
// 2. 绑定表达式临时结果(右值)
int x = 1, y = 2;
int &&r2 = x + y;
cout << r2; // 输出 3
关键特性
- 右值引用只能绑定右值,不能直接绑定普通左值
int a = 10; // int &&r = a; // 编译报错:左值不能绑定到右值引用 - 一旦右值引用绑定了临时右值,这个引用本身就变成了左值(有名字、可寻址)。
三、为什么需要右值引用?—— 解决拷贝浪费
1. 传统拷贝的问题
对于自定义类、字符串、容器这类占用堆内存的对象,普通拷贝会做深拷贝:
完整复制一份内存数据,临时对象产生 → 拷贝 → 临时对象销毁,产生大量不必要的内存开销和性能损耗。
举个例子:自定义字符串类(模拟 std::string)
#include <iostream>
#include <cstring>
using namespace std;
class MyString
{
private:
char *data;
public:
// 构造函数
MyString(const char *str)
{
cout << "构造函数" << endl;
int len = strlen(str);
data = new char[len + 1];
strcpy(data, str);
}
// 拷贝构造函数(深拷贝)
MyString(const MyString &other)
{
cout << "拷贝构造函数(深拷贝)" << endl;
int len = strlen(other.data);
data = new char[len + 1]; // 重新开辟内存
strcpy(data, other.data);
}
// 析构函数
~MyString()
{
cout << "析构函数" << endl;
delete[] data;
}
};
// 返回临时 MyString 对象(右值)
MyString getTempStr()
{
return MyString("hello c++11");
}
int main()
{
MyString s = getTempStr();
return 0;
}
执行流程(C++98 模式)
getTempStr()内部创建临时对象 → 调用构造函数- 临时对象赋值给
s→ 调用拷贝构造函数(完整复制内存) - 临时对象生命周期结束 → 调用析构函数释放内存
问题:临时对象马上就要销毁,我们明明可以直接把临时对象的内存“抢过来”用,却还要完整拷贝一遍,纯纯资源浪费。
2. 解决方案:移动语义
移动语义:借助右值引用,把即将销毁的临时对象(右值) 的内存资源,直接转移给新对象,而非拷贝。
- 动作:转移所有权,不是复制数据
- 效率:几乎零开销,比深拷贝快得多
四、移动构造函数(移动语义核心)
C++11 允许我们重载移动构造函数,语法基于右值引用。
1. 语法格式
类名(类名 && 临时对象)
{
// 转移资源,不拷贝
}
2. 给上面的MyString 添加移动构造函数
// 移动构造函数:参数是 右值引用
MyString(MyString &&other)
{
cout << "移动构造函数(转移资源)" << endl;
// 1. 直接接管对方的堆内存指针
data = other.data;
// 2. 把原对象指针置空,防止原对象析构时重复释放内存
other.data = nullptr;
}
3. 完整运行 & 效果对比
再次运行 MyString s = getTempStr();
C++11 执行流程:
- 创建临时对象 → 构造函数
- 临时对象是右值,编译器自动匹配 移动构造函数(转移指针,无内存拷贝)
- 临时对象析构(指针已置空,不会释放有效内存)
总结:有了移动构造,临时对象不再深拷贝,直接移交资源,性能大幅提升。
补充规则
- 编译器匹配规则:
- 传入左值 → 调用 拷贝构造(
const 类&) - 传入右值 → 调用 移动构造(
类&&)
- 传入左值 → 调用 拷贝构造(
- 移动构造函数中,必须把源对象的资源指针置空,否则两个对象指向同一块堆内存,析构时会重复释放,程序崩溃。
五、std::move 函数(强制转为右值)
1. 作用
std::move 定义在 <utility> 头文件中,功能只有一个:
强制把一个左值,转换成右值
场景:明明是有名字的左值对象(不会马上销毁),但我们确定不再使用它,希望把它的资源移动给其他对象,而非拷贝。
2. 语法
std::move(左值变量)
3. 示例演示
沿用 MyString 类:
int main()
{
MyString s1("test move"); // s1 是普通左值
// 直接赋值:左值 → 调用拷贝构造
MyString s2 = s1;
// 使用 std::move:把左值 s1 强制转为右值
MyString s3 = std::move(s1);
return 0;
}
运行结果:
s2 = s1:调用 拷贝构造s3 = std::move(s1):s1被转为右值,调用 移动构造
4. 重要使用提醒
执行 std::move(s1) 之后:
s1的堆内存资源已经被转移走,内部指针变为nullptr;- 不要再正常使用 s1(相当于一个“空壳对象”),强行读写会出问题;
std::move本身不移动任何数据,它只是做类型转换,真正转移资源的是移动构造/移动赋值函数。
六、移动赋值运算符
和拷贝赋值对应,C++11 也支持移动赋值,同样使用右值引用。
1. 语法
类名 & operator=(类名 &&other)
{
cout << "移动赋值运算符" << endl;
// 1. 释放自身原有资源
delete[] data;
// 2. 接管对方资源
data = other.data;
// 3. 原对象置空
other.data = nullptr;
return *this;
}
2. 使用场景
MyString s4("aaa");
MyString s5("bbb");
s5 = std::move(s4); // 移动赋值,而非拷贝赋值
七、四大构造/赋值函数总结
C++11 之后,一个常规类默认存在 6 个特殊成员函数,这里区分核心 4 个:
| 函数类型 | 参数形式 | 触发场景 | 行为 |
|---|---|---|---|
| 普通构造函数 | 类名(参数) |
创建新对象时 | 初始化资源 |
| 拷贝构造函数 | 类名(const 类&) |
用左值对象初始化新对象 | 深拷贝数据 |
| 移动构造函数 | 类名(类&&) |
用右值对象初始化新对象 | 转移资源 |
| 拷贝赋值运算符 | operator=(const 类&) |
左值对象赋值 | 深拷贝数据 |
| 移动赋值运算符 | operator=(类&&) |
右值对象赋值 | 转移资源 |
八、常见使用场景 & 实战建议
1. 什么时候会自动触发移动语义?
函数返回临时对象、字面量构造的临时对象 → 编译器自动识别为右值,调用移动构造。
2. 什么时候手动用 std::move?
明确某个左值对象后续不再使用,想把它的资源转移出去,手动转成右值。
例:容器元素转移、局部对象移交。
3. 注意事项
- 右值引用
&&只能绑定右值,普通左值不能直接赋值给右值引用; std::move只是类型转换,不是移动动作本身;- 移动资源后,原对象变为空壳,禁止继续正常使用;
- 移动构造/移动赋值中,一定要将源对象指针置空,防止重复析构崩溃;
- 内置类型(
int/double)移动和拷贝无区别,移动语义主要针对堆内存对象(string、vector、自定义类)。
总结
- 左值:有名字、可寻址;右值:临时值、无名字、不可寻址。
- C++11 新增 右值引用
&&,专门绑定右值。 - 移动语义:利用右值引用,转移临时对象资源,替代低效深拷贝。
- 移动构造/移动赋值:实现移动语义的核心函数。
- std::move:强制将左值转为右值,手动触发移动语义。
- 核心价值:优化内存拷贝,提升程序运行效率。
更多推荐


所有评论(0)