【C++】移动语义
warning:
这篇文章距离上次修改已过968天,其中的内容可能已经有所变动。
最近好好研究了一下 C++ 中的移动语义。
左值 & 右值
已经在内存中分配了地址的、可以取到地址的,是左值。
在内存中未分配地址的、仅存在于寄存器中、无法取到地址的,是右值。
int a = 8; // a 已分配内存,是左值。
a + 1; // a + 1 仅存在于寄存器中,是右值。
右值引用
T&&
是右值引用,只接受右值:
int&& a = 8; // 正确,8 仅存在于寄存器中。
int v = 9;
int&& b = v; // 错误,v 是左值,无法存入右值引用。
int&& c = v + 3; // 正确,v + 3 的结果仅存在于寄存器中,是右值,可以存入右值引用。
T&&
虽然是右值引用,但它在内存中分配了地址,所以以上的 int&& a
int&& b
int&& c
均为左值。
info:宝可梦里有个笑话:喷火龙不是龙。现在 C++ 也有了,右值引用不是右值。
移动语义
拷贝构造和拷贝赋值时需要对对象进行深拷贝。
但在很多情况下只需要原样转移对象,而这时拷贝就会产生很多不必要的开销。
假设有如下 Foo
类:
class Foo {
public:
// 默认构造,Foo 中有很大的数据
Foo() {
data = new int[1919];
}
// 默认析构,释放掉大数据
~Foo() {
delete[] data;
}
// 拷贝构造,深拷贝大数据
Foo(const Foo& obj) {
data = new int[1919];
memcpy(data, obj.data, sizeof(int[1919]));
}
// 拷贝赋值,深拷贝大数据
Foo& operator=(const Foo& obj) {
memcpy(data, obj.data, sizeof(int[1919]));
}
private:
// 很大的数据
int* data;
}
目前情况:
Foo a = Foo(); // 假设 Foo 中包含很大的数据
Foo b; // 我们只需要把 a 原样移动到 b,而后不再使用 a。
Foo b = a; // 但是这里 b 执行了拷贝构造,大数据进行了一次不必要的拷贝。
C++11 引入了移动构造和移动赋值的概念:
Foo a = Foo(); // 假设 Foo 中包含很大的数据
Foo b; // 我们只需要把 a 原样移动到 b,而后不再使用 a。
Foo b = std::move(a);
std::move
并没有真正移动 a
,只是将 a
转换为了右值引用,从而调用了 b
的移动构造函数:
Foo::Foo(Foo&& obj);
但是 Foo
并没有自定义的移动构造函数,因此编译器生成了默认的移动构造函数:
- 对于成员中的基础类型,直接复制值。
- 对于成员中的自定类型,调用其移动构造函数。
所以实际上,b
执行了默认的移动构造,b
复制了 a
中的 data
指针,导致 a
和 b
都拥有了相同的大数据的指针,
最终 a
和 b
析构时会二次执行 delete[]
导致错误。
我们需要为 Foo
编写自定义的移动构造函数,将 a
中的 data
指针夺走,让 a
作废:
Foo(Foo&& obj){
data = obj.data; // 得到被移动对象的大数据指针。
obj.data = nullptr; // 清空被移动对象的大数据指针。
}
此时执行移动:
Foo a = Foo(); // 假设 Foo 中包含很大的数据
Foo b; // 我们只需要把 a 原样移动到 b,而后不再使用 a。
Foo b = std::move(a); // a 的数据原样移动到了 b 中,避免了深拷贝。并且 a 失去了数据,因此 a 作废。
在析构时,a
要执行 delete[] data
。因为 b
将 a
的 data
指针夺走了,a
的 data
指针指向了 nullptr
。因此 a
析构时实际上执行了 delete[] nullptr
,没有效果。b
则执行了 delete[] data
,确确实实释放了 data
大数据,皆大欢喜。
移动赋值同理。
Error:
std::move
并没有起到任何移动的效果,其仅仅只是将传入的对象强制转换为右值引用,以便触发类的移动构造和移动赋值函数,实际的“移动”操作都是在移动构造和移动赋值函数中进行的。总结
因为 C++11 中新加入的移动语义,所以对于含有原始指针的类,除了添加自定义的拷贝函数之外,还要添加自定义的移动函数。如果暂时懒得写移动构造、赋值函数的话,干脆直接禁止移动(= delete
),以免造成内存问题。
评论已关闭