什么情况下 C++ 会调用复制构造、移动构造和赋值运算
2023年11月17日
编程
为什么要把复制构造、移动构造和赋值运算三个放在一起说呢?因为他们都和一个运算符 `=` 有关系。虽然写起来都是 `=` ,但是表示的含义可能完全不同。 但万变不离其宗, 复制构造、移动构造归根结底是构造函数,用于构造类的实例的。而赋值运算本质上是一个运算符,是操作类实例的。 # 复制构造 即使是最基础的C++教程,也会把复制构造函数作为类设计的重点之一。C++虽然会为类创建默认的复制构造函数,但是默认的复制构造函数只提供最基础的功能,也就是类成员间的值拷贝,所以遇到裸指针等掌握了独有资源的类来说就会非常危险了。此时就需要创建复制构造函数。 ```cpp class MyClass { MyClass(const MyClass& instance) {} } ``` 这里有一个要点:**参数类型必须是自身类型的常量引用**。 那么复制构造函数什么时候会被用到呢?有以下几种情况 一、通过类的另一个实例构造类实例 ```cpp MyClass a {}; MyClass b = a; // 调用复制构造函数 ``` 我们构造了一个类实例 `b` ,但是其初始值是 `a` ,此时调用复制构造函数。 二、通过值传递参数 ```cpp void my_function(MyClass obj); my_function(a); ``` `a` 在传递给 `obj` 时会被复制一份。 当然还有其他情况。但有一种情况是例外的,看似在复制构造,实际上可能并没有复制。 ```cpp MyClass create() { MyClass a; // 对 a 进行一些操作 return a; } ``` 貌似 `a` 返回后要被复制一份,但是由于编译器的优化,这里 `a` 直接在赋值位置被构造,避免了复制一次。 # 移动构造 移动构造主要是针对提升性能和右值引用设计的。关于左值引用和右值引用的讨论很多,细节也很多,不过大体上而言,对临时的、不能取地址的、没有名字的变量只能取右值引用,或者说,只能出现在等号右边不能出现在等号左边的就是右值。右值引用用 `&&` 表示。但是一个非临时的、能取地址的、有名字的变量,可以使用 `std::move()` 函数将其变为右值引用。 移动构造函数就是参数为**自身类型右值引用**的构造函数, ```cpp class MyClass { MyClass(MyClass&& instance) {} } ``` 注意不能是常量右值引用,因为新实例夺取了原实例的资源,所以往往需要对原实例进行一些修改,避免原实例再一次释放资源。 那么移动构造函数的使用场景是什么? 一、使用 `std::move()` 将原实例移动到新实例,从而避免复制 最常见的是智能指针 `unique_ptr<>` 的操作。由于该只能指针的所有权是唯一的,只能有一个实例控制该指针,所以只能进行移动操作。 二、函数参数使用右值引用以接受临时变量或避免拷贝 一种情况是操作容器类型时,例如 `vector<>`,可以使用 `emplace_back()` 插入元素,避免对象复制。 还有一种情况是做矩阵运算等复杂运算。比如有两个矩阵 `A` 和 `B` ,现在要求 `A` 左乘以 `B` 的转置,也就是 ```cpp A * t(B) ``` 这里的 `*` 一定是重载的乘法运算符,以支持矩阵运算。而 `t()` 函数返回的是一个临时变量,因此只能让重载的 `*` 运算符支持右值引用,从而实现上述表达式。不然就得将 `t(B)` 保存成 `Bt` 然后在计算 `A*Bt` 。这是非常不方便的。 然而如果经过编译器优化,如果函数参数传入的右值引用是构造函数构造的,那么往往移动构造函数都会避免。比如 ```cpp void print(MyClass&& instance); print(create()) ``` 虽然 `create()` 函数构造了一个对象,以右值引用的形式传入了 `print`,看似这里需要移动构造,但实际上并没有。因此,返回新创建的类对象的函数,如 `create()` ,无需将返回值类型设置为右值引用,这样反而会干扰编译器的优化。 由此可见,移动构造函数的使用场景还是比较有限的,一般只有用到 `unique_ptr<>` 智能指针和避免深拷贝提高性能的时候需要用到。 # 赋值运算 这个相对来说就简单多了,重载 `operator=` 即可。但是传入左值引用和右值引用的情况需要分别处理。往往如果是左值,需要执行类似于复制的逻辑。 ```cpp class MyClass { // 普通赋值运算符的重载 MyClass& operator=(const MyClass& other) { if (this != &other) { // 避免自我赋值 data = other.data; } return *this; } }; ``` 如果传入的是函数返回值,上述拷贝赋值依然是可以执行的。但是这就会多出一次复制的成本。如果要支持移动,需要单独设计移动逻辑。 ```cpp MyClass& operator=(MyClass&& other) noexcept { if (this != &other) { // 避免自我赋值 data = std::move(other.data); } return *this; } ``` 但是需要注意的是,只有当类对象构造好了之后才能进行赋值,否则执行的还是构造函数。 # 实例 下面是的类型设计了复制构造、移动构造、赋值运算。 ```cpp #include <iostream> using namespace std; class MyClass { public: static size_t Total; static const std::string Identifer; MyClass(size_t size): mSize(size), mId(Total++) { mpData = new int[mSize]; std::cout << "[MyClass] " << Identifer.data()[mId] << " construct\n"; } MyClass(const MyClass& instance) { mId = Total++; mSize = instance.mSize; mpData = new int[mSize]; memcpy(mpData, instance.mpData, mSize * sizeof(int)); std::cout << "[MyClass] " << Identifer.data()[mId] << " copy construct from " << Identifer.data()[instance.mId] << "\n"; } MyClass(MyClass&& instance) { mId = instance.mId; mSize = instance.mSize; mpData = instance.mpData; instance.mMoved = true; std::cout << "[MyClass] " << Identifer.data()[mId] << " move construct\n"; } ~MyClass() { if (!mMoved) delete[] mpData; mpData = nullptr; mSize = 0; Total--; if (mMoved) std::cout << "[MyClass] " << Identifer.data()[mId] << " destroy skiped\n"; else std::cout << "[MyClass] " << Identifer.data()[mId] << " destroyed\n"; } const int* data() { return mpData; } const size_t size() { return mSize; } void set(size_t i, int value) { mpData[i] = value; } MyClass& operator=(const MyClass& other) { std::cout << "[MyClass] " << Identifer.data()[mId] << " copy assigned from " << Identifer.data()[other.mId] << "\n"; if (this != &other) { delete[] mpData; mId = other.mId; mSize = other.mSize; mpData = new int[mSize]; memcpy(mpData, other.mpData, mSize * sizeof(int)); } return *this; } MyClass& operator=(MyClass&& other) { std::cout << "[MyClass] " << Identifer.data()[mId] << " move assigned from " << Identifer.data()[other.mId] << "\n"; if (this != &other) { delete[] mpData; mId = other.mId; mSize = other.mSize; mpData = other.mpData; other.mMoved = true; } return *this; } private: int mId; int* mpData; size_t mSize; bool mMoved = false; }; inline size_t MyClass::Total = 0; inline const std::string MyClass::Identifer = "abcdefghijklmnopqrstuvwxyz"; void print(MyClass obj) { cout << "[print] begin\n"; for (size_t i = 0; i < obj.size(); i++) { cout << (i == 0 ? "" : ", ") << obj.data()[i]; } cout << "\n"; cout << "[print] end\n"; } void print_ref(MyClass& obj) { cout << "[print] begin\n"; for (size_t i = 0; i < obj.size(); i++) { cout << (i == 0 ? "" : ", ") << obj.data()[i]; } cout << "\n"; cout << "[print] end\n"; } void print_move(MyClass&& obj) { cout << "[print] begin\n"; for (size_t i = 0; i < obj.size(); i++) { cout << (i == 0 ? "" : ", ") << obj.data()[i]; } cout << "\n"; cout << "[print] end\n"; } MyClass my_create(initializer_list<int> elements) { cout << "[my_create] begin\n"; MyClass c(elements.size()); auto cursor = elements.begin(); for (size_t i = 0; i < elements.size(); i++) { c.set(i, cursor[i]); } cout << "[my_create] end\n"; return c; } int main(int, char**){ MyClass a(10); MyClass b = a; print(b); print_ref(b); MyClass c = my_create({1, 2, 3}); print_ref(c); print_move(my_create({4, 5, 6})); cout << "[main] move construct\n"; MyClass c1(std::move(c)); print_ref(c1); cout << "[main] assign\n"; c1 = a; c1 = my_create({7, 8, 9}); cout << "[main] cleaning up\n"; } ``` 那么 `main()` 函数执行的时候,到底进行了多少次构造、多少次复制构造、多少次拷贝构造? 请看程序运行结果吧。 ```plaintext [MyClass] a construct [MyClass] b copy construct from a [MyClass] c copy construct from b [print] begin 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 [print] end [MyClass] c destroyed [print] begin 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 [print] end [my_create] begin [MyClass] c construct [my_create] end [print] begin 1, 2, 3 [print] end [my_create] begin [MyClass] d construct [my_create] end [print] begin 4, 5, 6 [print] end [MyClass] d destroyed [main] move construct [MyClass] c move construct [print] begin 1, 2, 3 [print] end [main] assign [MyClass] c copy assigned from a [my_create] begin [MyClass] d construct [my_create] end [MyClass] a move assigned from d [MyClass] d destroy skiped [main] cleaning up [MyClass] d destroyed [MyClass] c destroy skiped [MyClass] b destroyed [MyClass] a destroyed ```
感谢您的阅读。本网站 MyZone 对本文保留所有权利。