一、左值 vs 右值
-
左值
-
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
int a = 10; // a 是左值 int& b = a; // b 是左值引用 -
-
右值
-
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。(我们可以将右值分为两种,第一种是纯右值,比如常量之类的,第二种则是将亡值,特点是既有身份(可以取地址),又允许被移动,被移动是指其资源可以被安全地转移,而无需执行深拷贝,比如自定义类型)
42; // 字面量是右值 a + b; // 表达式结果是右值 int func() { return 1; } func(); // 函数返回的临时值是右值 -
二、右值引用
-
语法
右值引用用&&表示,只能绑定到右值。int&& rref = 42; // 正确:右值引用绑定到字面量 int&& rref2 = a + b; // 正确:绑定到表达式结果 -
核心目的
- 实现移动语义:允许“窃取”右值的资源,避免不必要的深拷贝。
- 完美转发(Perfect Forwarding):保持参数类型不变地传递(此处主要讨论移动语义)。
三、左值引用与右值引用比较
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
四、左值引用的短板
在C++中,左值引用(T&)和右值引用(T&&)各自有不同的设计目标和适用场景。左值引用虽然功能强大,但在某些场景下存在明显短板,而右值引用的引入正是为了解决这些问题。
1. 无法高效管理临时对象
- 问题:左值引用只能绑定到左值(有持久状态的对象),若要将临时对象(右值)传递给函数,必须使用
const T&(常量左值引用),但这会带来两个问题:- 无法修改对象:
const引用禁止对临时对象进行修改。 - 强制拷贝:如果函数需要持有对象(而非仅读取),则必须进行深拷贝,导致性能损耗。
- 无法修改对象:
- 示例:
void process(const std::string& str) { // 若需要存储 str,必须拷贝: std::string local_str = str; // 深拷贝,性能低 } process("Hello"); // 传递临时对象(右值)
2. 资源无法转移所有权
- 问题:左值引用只能访问对象,但无法安全地“窃取”其资源(如动态内存)。当需要将资源从一个对象转移到另一个对象时,必须进行深拷贝,效率低下。
- 示例:
class BigData { int* data; // 大量动态内存 public: // 拷贝构造函数(深拷贝) BigData(const BigData& other) { data = new int[1000000]; std::copy(other.data, other.data + 1000000, data); } }; BigData a; BigData b = a; // 深拷贝,耗时且内存占用翻倍
3. 无法支持完美转发
- 问题:在泛型编程中,模板函数需要将参数原封不动(保留左值/右值属性)传递给其他函数。左值引用无法区分传入的是左值还是右值,导致转发时可能丢失值类别信息。
- 示例:
template<typename T> void wrapper(T& arg) { func(arg); // 参数永远是左值,无法转发右值 } wrapper(42); // 错误:左值引用无法绑定右值
五、右值引用如何解决这些问题
1. 移动语义
- 核心思想:通过右值引用直接“窃取”临时对象或即将销毁对象的资源,避免深拷贝。
- 实现方式:
- 定义移动构造函数和移动赋值运算符,接受右值引用参数。
- 转移资源所有权,将源对象置于可安全析构的状态(如置空指针)。
- 示例:
class BigData { int* data; public: // 移动构造函数 BigData(BigData&& other) noexcept { data = other.data; // 直接接管资源 other.data = nullptr; // 置空源对象指针 } }; BigData a; BigData b = std::move(a); // 调用移动构造函数,无深拷贝
对于管理资源的类,传统的拷贝构造函数会进行深拷贝:
class String {
public:
char* data;
// 拷贝构造函数(深拷贝)
String(const String& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
};
当拷贝临时对象(右值)时,深拷贝是低效的,因为临时对象很快会被销毁。
移动构造函数通过“窃取”右值的资源,避免深拷贝:
class String {
public:
char* data;
// 移动构造函数
String(String&& other) noexcept {
data = other.data; // 直接接管资源
other.data = nullptr; // 置空原对象,避免重复释放
}
};
class BigData {
int* buffer; // 大量动态内存
public:
// 移动构造函数(接受将亡值)
BigData(BigData&& other) noexcept
: buffer(other.buffer) { // 直接接管资源
other.buffer = nullptr; // 置空原对象的资源
}
};
BigData a;
BigData b = std::move(a); // a 被转换为将亡值,触发移动构造函数
#include <iostream>
#include <vector>
class HeavyData {
public:
// 默认构造函数(分配资源)
HeavyData() {
data = new int[1000000]; // 模拟大型资源
std::cout << "Default Constructor\n";
}
// 移动构造函数
HeavyData(HeavyData&& other) noexcept {
data = other.data; // 窃取资源
other.data = nullptr; // 置空原对象资源
std::cout << "Move Constructor\n";
}
// 析构函数
~HeavyData() {
delete[] data;
std::cout << "Destructor\n";
}
private:
int* data = nullptr;
};
// 函数返回一个局部 HeavyData 对象(传值返回)
HeavyData createHeavyData() {
HeavyData localObj; // 局部对象
return localObj; // 返回值时触发移动构造(而非拷贝)!
}
int main() {
HeavyData obj = createHeavyData();
return 0;
}
- 关键点:
- 参数为右值引用
String&&。 - 直接“窃取”资源,不分配新内存。
- 将原对象的资源指针置空,确保析构安全。
- 参数为右值引用
当使用右值初始化对象时,优先调用移动构造函数:
String s1 = "Hello"; // 调用普通构造函数
String s2 = s1; // 调用拷贝构造函数(s1 是左值)
String s3 = std::move(s1); // 调用移动构造函数(std::move 将左值转为右值)
String s4 = String("World"); // 调用移动构造函数(临时对象是右值)
2、移动赋值运算符
类似地,移动赋值运算符用于“窃取”右值的资源:
class String {
public:
// 移动赋值运算符
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data; // 释放当前资源
data = other.data; // 接管新资源
other.data = nullptr;
}
return *this;
}
};
-
noexcept声明
移动构造函数/赋值运算符应标记为noexcept,以便标准库容器安全使用移动语义:String(String&& other) noexcept { ... }
六、std::move 的作用
- 功能:将左值强制转换为右值引用,表示该对象可以被“移动”。
- 示例:
String s1 = "Hello"; String s2 = std::move(s1); // s1 的资源被转移到 s2,s1 变为空 - 注意:
std::move本身不进行任何移动操作,只是类型转换。
七、移动语义的优势
-
性能提升
避免不必要的深拷贝,尤其适用于:- 容器操作(如
std::vector的push_back)。 - 大对象(如文件、网络连接)。
- 容器操作(如
-
资源所有权转移
在传递临时对象时,直接转移资源而非复制。
八、万能引用
1. 定义
万能引用又被成为引用折叠,如果传左值就是左值引用,传右值就是右值引用,但其成立需满足两个条件:
- 类型推导:
T必须是通过模板参数推导得到的类型。 - 引用折叠:通过引用折叠规则处理推导结果。
2. 语法示例
template<typename T>
void func(T&& arg) {
// arg 是万能引用,可以绑定到左值或右值
}
3. 类型推导规则
- 若传入 左值:
int x = 42; func(x); // T 推导为 int&,arg 类型折叠为 int& - 若传入 右值:
func(42); // T 推导为 int,arg 类型为 int&&
九、完美转发
1. 核心目标
将函数模板的参数 以原始值类别(左值/右值) 传递给其他函数,避免不必要的拷贝或类型丢失。
2. 实现工具
-
std::forward<T>:根据T的类型,将参数转换为左值或右值引用。template<typename T> void wrapper(T&& arg) { // 将 arg 按原始类型传递给其他函数 target(std::forward<T>(arg)); }
3. 工作原理
- 若
arg绑定到 左值:T推导为T&,std::forward<T>(arg)返回T&(左值)。
- 若
arg绑定到 右值:T推导为T,std::forward<T>(arg)返回T&&(右值)。
4. 对比 std::move
std::move无条件将参数转为右值。std::forward有条件地保持值类别。
十、注意事项
-
避免过度转发
仅在需要保留值类别时使用std::forward,否则直接传递参数即可。 -
万能引用与重载冲突
万能引用模板可能意外匹配非预期类型,需约束模板参数:template<typename T> auto func(T&&) -> std::enable_if_t<std::is_integral_v<T>> { ... } -
引用折叠的边界情况
明确理解T& &&折叠为T&,T&& &&折叠为T&&。
1675

被折叠的 条评论
为什么被折叠?



