右值引用和移动语义
前置
在 C++ 中,右值引用(Rvalue Reference) 和移动语义(Move Semantics) 是 C++11 引入的核心特性,旨在解决「临时对象拷贝的性能开销」和「资源转移」问题,同时完善值类别体系
一、前置
首先需明确 C++ 的「值类别」—— 所有表达式可分为左值(Lvalue) 和右值(Rvalue):
| 类型 | 特征 | 示例 |
|---|---|---|
| 左值 | 有名称、可取地址、持久存在 | 变量(int a)、this、返回左值引用的函数 |
| 右值 | 无名称、不可取地址、临时存在 | 字面量(10,“hello”)、临时对象(string(“abc”))、返回值的函数 |
二、右值引用(&&):绑定右值的引用
1.定义
右值引用是一种特殊的引用类型,语法为T&&,专门用于绑定右值(临时对象、字面量等),无法绑定左值(除非用std::move转换)。
#include <iostream>
#include <string>
using namespace std;
int main() {
// 右值引用绑定临时对象(右值)
string&& rref = string("hello");
cout << rref << endl; // 输出 hello
string s = "world";
// string&& rref2 = s; // 编译报错:不能绑定左值到右值引用
string&& rref2 = move(s); // 用std::move将左值转为右值引用(将亡值)
return 0;
}
2、核心特点
①右值引用的生命周期:绑定临时对象时,会延长临时对象的生命周期至右值引用自身销毁;
②std::move的作用:并非 “移动”,仅做「类型转换」—— 将左值强制转为右值引用(标记为 “可被移动”),对象本身未发生任何变化。
三、移动构造和移动赋值
1、使用 std::move 将左值转为右值后,是否需要手动编写移动构造 / 移动赋值函数,取决于你的类的特性和需求
| 类的场景 | 是否需要手动写移动构造 / 移动赋值 | 原因 |
|---|---|---|
| 仅含基本类型(int/float 等) | 含动态资源(堆内存 / 文件句柄) | 编译器自动生成的默认移动函数足够(移动等价于拷贝,无性能收益) |
| 含动态资源(堆内存 / 文件句柄) | 必须手动写 | 编译器默认移动函数是 “浅拷贝”,会导致资源重复释放、野指针等问题 |
| 依赖第三方不可拷贝资源(如 unique_ptr) | 建议手动写(或编译器自动生成) | 若类无自定义析构 / 拷贝函数,编译器会自动生成移动函数;否则需手动实现 |
2、核心原理:编译器的 “默认移动函数” 规则
C++11 起,编译器会为类自动生成默认的移动构造 / 移动赋值运算符,但满足以下全部条件时才会生成:
类没有自定义的拷贝构造函数;
类没有自定义的拷贝赋值运算符;
类没有自定义的析构函数;
类没有手动删除移动构造 / 移动赋值(如 MyClass(MyClass&&) = delete;)。
3、关键总结
std::move 只是 “触发器”:它仅将左值转为右值引用,但真正的资源转移依赖移动构造 / 移动赋值;
手动写移动函数的核心场景:类包含「动态分配的资源」(堆内存、文件句柄、网络连接等),此时编译器默认移动函数会导致资源冲突;
无需手动写的场景:类仅含基本类型 / 已实现移动语义的 STL 类型,编译器自动生成的移动函数足够安全;
避坑点:若类自定义了析构 / 拷贝构造 / 拷贝赋值,编译器会禁用默认移动函数,此时若用 std::move,会触发拷贝构造(而非移动),需手动实现移动函数。
四、移动构造和移动赋值实现
#include <iostream>
#include <cstring>
using namespace std;
class MyString {
public:
char* buf;
int len;
// 1. 默认构造函数(保留,但main中不调用)
MyString() : buf(nullptr), len(0) {
cout << "默认构造函数" << endl;
}
// 2. 带参构造函数
MyString(const char* str) {
cout << "带参构造函数" << endl;
len = strlen(str);
buf = new char[len + 1];
strcpy(buf, str);
}
// 3. 深拷贝构造
MyString(const MyString& other) {
cout << "深拷贝构造" << endl;
len = other.len;
buf = (other.buf == nullptr) ? nullptr : new char[len + 1];
if (buf != nullptr) {
strcpy(buf, other.buf);
}
}
// 4. 移动构造
MyString(MyString&& other) noexcept {
cout << "移动构造" << endl;
len = other.len;
buf = other.buf;
other.len = 0;
other.buf = nullptr;
}
// 5. 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
cout << "移动赋值" << endl;
if (this == &other) return *this;
delete[] buf; // 释放当前资源
len = other.len;
buf = other.buf;
other.len = 0;
other.buf = nullptr;
return *this;
}
// 6. 析构函数
~MyString() {
if (buf) {
delete[] buf;
buf = nullptr;
}
}
};
int main() {
// 删掉多余的s1(避免默认构造输出)
// MyString s1;
// 2. 带参构造
char str[] = "example";
cout<<"----------------1----------------"<<endl;
MyString s2(str);
cout<<"----------------2----------------"<<endl;
// 3. 深拷贝构造
MyString s3 = s2;
cout<<"----------------3----------------"<<endl;
4. 移动构造(禁用RVO后触发)
MyString s4 = getString();
cout<<"----------------4----------------"<<endl;
// 5. 移动赋值(先初始化s5,再赋值,触发operator=)
MyString s5; // 这里会调用默认构造(若想完全消除,可注释+调整)
cout<<"----------------5----------------"<<endl;
s5 = MyString("world"); // 临时对象→移动赋值(而非初始化)
cout<<"----------------6----------------"<<endl;
return 0;
}
注意:GCC/Clang 编译器需添加编译参数 -fno-elide-constructors(禁用返回值优化),否则移动构造会被编译器跳过。
更多推荐


所有评论(0)