一、引言
- 左值(lvalue):表示具名对象或内存中已有的资源,通常可以在表达式中多次使用。例如,一个变量
a
或数组元素等都属于左值。 -
右值(rvalue):通常指临时对象或表达式求值结果,它们没有持久化存储,往往在使用后即被销毁。例如,函数返回的临时对象或字面常量。
- 拷贝:在拷贝构造和拷贝赋值中,程序会分配新的内存并将源对象中的数据完整地复制到目标对象中。这样做确保了两个对象相互独立,但对于拥有动态资源(如堆内存、文件句柄、网络连接等)的对象,可能会造成性能开销或资源重复管理(需要深拷贝以防双重释放)。
- 移动:移动语义的引入(从 C++11 开始)目的在于减少不必要的数据拷贝,尤其是在处理临时对象时。移动操作不会创建数据副本,而是直接“窃取”源对象的内部资源,然后将源对象置于一个有效但未指定的状态。这样可以显著提高性能,尤其是在容器、智能指针等类中效果明显。
二、构造和赋值
1. 左值引用的拷贝构造和拷贝赋值
拷贝构造函数
-
原型:通常写为
Foo(const Foo& other);
使用常量引用主要有两方面原因:
- 避免在拷贝过程中修改源对象。
- 可以接受常量对象作为参数。
-
实现要点:
- 为目标对象分配新的内存或资源。
- 将源对象内的各个数据成员逐个拷贝(深拷贝或按值拷贝)。
- 如果类中有指针等资源,通常需要进行深拷贝以确保两个对象不共享同一资源,从而避免后续的资源释放问题。
拷贝赋值运算符
-
原型:通常写为
1
Foo& operator=(const Foo& other);
-
实现要点:
- 自我赋值检查(例如:
if (this == &other) return *this;
)。 - 释放目标对象当前占有的资源(如果有)。
- 分配新的资源或复制数据,使得目标对象与源对象内容相同。
- 返回当前对象的引用以支持链式赋值。
- 自我赋值检查(例如:
示例代码
class Foo {
private:
int* data;
public:
// 拷贝构造函数
Foo(const Foo& other) {
data = new int(*other.data); // 对动态分配内存的深拷贝
}
// 拷贝赋值运算符
Foo& operator=(const Foo& other) {
if (this != &other) { // 自我赋值检查
delete data; // 释放已有资源
data = new int(*other.data);
}
return *this;
}
~Foo() {
delete data;
}
};
2. 右值引用的移动构造和移动赋值
移动构造函数
-
原型:通常写为
1
Foo(Foo&& other) noexcept;
移动构造函数接受一个右值引用参数(
Foo&&
),表明这个参数可以“被窃取”。 -
实现要点:
- 直接获取源对象的资源指针,而不分配新的内存。
- 将源对象的资源指针置空或置于一个安全状态,防止析构时重复释放资源。
- 标记为
noexcept
能使容器在发生异常时更安全高效地转移资源。
移动赋值运算符
-
原型:通常写为
1
Foo& operator=(Foo&& other) noexcept;
-
实现要点:
- 同样需要进行自我赋值检查(尽管自我移动较为罕见,但有时依然需要)。
- 释放目标对象当前的资源。
- 将源对象的内部资源转移给目标对象。
- 将源对象置于有效状态(一般将内部指针置为
nullptr
)。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Foo {
private:
int* data;
public:
// 移动构造函数
Foo(Foo&& other) noexcept : data(other.data) {
other.data = nullptr; // 使 other 处于一个可析构状态
}
// 移动赋值运算符
Foo& operator=(Foo&& other) noexcept {
if (this != &other) { // 自我赋值检查
delete data; // 释放当前资源
data = other.data; // 接管资源
other.data = nullptr; // 重置 other
}
return *this;
}
~Foo() {
delete data;
}
};