什么情况下 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
```
感谢您的阅读。本网站「地与码之间」对本文保留所有权利。