C++11 核心特性深度解析(二):右值引用与移动语义
一、右值引用与移动语义
1.1 左值与右值
核心定义
| 类别 | 定义 | 特征 |
|---|---|---|
| 左值(lvalue) | 表示数据的表达式,有持久状态,存储在内存中 | 可以取地址,可以出现在赋值符号左边或右边 |
| 右值(rvalue) | 字面值常量或表达式求值过程中创建的临时对象 | 不能取地址,只能出现在赋值符号右边 |
现代解释:
lvalue = loactor value:有明确存储地址的对象
rvalue = read value:提供数据值但不可寻址的对象
#include <iostream>
using namespace std;
int main() {
// ========== 左值:可以取地址 ==========
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
cout << &c << endl; // OK
cout << (void*)&s[0] << endl; // OK
// ========== 右值:不能取地址 ==========
double x = 1.1, y = 2.2;
10; // 字面值常量
x + y; // 表达式结果
fmin(x, y); // 函数返回值(传值返回)
string("11111"); // 匿名对象
// 以下全部编译错误:
// cout << &10 << endl;
// cout << &(x+y) << endl;
// cout << &(fmin(x,y)) << endl;
// cout << &string("11111") << endl;
return 0;
}

1.2 左值引用与右值引用

语法对比

引用规则
| 引用类型 | 能否引用左值 | 能否引用右值 | 方式 |
|---|---|---|---|
左值引用 T& |
✅ | ❌ | 直接绑定 |
const 左值引用 const T& |
✅ | ✅ | 直接绑定 |
右值引用 T&& |
❌ | ✅ | 直接绑定 |
右值引用 T&& |
✅(通过 move) | — | move(左值) 或强制转换 |

#include <iostream>
using namespace std;
int main() {
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
double x = 1.1, y = 2.2;
// ========== 左值引用给左值取别名 ==========
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];
// ========== 右值引用给右值取别名 ==========
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");
// ========== const 左值引用引用右值 ==========
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
// ========== 右值引用引用 move(左值) ==========
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s; // 等价于 move
// ========== 关键:变量表达式都是左值属性 ==========
cout << &b << endl; // OK,b 是左值
cout << &r1 << endl; // OK,r1 是左值引用变量,表达式属性是左值
cout << &rr1 << endl; // OK,rr1 是右值引用变量,表达式属性是左值!
// 因此:rr1 的属性是左值,不能再被右值引用绑定,除非 move 一下
int& r6 = r1; // OK
// int&& rrx6 = rr1; // 错误!rr1 是左值属性
int&& rrx6 = move(rr1); // OK
return 0;
}


std::move 的实现
template <class T>
remove_reference_t<T>&& move(T&& _Arg) {
// forward _Arg as movable
return static_cast<remove_reference_t<T>&&>(_Arg);
}
本质:move 内部是强制类型转换,将左值转换为右值引用。
重要设计:右值引用变量表达式是左值
int&& x = 1; // x 是右值引用
f(x); // 调用 f(int& x) —— x 是左值属性!
f(move(x)); // 调用 f(int&& x) —— move 后恢复右值属性
这个设计看似奇怪,但在移动语义的使用场景中非常有价值
1.3 引用延长生命周期
右值引用可用于为临时对象延长生命周期,
const左值引用也能做到,但无法修改:
int main() {
std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:不能绑定到左值
const std::string& r2 = s1 + s1; // OK:const 左值引用延长生存期
// r2 += "Test"; // 错误:不能通过 const 引用修改
std::string&& r3 = s1 + s1; // OK:右值引用延长生存期
r3 += "Test"; // OK:能通过非 const 引用修改
std::cout << r3 << '\n';
return 0;
}
1.4 左值和右值的参数匹配
C++98 的局限

C++11 的精确匹配
#include <iostream>
using namespace std;
void f(int& x) {
cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x) {
cout << "const 左值引用重载 f(" << x << ")\n";
}
void f(int&& x) {
cout << "右值引用重载 f(" << x << ")\n";
}
int main() {
int i = 1;
const int ci = 2;
f(i); // 调用 f(int&) —— 左值
f(ci); // 调用 f(const int&) —— const 左值
f(3); // 调用 f(int&&) —— 右值
f(move(i)); // 调用 f(int&&) —— move 后的左值
int&& x = 1;
f(x); // 调用 f(int&) —— x 是左值属性!
f(move(x)); // 调用 f(int&&) —— move 后恢复右值属性
return 0;
}
匹配规则总结:
| 实参类型 | 匹配优先级 |
|---|---|
| 左值 | T& > const T& |
| const 左值 | const T& |
| 右值 | T&& > const T& |
1.5 右值引用和移动语义的使用场景

