C++ 右值引用与移动语义
右值引用和移动语义是 C++11 引入的核心特性,它们从根本上改变了 C++ 资源管理的方式,带来了显著的性能提升。理解这些概念对于编写高效的现代 C++ 代码至关重要。
值类别
左值与右值
在理解右值引用之前,需要先明确值类别的概念:
左值(lvalue) 是有名字、有地址的表达式,可以出现在赋值号的左边。左值代表一个持久的对象,在表达式结束后仍然存在。
右值(rvalue) 是没有名字、没有持久地址的表达式,只能出现在赋值号的右边。右值代表临时对象,在表达式结束后就会被销毁。
int x = 10; // x 是左值,10 是右值
int y = x + 5; // y 是左值,x + 5 是右值
int& ref = x; // 正确:左值引用绑定到左值
// int& ref2 = 10; // 错误:左值引用不能绑定到右值
const int& cref = 10; // 正确:const 左值引用可以绑定到右值
判断左值和右值
一个简单的判断方法:如果能取地址,就是左值;否则是右值。
int x = 10;
&x; // 正确,x 是左值
// &10; // 错误,10 是右值
// &(x + 1); // 错误,x + 1 是右值
std::string s1 = "hello";
std::string s2 = "world";
std::string s3 = s1 + s2; // s1 + s2 是右值(临时对象)
&s1; // 正确,s1 是左值
// &(s1 + s2); // 错误,s1 + s2 是右值
常见例子
// 左值示例
int x = 10; // x 是左值
x = 20; // 可以赋值
int arr[5] = {1,2,3,4,5};
arr[0]; // 下标表达式是左值
*x; // 解引用是左值
std::string s;
s; // 变量是左值
// 右值示例
10; // 字面量是右值
x + 1; // 算术表达式结果是右值
x++; // 后置自增返回右值
std::string("hello"); // 临时对象是右值
s1 + s2; // 字符串拼接结果是右值
foo(); // 函数返回非引用类型是右值
右值引用
基本语法
右值引用使用 && 声明,只能绑定到右值:
int x = 10;
// 左值引用:只能绑定到左值
int& lr = x; // 正确
// int& lr2 = 10; // 错误
// 右值引用:只能绑定到右值
int&& rr = 10; // 正确
// int&& rr2 = x; // 错误
// const 左值引用:可以绑定到任何值
const int& clr = x; // 正确
const int& clr2 = 10; // 正确
右值引用的意义
右值引用的核心价值在于:可以修改右值,实现资源转移(移动语义)。
std::string s1 = "hello";
std::string s2 = "world";
// 传统方式:s1 + s2 创建临时对象,然后拷贝给 s3,临时对象销毁
std::string s3 = s1 + s2; // 可能涉及内存分配和拷贝
// 使用右值引用:直接"窃取"临时对象的资源
std::string&& rref = s1 + s2; // 绑定到临时对象
// 可以修改临时对象的内容
rref += "!"; // 直接修改临时对象
std::move
std::move 是将左值转换为右值引用的工具:
#include <utility> // std::move
std::string str = "hello";
// str 是左值
std::string& ref = str;
// std::move(str) 返回右值引用
std::string&& rref = std::move(str);
// 注意:std::move 只是类型转换,不执行任何移动
// 移动操作发生在赋值或构造时
std::string str2 = std::move(str); // 移动构造
// 此时 str 处于"移后状态"(可能为空,但不保证)
理解 std::move:
std::move 本质上是一个类型转换函数:
// std::move 的简化实现
template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
// 它只是将参数转换为右值引用,真正的移动由移动构造函数完成
移动语义
问题:不必要的拷贝
考虑一个持有动态内存的类:
#include <cstring>
class MyString {
private:
char* data;
size_t length;
public:
// 构造函数
MyString(const char* s = "") {
length = strlen(s);
data = new char[length + 1];
strcpy(data, s);
std::cout << "构造: " << data << std::endl;
}
// 拷贝构造函数
MyString(const MyString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "拷贝构造: " << data << std::endl;
}
// 拷贝赋值运算符
MyString& operator=(const MyString& other) {
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "拷贝赋值: " << data << std::endl;
}
return *this;
}
// 析构函数
~MyString() {
std::cout << "析构: " << (data ? data : "null") << std::endl;
delete[] data;
}
const char* c_str() const { return data; }
};
// 问题示例
MyString createString() {
MyString s("hello");
return s; // 返回时发生拷贝?
}
void problemDemo() {
MyString s1 = createString(); // 多次拷贝!
MyString s2 = s1; // 拷贝构造
s2 = s1; // 拷贝赋值
}
解决:移动构造和移动赋值
class MyString {
private:
char* data;
size_t length;
public:
// 构造函数
MyString(const char* s = "") {
length = strlen(s);
data = new char[length + 1];
strcpy(data, s);
}
// 拷贝构造函数
MyString(const MyString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "拷贝构造" << std::endl;
}
// 移动构造函数
MyString(MyString&& other) noexcept {
// 直接"窃取"资源
data = other.data;
length = other.length;
// 将源对象置于安全状态
other.data = nullptr;
other.length = 0;
std::cout << "移动构造" << std::endl;
}
// 拷贝赋值运算符
MyString& operator=(const MyString& other) {
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "拷贝赋值" << std::endl;
}
return *this;
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
// 释放自己的资源
delete[] data;
// 窃取对方的资源
data = other.data;
length = other.length;
// 将对方置于安全状态
other.data = nullptr;
other.length = 0;
std::cout << "移动赋值" << std::endl;
}
return *this;
}
~MyString() {
delete[] data;
}
};
// 使用示例
void moveDemo() {
MyString s1("hello");
// 移动构造
MyString s2 = std::move(s1); // s1 的资源转移给 s2
// 移动赋值
MyString s3;
s3 = std::move(s2); // s2 的资源转移给 s3
// 返回值优化
MyString s4 = createString(); // 可能直接移动或 RVO
}
移动语义的本质
移动语义的核心思想是:不进行深拷贝,而是转移资源的所有权。
拷贝语义:创建新资源,复制数据(慢)
┌───────────┐ ┌───────────┐
│ 原对象 │ ──────> │ 新对象 │
│ data ────────┐ │ data ────────┐
│ length │ │ │ length │ │
└───────────┘ │ └───────────┘ │
▼ ▼
[内存块] [内存块(副本)]
移动语义:转移资源指针(快)
┌───────────┐ ┌───────────┐
│ 原对象 │ │ 新对象 │
│ data=null│ │ data ────────┐
│ length=0 │ │ length │ │
└───────────┘ └───────────┘ │
▼
[内存块]
std::forward 和完美转发
引用折叠
当模板参数推导和引用结合时,会发生引用折叠:
template<typename T>
void func(T&& param) {}
int x = 10;
// 传入左值:T 推导为 int&,T&& 折叠为 int&
func(x); // T = int&, T&& = int& && = int&
// 传入右值:T 推导为 int,T&& 保持为 int&&
func(10); // T = int, T&& = int&&
// 引用折叠规则:
// & & = &
// & && = &
// && & = &
// && && = &&
完美转发
完美转发是指在函数模板中将参数以原始的值类别(左值或右值)转发给其他函数:
#include <utility>
void process(int& x) {
std::cout << "左值引用" << std::endl;
}
void process(int&& x) {
std::cout << "右值引用" << std::endl;
}
// 错误的转发:参数总是左值
template<typename T>
void wrongForward(T&& param) {
process(param); // param 是左值!总是调用左值版本
}
// 完美转发:保持原始值类别
template<typename T>
void perfectForward(T&& param) {
process(std::forward<T>(param)); // 正确转发
}
int main() {
int x = 10;
wrongForward(x); // 输出:左值引用
wrongForward(10); // 输出:左值引用(错误!应该是右值)
perfectForward(x); // 输出:左值引用
perfectForward(10); // 输出:右值引用(正确!)
return 0;
}
std::forward 的实现
// std::forward 的简化实现
template<typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
template<typename T>
T&& forward(typename std::remove_reference<T>::type&& t) noexcept {
static_assert(!std::is_lvalue_reference<T>::value,
"不能将右值转发为左值");
return static_cast<T&&>(t);
}
完美转发的应用
#include <utility>
#include <memory>
// 工厂函数:完美转发参数给构造函数
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
// 包装函数:保持参数的值类别
template<typename Func, typename... Args>
auto invoke(Func&& f, Args&&... args)
-> decltype(std::forward<Func>(f)(std::forward<Args>(args)...)) {
return std::forward<Func>(f)(std::forward<Args>(args)...);
}
规则和最佳实践
五法则(Rule of Five)
如果定义了以下任何一个,就应该定义全部五个:
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
class Resource {
private:
int* data;
size_t size;
public:
// 1. 构造函数
Resource(size_t n = 0) : size(n), data(n ? new int[n] : nullptr) {}
// 2. 析构函数
~Resource() {
delete[] data;
}
// 3. 拷贝构造函数
Resource(const Resource& other)
: size(other.size), data(other.size ? new int[other.size] : nullptr) {
std::copy(other.data, other.data + size, data);
}
// 4. 移动构造函数
Resource(Resource&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
// 5. 拷贝赋值运算符
Resource& operator=(const Resource& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = size ? new int[size] : nullptr;
std::copy(other.data, other.data + size, data);
}
return *this;
}
// 6. 移动赋值运算符
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};
零法则(Rule of Zero)
最佳实践:让编译器自动生成所有特殊成员函数:
#include <vector>
#include <string>
// 使用标准库容器,不需要手动定义任何特殊成员函数
class Person {
std::string name;
std::vector<int> scores;
public:
Person(const std::string& n) : name(n) {}
// 编译器自动生成:
// - 析构函数
// - 拷贝构造函数
// - 移动构造函数
// - 拷贝赋值运算符
// - 移动赋值运算符
// 都会正确工作!
};
移动语义的注意事项
// 1. 移动后的对象处于"有效但未指定"状态
std::string s1 = "hello";
std::string s2 = std::move(s1);
// s1 现在处于有效但未指定状态
// 可以安全地赋值、销毁,但不要假设其内容
s1 = "world"; // 正确
// 2. 不要移动后会自动复用的对象
void badExample() {
std::string str = "hello";
process(std::move(str)); // 移动 str
std::cout << str << std::endl; // 危险!str 可能是空的
}
// 3. 移动构造函数应该标记为 noexcept
class MyClass {
public:
MyClass(MyClass&& other) noexcept { // 重要!
// ...
}
};
// 这允许标准库容器在重分配时使用移动而非拷贝
// 4. const 对象不能被移动
const std::string cs = "hello";
std::string s = std::move(cs); // 实际上是拷贝!
// 因为 const 右值引用会退化为 const 左值引用
// 5. 成员变量的移动
class Container {
std::vector<int> data;
std::string name;
public:
Container(Container&& other) noexcept
: data(std::move(other.data)), // 移动成员
name(std::move(other.name)) {} // 移动成员
};
实际应用
高效的 swap 实现
// 通用的 swap 使用移动语义
template<typename T>
void mySwap(T& a, T& b) {
T temp = std::move(a); // 移动构造
a = std::move(b); // 移动赋值
b = std::move(temp); // 移动赋值
}
// std::swap 已经使用这种方式实现
高效的容器操作
#include <vector>
#include <algorithm>
void containerDemo() {
std::vector<std::string> vec;
// 移动插入而非拷贝插入
std::string s = "hello";
vec.push_back(std::move(s)); // s 的内容移动到 vector
// 原地构造(C++11)
vec.emplace_back("world"); // 直接在 vector 中构造
// 移动整个 vector
std::vector<std::string> vec2 = std::move(vec);
// vec 现在为空
}
工厂函数
template<typename T, typename... Args>
std::unique_ptr<T> create(Args&&... args) {
return std::make_unique<T>(std::forward<Args>(args)...);
}
// 使用
auto person = create<Person>("张三", 25);
小结
本章学习了:
- 值类别:左值和右值的区别
- 右值引用:
T&&和std::move - 移动语义:移动构造函数和移动赋值运算符
- 完美转发:
std::forward和引用折叠 - 五法则和零法则:何时需要定义特殊成员函数
- 最佳实践:正确使用移动语义的规则
下一步
接下来让我们学习 C++ Lambda 表达式,了解函数式编程特性。