对左右值,左右值引用,移动语义,移动构造函数,拷贝函数,完美转发的总结
2024年5月27日
对左右值,左右值引用,移动语义,移动构造函数,拷贝函数,完美转发的总结
左值,是一个容器,可被追踪(就是有名字)。在表达式结束后依然存在的对象(可以通过名字访问的变量)。
右值,是一个临时表达式、返回值,不可被追踪。只能在表达式中临时存在的值,不能通过名字再次引用,比如字面量(如 10)或表达式的结果、函数返回值。
左值引用(type&),另一个已存在的变量的别名,相当于快捷方式
右值引用(type&&),另一个临时常量、临时表达式的别名
如果说左值引用是另一个左值的快捷方式
那么右值引用就是另一个右值的快捷方式
但右值引用较左值引用是要特殊的,因为右值移动作为参数时,更像是移动,也就是触发移动构造或移动赋值操作,而不是进行代价较高的拷贝操作。
拷贝构造函数
使用左值引用,分深浅拷贝
浅拷贝是指仅将值拷贝,这将引发“两个对象指向一个内存区域”的问题,因为值拷贝的同时也将相同的地址给拷贝了
深拷贝则是将对象内存再复制一个,两个对象指向两个不同的内存区域,但内存区域是相同的内容
移动语义
将一个地方的资源移动到另一个地方的行为,就叫做移动语义
需要将右值引用作为参数来实现这一点
因为一旦一个地方将要被移动,它就会被降级为临时对象,而右值引用就是专门处理临时对象的
移动构造函数和移动赋值重载运算符是移动语义的具体提现
如果实现这两个方法,需要留意将形参 delete 掉
完美转发
指的是将函数重载,区分左值参数和右值参数的版本并分别进行特殊处理
然后用一个(模板)函数来决定形参究竟是左值参数还是右值参数,这往往是由 std::forward 方法实现
chatGPT补充:
左值和右值
左值(lvalue):可以取地址的对象,具有持久的内存地址。例如,变量x就是一个左值:
int x = 10;
int* p = &x; // p 是指向 x 的指针
右值(rvalue):临时对象或表达式的结果,不能取地址。例如,字面常量和表达式的结果都是右值:
int x = 3; // 3 是右值
int y = x + 5; // x + 5 是右值
int z = f(); // f的返回值是右值
左值引用和右值引用
左值引用(lvalue reference):
int a = 10;
int& ref = a; // ref 是 a 的左值引用
右值引用(rvalue reference):
int&& rref = 20; // rref 是右值 20 的右值引用
右值引用允许我们捕捉即将被销毁的临时对象,这使得“移动语义”成为可能。
拷贝构造函数
拷贝构造函数用于创建对象的副本:
class MyClass {
public:
int* data;
MyClass(int value) : data(new int(value)) {}
// 拷贝构造函数(浅拷贝)
MyClass(const MyClass& other) : data(other.data) {}
// 拷贝构造函数(深拷贝)
MyClass(const MyClass& other) : data(new int(*other.data)) {}
};
浅拷贝会导致多个对象指向同一个内存区域,而深拷贝会创建独立的副本。
移动语义
移动构造函数和移动赋值运算符:
class MyClass {
public:
int* data;
MyClass(int value) : data(new int(value)) {}
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr; // 将其他对象的指针置为nullptr,表示它不再拥有资源
}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete data; // 释放旧资源
data = other.data;
other.data = nullptr;
}
return *this;
}
~MyClass() {
delete data;
}
};
移动构造函数和移动赋值运算符的核心是将资源从一个对象“移动”到另一个对象,并确保原对象不再拥有这些资源。
完美转发
完美转发允许我们在模板函数中保留参数的左值或右值属性:
#include <utility>
void process(int& i) { /* 左值处理 */ }
void process(int&& i) { /* 右值处理 */ }
template<typename T>
void forward(T&& t) { // 注意这里需要是右值引用形参
process(std::forward<T>(t)); // 根据T类型决定调用左值或右值版本的process
}
通过使用std::forward,我们可以将模板参数完美地转发到目标函数,保持其左值或右值属性。
注:为什么代码
void process(int& i) { /* 左值处理 */ }
void process(int&& i) { /* 右值处理 */ }
是特化类型(指明类型,指明了是 int,但上层调用函数 forward 却是一个模板函数)?
也可以使用函数模板来保持一致
但在这种实现方式中,右值引用的参数被绑定到非常临时的右值引用,这可能不是你期望的结果
所以如果我们想要精确区分左值和右值,则更倾向于使用函数重载而不是模板函数重载

浙公网安备 33010602011771号