1.5.1 左值引用的局限
左值引用已经解决了大多数场景的拷贝效率问题(传参、传返回值),但有些场景不能使用传左值引用返回:
class Solution {
public:
// 传值返回需要拷贝 —— str 是局部对象,函数结束就销毁
string addStrings(string num1, string num2) {
string str;
// ... 计算逻辑 ...
return str; // C++98:只能拷贝;C++11:可以移动!
}
// 传值返回代价更大
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
// ... 计算逻辑 ...
return vv;
}
};
问题本质:返回对象是局部对象,函数结束就析构销毁了,即使是右值引用返回也无法改变这个事实。
1.5.2 移动构造和移动赋值

定义
| 函数 | 参数类型 | 行为 |
|---|---|---|
| 移动构造 | T&&(右值引用) |
"窃取"右值对象的资源,而非拷贝 |
| 移动赋值 | T&&(右值引用) |
"窃取"右值对象的资源,而非拷贝赋值 |
适用场景
对于 string、vector 等深拷贝的类,移动构造和移动赋值才有意义:
namespace bit {
class string {
public:
// 普通构造
string(const char* str = "")
: _size(strlen(str)), _capacity(_size) {
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造 —— 深拷贝
string(const string& s)
: _str(nullptr) {
cout << "string(const string& s)--拷贝构造" << endl;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// ========== 移动构造 —— 窃取资源 ==========
string(string&& s) {
cout << "string(string&& s)--移动构造" << endl;
swap(s); // 直接交换指针,不拷贝数据!
}
// 拷贝赋值
string& operator=(const string& s) {
cout << "string& operator=(const string& s)--拷贝赋值" << endl;
if (this != &s) {
// ... 深拷贝逻辑 ...
}
return *this;
}
// ========== 移动赋值 —— 窃取资源 ==========
string& operator=(string&& s) {
cout << "string& operator=(string&& s)--移动赋值" << endl;
swap(s); // 直接交换指针
return *this;
}
void swap(string& s) {
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
~string() {
delete[] _str;
_str = nullptr;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
int main() {
bit::string s1("xxxxx");
bit::string s2 = s1; // 拷贝构造
bit::string s3 = bit::string("yyyyy"); // 构造 + 移动构造,优化后直接构造
bit::string s4 = move(s1); // 移动构造 —— s1 的资源被"窃取"
return 0;
}
移动语义的核心:不是拷贝数据,而是交换指针,让原对象交出资源所有权。
1.5.3 解决传值返回问题
namespace bit {
string addStrings(string num1, string num2) {
string str;
// ... 计算 ...
return str;
}
}
// 场景1:用返回值构造对象
int main() {
bit::string ret = bit::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
// 场景2:赋值给已有对象
int main() {
bit::string ret;
ret = bit::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
编译器优化演进
| 环境 | 无移动语义 | 有移动语义 | 现代编译器优化 |
|---|---|---|---|
| VS2019 Debug | 2 次拷贝构造 | 2 次移动构造 | — |
| VS2019 Release / VS2022 | — | — | 合三为一,直接构造 |
| Linux (g++ -fno-elide-constructors) | 2 次拷贝 | 2 次移动 | — |
-
无移动语义 + 无优化:
str→ 拷贝构造 → 临时对象 → 拷贝构造 →ret -
有移动语义 + 无优化:
str→ 移动构造 → 临时对象 → 移动构造 →ret -
现代编译器优化(NRVO/RVO):直接在
ret的内存位置构造str,零拷贝!
右值对象构造,只有拷贝构造,没有移动构造的场景


右值对象构造,只有拷贝构造,也有移动构造的场景



右值对象构造,只有拷贝构造和拷贝复制,没有移动构造的移动赋值场景


右值对象构造,只有拷贝构造和拷贝复制,也有移动构造的移动赋值场景


赋值场景的优化
bit::string ret; ret = bit::addStrings("11111", "2222");
无移动语义:拷贝构造临时对象 + 拷贝赋值给
ret有移动语义:移动构造临时对象 + 移动赋值给
ret现代编译器:直接构造临时对象,
str本质是其引用,底层用指针实现
3.5.4 在传参中的提效
C++11 以后,容器的 push 和 insert 系列接口增加了右值引用版本:
// STL 新增接口
void push_back(const value_type& val); // 左值版本
void push_back(value_type&& val); // 右值版本
iterator insert(const_iterator position, value_type&& val);


1.6 类型分类
C++11 对值类别进行了更精细的划分:


| 类别 | 全称 | 说明 | 示例 |
|---|---|---|---|
| glvalue | generalized value(泛左值) | 有身份的对象 | 包含 lvalue 和 xvalue |
| lvalue | locator value | 可寻址的持久对象 | 变量名、*p、s[0] |
| xvalue | expiring value(将亡值) | 即将被移动的对象 | move(x)、static_cast<X&&>(x) |
| prvalue | pure rvalue(纯右值) | 纯临时值 | 42、a+b、str.substr(1,2) |
| rvalue | — | 包含 xvalue 和 prvalue | — |
C++98 的右值 ≈ C++11 的 prvalue
1.7 引用折叠
++ 不能直接定义引用的引用(
int&&& r = i;报错),但通过模板或 typedef 可以构成引用的引用:
规则:右值引用的右值引用折叠成右值引用,所有其他组合折叠成左值引用。
| 类型组合 | 折叠结果 |
|---|---|
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // int& & → int&
lref&& r2 = n; // int& && → int&
rref& r3 = n; // int&& & → int&
rref&& r4 = 1; // int&& && → int&&
万能引用(Universal Reference)

万能引用的判定条件:
必须是函数模板参数
必须是
T&&形式(const T&&不是万能引用)
template<class T>
void f1(T& x) {} // 总是左值引用
template<class T>
void f2(T&& x) {} // 可能是左值引用或右值引用
int main() {
int n = 0;
// f1 实例化
f1<int>(n); // void f1(int& x) —— 无折叠
// f1<int>(0); // 错误!右值不能绑定到左值引用
f1<int&>(n); // void f1(int& x) —— int& & → int&
f1<int&&>(n); // void f1(int& x) —— int&& & → int&
// f2 实例化
f2<int>(n); // 错误!void f2(int&& x),左值不能绑定
f2<int>(0); // OK!void f2(int&& x)
f2<int&>(n); // void f2(int& x) —— int& && → int&
// f2<int&>(0); // 错误!
f2<int&&>(n); // 错误!void f2(int&& x),左值不能绑定
f2<int&&>(0); // OK!void f2(int&& x) —— int&& && → int&&
}
1.8 完美转发
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x){ cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x){ cout << "const 右值引用" << endl; }
template<class T>
void Function(T&& t) { // 万能引用
Fun(t); // 问题:t 是变量,表达式属性是左值!
// 无论传进来的是左值还是右值,都匹配 Fun(int&)
}
问题根源:变量表达式都是左值属性,右值被右值引用绑定后,右值引用变量 t 的属性变成了左值。
std::forward 实现
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept {
// forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg);
}
原理:
-
实参是右值 →
T推导为int→forward<int>(t)→static_cast<int&&>(t)→ 返回右值引用 -
实参是左值 →
T推导为int&→forward<int&>(t)→static_cast<int& &&>(t)→ 折叠为int&→ 返回左值引用
template<class T>
void Function(T&& t) {
Fun(forward<T>(t)); // 保持 t 的原始属性
}
int main() {
Function(10); // 右值 → Fun(int&&)
int a;
Function(a); // 左值 → Fun(int&)
Function(move(a)); // 右值 → Fun(int&&)
const int b = 8;
Function(b); // const 左值 → Fun(const int&)
Function(move(b)); // const 右值 → Fun(const int&&)
return 0;
}
完美转发的使用场景
模板函数中传递参数:保持参数的原始值类别
工厂函数:
make_unique、make_sharedemplace 系列接口:将参数包完美转发给构造函数
template <class... Args>
void emplace_back(Args&&... args) {
insert(end(), forward<Args>(args)...); // 完美转发参数包
}
| 特性 | C++98 | C++11 | 核心价值 |
|---|---|---|---|
| 初始化方式 | ()、= 不统一 |
{} 统一初始化 |
语法统一,防止窄化转换 |
| 容器多值初始化 | 不支持 | std::initializer_list |
简洁、高效 |
| 传值返回优化 | 只能拷贝 | 移动语义 | 避免深拷贝,性能提升显著 |
| 参数传递 | 左值引用/传值 | 右值引用 + 完美转发 | 按需选择拷贝或移动 |
| 模板编程 | 固定参数 | 可变参数模板 | 更灵活的泛型编程 |
更多推荐

所有评论(0)