C++11标准新增的右值引用和移动语义,解决了传统C++中临时对象拷贝带来的性能损耗问题,是现代C++开发必须掌握的核心特性。理解这两个特性,能帮助我们写出更高效、资源利用率更高的代码。

右值引用的基本概念
要理解右值引用,首先需要明确左值和右值的区别。左值是指可以出现在赋值号左边的表达式,通常有明确的名字和可取地址的属性;右值则是指临时对象、字面量等,不能出现在赋值号左边,也不具备持久的内存地址。
右值引用的语法是T&&,其中T是类型名,它可以绑定到右值,而不能绑定到左值。例如下面的代码:
#include <iostream>
using namespace std;
int main() {
int a = 10; // a是左值
int& lref = a; // 左值引用,绑定左值a
// int& lref2 = 10; // 错误,左值引用不能绑定右值10
int&& rref = 10; // 右值引用,绑定右值10
int&& rref2 = a + 5; // 绑定右值表达式a+5的结果
return 0;
}
需要注意的是,右值引用变量本身是一个左值,因为它有名字可以取地址,所以不能把右值引用变量直接绑定到其他右值引用上,除非用std::move将其转换为右值。
移动语义的作用
移动语义的核心思想是转移资源的所有权,而不是复制资源。比如当一个临时对象持有堆内存、文件句柄等资源时,传统的拷贝构造会复制一份资源,而移动构造只需要把临时对象的资源指针指向新的对象,再把临时对象的指针置为空,这样就避免了额外的资源分配和释放开销。
移动语义的实现依赖于右值引用,编译器会优先选择移动构造或移动赋值来处理右值参数,从而触发资源的转移。
移动构造与移动赋值的实现
以一个简单的自定义字符串类为例,我们来实现移动构造和移动赋值,对比和普通拷贝构造的区别。
基础字符串类定义
#include <iostream>
#include <cstring>
using namespace std;
class MyString {
private:
char* data;
size_t length;
public:
// 默认构造
MyString() : data(nullptr), length(0) {}
// 带参构造
MyString(const char* str) {
if (str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
} else {
data = nullptr;
length = 0;
}
}
// 拷贝构造(深拷贝)
MyString(const MyString& other) {
cout << "调用拷贝构造" << endl;
if (other.data) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
} else {
data = nullptr;
length = 0;
}
}
// 移动构造
MyString(MyString&& other) noexcept {
cout << "调用移动构造" << endl;
// 转移资源所有权
data = other.data;
length = other.length;
// 置空原对象资源,避免析构时释放
other.data = nullptr;
other.length = 0;
}
// 拷贝赋值运算符
MyString& operator=(const MyString& other) {
cout << "调用拷贝赋值" << endl;
if (this != &other) {
// 释放原有资源
delete[] data;
// 深拷贝新资源
if (other.data) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
} else {
data = nullptr;
length = 0;
}
}
return *this;
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
cout << "调用移动赋值" << endl;
if (this != &other) {
// 释放原有资源
delete[] data;
// 转移资源所有权
data = other.data;
length = other.length;
// 置空原对象资源
other.data = nullptr;
other.length = 0;
}
return *this;
}
// 析构函数
~MyString() {
delete[] data;
}
void print() const {
if (data) {
cout << data << endl;
} else {
cout << "空字符串" << endl;
}
}
};
移动语义效果验证
我们通过以下测试代码查看移动构造和移动赋值的效果:
int main() {
MyString str1("hello world");
cout << "str1: ";
str1.print();
// 拷贝构造,触发深拷贝
MyString str2 = str1;
cout << "str2: ";
str2.print();
// 移动构造,str1是左值,需要用move转为右值
MyString str3 = std::move(str1);
cout << "str3: ";
str3.print();
cout << "str1移动后: ";
str1.print();
// 移动赋值
MyString str4;
str4 = std::move(str3);
cout << "str4: ";
str4.print();
cout << "str3移动后: ";
str3.print();
return 0;
}
运行上述代码,输出结果如下:
str1: hello world 调用拷贝构造 str2: hello world 调用移动构造 str3: hello world str1移动后: 空字符串 调用移动赋值 str4: hello world str3移动后: 空字符串
可以看到,使用std::move将左值转换为右值后,编译器会优先调用移动构造和移动赋值,避免了堆内存的重复分配和复制,提升了性能。
移动语义的注意事项
- 移动构造和移动赋值通常需要标记为
noexcept,因为标准库容器在扩容时如果移动操作不保证不抛异常,会退化为使用拷贝操作,影响性能。 - 被移动后的对象处于有效但未指定的状态,不能再假设它持有原来的资源,通常建议只对其进行赋值或销毁操作。
- 不要对内置类型使用
std::move,因为内置类型的移动和拷贝效果是一致的,没有性能提升。
总结
右值引用是C++11引入的新引用类型,用于绑定右值;移动语义基于右值引用实现,通过转移资源所有权减少不必要的拷贝开销。移动构造和移动赋值是移动语义的具体体现,自定义类型实现这两个函数后,可以让临时对象的资源得到有效利用。在实际开发中,移动语义在容器元素插入、函数返回值传递等场景都能带来明显的性能提升,是优化C++程序的重要手段。