理解直接初始化与复制初始化的区别

简介

当用于类类型对象时,初始化的复制形式和直接形式有所不同:

  • 直接初始化直接调用与实参匹配的构造函数。
  • 复制初始化则是首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。

很多时候编译器会默认给我们进行优化,省去创建临时对象这一步,以此来节省创建临时对象的损耗。

对比

下面的源代码来自于c++拷贝初始化和直接初始化的底层区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <cstring>
#include <iostream>
using namespace std;
class ClassTest {
public:
ClassTest() {
c[0] = '\0';
cout << "ClassTest()" << endl;
}
ClassTest& operator=(const ClassTest& ct) {
strcpy(c, ct.c);
cout << "ClassTest& operator=(const ClassTest &ct)" << endl;
return *this;
}
ClassTest(ClassTest&& ct) { cout << "ClassTest(ClassTest&& ct)" << endl; }
ClassTest& operator=(ClassTest&& ct) {
strcpy(c, ct.c);
cout << "ClassTest & operator=(ClassTest&& ct)" << endl;
return *this;
}
ClassTest(const char* pc) {
strcpy(c, pc);
cout << "ClassTest (const char *pc)" << endl;
}
ClassTest(const ClassTest& ct) {
strcpy(c, ct.c);
cout << "ClassTest(const ClassTest& ct)" << endl;
}
virtual int ff() { return 1; }

private:
char c[256];
};

ClassTest f1() {
ClassTest c;
return c;
}
void f2(ClassTest ct) { ; }
int main() {
ClassTest ct1("ab"); // 直接初始化
ClassTest ct2 = "ab"; // 复制初始化
ClassTest ct3 = ct1; // 复制初始化
ClassTest ct4(ct1); // 直接初始化
ClassTest ct5 = ClassTest("ab"); // 复制初始化
ClassTest ct6 = f1();
f1();
f2(ct1);
return 0;
}

ClassTest ct1(“ab”)

根据参数确定对应的构造函数 : ClassTest(const char* pc);

ClassTest ct2 = “ab”

该构造函数本应该为复制初始化,通过创建”ab”的临时对象,之后将临时对象复制到初始化对象中。

但是结果是显示调用了 ClassTest(const char* pc); 与上面的结果一致。

该结果应是执行了编译器的默认优化。

注意:对于编译器,即使启用了 -O0 来关闭优化,还是会进行一些必要的优化方式,如:减少不必要的复制。

ClassTest ct3 = ct1

该构造函数与第二个例子原理相同,区别是,第二个例子需要先构造一个临时对象,而该例子中已经存在一个对象,所以进行拷贝构造。

值得说明的是:如果该类中不存在复制初始化,编译器会默认生成一个复制初始化,该初始化方式为对应字段简单的拷贝。

ClassTest ct4(ct1)

根据参数确定对应的构造函数 : ClassTest(const ClassTest& ct);

ClassTest ct5 = ClassTest(“ab”)

出乎意料的是通过输出结果来看,该方式并没有临时对象的拷贝,该方式是直接将ct5的地址作为实参去调用构造函数。

ClassTest ct6 = f1()

在调用f1的时候,也传进了ct6对象的地址,在f1内部对c进行初始化后,直接通过c对象地址和ct6地址调用移动构造函数,对ct6进行了初始化,最后返回的是ct6对象地址。

f2(ct1)

一般来说,编译器对于一个非引用类型的变量,不是直接传入ct1对象地址,而是在栈上生成一个临时对象并且用拷贝构造函数进行初始化,最后再传入临时对象的地址调用f2函数。

总结

  • 复制初始化:将一个已有的对象(包括临时对象)拷贝到正在创建的对象(一般用=)。
  • 直接初始化:通过括号给对象提供一定的参数,根据参数匹配对应的构造函数。
  • 编译器的优化总结为尽可能的避免使用临时对象。而是选择在对象初始化时去选择对应的构造函数。