移动语义
移动语义的设计初衷:
**移动语义的主要目标是避免不必要的对象复制,特别是那些拥有动态分配的资源(如堆内存)的对象。**之前在C++98/03中,我们仅仅有拷贝构造和拷贝赋值,这意味着任何时候对象被复制或赋值时,其资源都会被复制,这很可能导致效率问题。
如果能够直接使用源对象拥有的资源,可以节省资源申请和释放的时间。C++11新增加的移动语义就能够做到这一点。
实现移动语义要增加两个函数:移动构造函数和移动赋值函数。
这两个函数的设计是为了直接取得另一个对象的资源,而不是复制这些资源。这在涉及大量数据或其他昂贵操作时尤其有用,例如动态内存分配。
请看简单示例:
cpp
#include <iostream>
using namespace std;
class DynamicArray {
public:
size_t size_; // 数组大小
int* data_; // 指向动态分配的数组
public:
// 构造函数,分配资源
DynamicArray(size_t size) : size_(size), data_(new int[size]) {
cout << "Constructed [" << size << "]" << endl;
}
// 析构函数,释放资源
~DynamicArray() {
delete[] data_;
cout << "Destructed [" << size_ << "]" << endl;
}
// 拷贝构造函数(我们在此列出以说明它与移动构造函数的不同)
DynamicArray(const DynamicArray& other) : size_(other.size_), data_(new int[other.size_]) {
copy(other.data_, other.data_ + size_, data_);
cout << "Copied [" << size_ << "]" << endl;
}
// 移动构造函数
DynamicArray(DynamicArray&& other) noexcept : size_(other.size_), data_(other.data_) {
// 接管"other"对象的资源,并确保"other"对象处于可析构状态
other.data_ = nullptr; // 关键之处:使源对象不再指向堆内存
other.size_ = 0;
cout << "Moved [" << size_ << "]" << endl;
}
// 移动赋值函数
DynamicArray& operator=(DynamicArray&& other) noexcept {
if (this != &other) { // 防止自赋值
delete[] data_; // 释放当前对象的资源
// 接管"other"对象的资源
data_ = other.data_;
size_ = other.size_;
// 确保"other"对象处于可析构状态
other.data_ = nullptr;
other.size_ = 0;
cout << "Moved Assigned [" << size_ << "]" << endl;
}
return *this;
}
// 其他成员函数(如数据访问和操作函数)在此省略
};
int main() {
DynamicArray arr1(10); // 正常构造
DynamicArray arr2(move(arr1)); // 此处调用移动构造函数,'arr1' 不再拥有资源
DynamicArray arr3(5);
arr3 = move(arr2); // 此处调用移动赋值函数,'arr2' 不再拥有资源
// 注意:此时 'arr1' 和 'arr2' 不再拥有任何资源,它们是"空"的
// 'arr3' 现在拥有 'arr1' 的原始资源
return 0;
}
**移动语义是如何工作的呢?**移动语义通过引用右值引用(&&
)来实现,允许一个对象获取另一个即将销毁的对象的资源。
- 移动构造函数 :当我们用一个右值(临时对象或被
move
标记的对象)初始化新对象时,移动构造函数被调用。它会"窃取"这个右值的资源,然后将它置于一个可销毁的状态。通常,这涉及获取源对象的内部资源(例如指针),并将源对象的这些成员设为null或默认状态。 - 移动赋值运算符:类似于移动构造函数,当我们将一个右值赋给一个已存在的对象时,移动赋值运算符被调用。它也会窃取右值的资源,释放(或回收)左值对象原有的资源,并将右值对象置于可安全销毁的状态。
注意事项:
-
源对象状态:
-
在资源被移动之后,源对象仍然处于一个有效的状态,但其具体内容是未定义的。它必须能安全地析构,并且可以赋予新的值,但程序员不应该假设任何关于其当前值的信息。
-
不要试图使用一个已经移动过的对象的值,除非你已经明确地给它重新赋了值。
-
-
移动和拷贝的后备机制:
如果类未定义移动构造函数和移动赋值操作符,任何尝试移动对象的操作都会回退到使用拷贝构造函数和拷贝赋值操作符。这确保了代码的兼容性,即使在老的代码库或未经修改的类上也可以工作。但这也意味着,如果没有正确实现移动语义,程序将不会从中获益。
-
标准库的优化:
从 C++11 开始,标准库中的许多容器和算法都被优化以利用移动语义,特别是那些涉及到对象传递的操作。例如,当你插入一个对象到
vector
中,如果该对象是一个右值,那么移动构造函数就会被调用。这使得使用标准库的数据结构和算法更加高效。 -
移动语义的有效场景:
移动语义最有价值的场景是对于那些分配了堆内存或持有某种形式资源(文件句柄、网络连接等)的对象。对于基本类型(如
int
,float
)或不含动态分配资源的小对象,拷贝的代价很小,移动构造并不会带来太多优势,有时甚至可能是多余的。