【C++】移动语义

warning: 这篇文章距离上次修改已过762天,其中的内容可能已经有所变动。

最近好好研究了一下 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 指针,导致 ab 都拥有了相同的大数据的指针,
最终 ab 析构时会二次执行 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。因为 badata 指针夺走了,adata 指针指向了 nullptr。因此 a 析构时实际上执行了 delete[] nullptr,没有效果。
b 则执行了 delete[] data,确确实实释放了 data 大数据,皆大欢喜。

移动赋值同理。

Error: std::move 并没有起到任何移动的效果,其仅仅只是将传入的对象强制转换为右值引用,以便触发类的移动构造和移动赋值函数,实际的“移动”操作都是在移动构造和移动赋值函数中进行的。

总结

因为 C++11 中新加入的移动语义,所以对于含有原始指针的类,除了添加自定义的拷贝函数之外,还要添加自定义的移动函数。如果暂时懒得写移动构造、赋值函数的话,干脆直接禁止移动(= delete),以免造成内存问题。

最后修改于:2022年03月29日 00:14

评论已关闭