Default & Copy Constructor

如果一个类不包含任何构造函数,当需要时,编译器会自动合成一个,这句看似司空见惯的法则,其实和第一感觉不同。关键在于这个需要时,因为有时候其实编译器并不要专门为一个类合成构造函数。我们假设有一个类,其内部只有普通的内置类型成员,当有一个该类类型的局部对象时,其内部数据成员(如果有)的值保持随意就好,当有一个该类类型的全局对象时,其处于.bss段,直接默认清零即可,拷贝构造函数也是类似,将一个对象占有的空间的内容逐个字节拷贝即可,此时构造函数被称为trivial,即无用的。但是某些时候,当我们进行默认初始化,或者拷贝初始化时,如果我们没有显式定义构造函数和拷贝构造函数,此时编译器必须为我们合成,完成必需的工作,比如一个带有虚函数的类,编译器必须合成一个构造函数完成虚指针的设定,此时构造函数被称为nontrivial

只有nontrivial default constructor才会被编译器合成,本文讨论何时编译器会真正合成默认&拷贝构造函数。

我使用的编译环境:

1
2
3
4
5
6
7
> uname -a
Linux net 5.19.0-35-generic #36~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Feb 17 15:17:25 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
> g++ --version
g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

编译的优化级别一般是O0,反汇编工具objdump,由于c++

default constructor

如下测试程序,刚开始其成员均是内置类型,包括一个int类型成员i、float成员f和指针p:

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
class Foo
{
friend ostream &operator<<(ostream &os, const Foo &foo)
{
cout << foo.i << ' ' << foo.f << ' ' << foo.p;
return os;
}

private:
int i;
float f;
void *p;
};

Foo global;

int main()
{

Foo local;
cout << local << endl;
cout << global << endl;

return 0;
}

编译运行:

1
2
3
> g++ two.cc -g -o two && ./two
1631744200 4.58701e-41 0x7fde61426e88
0 0 0

局部变量的初始值是不确定的,而默认初始化的全局变量内容被清空为0即可,因为类中包含的只有数据成员(后续会讨论当类包含虚函数和虚基类时的情况)

查看.bss段,可见global确实在此处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> objdump -d -j .bss two

two: file format elf64-x86-64


Disassembly of section .bss:

0000000000004040 <_ZSt4cout@GLIBCXX_3.4>:
...

0000000000004150 <completed.0>:
...

0000000000004160 <global>:
...

0000000000004170 <_ZStL8__ioinit>:
...

为保持反汇编结果简洁,去掉cout输出,仅调用get_i():

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
class Foo
{
friend ostream &operator<<(ostream &os, const Foo &foo)
{
// cout << foo.i << ' ' << foo.f << ' ' << foo.p;
return os;
}

private:
int i;
float f;
void *p;
};

Foo global;

int main()
{

Foo local;
cout << local;
cout << global;

return 0;
}

查看反汇编结果的main函数部分:

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
> objdump -d -j .text two
... # 省略其余内容
0000000000001189 <main>:
1189: f3 0f 1e fa endbr64
118d: 55 push %rbp
118e: 48 89 e5 mov %rsp,%rbp
1191: 48 83 ec 20 sub $0x20,%rsp
1195: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
119c: 00 00
119e: 48 89 45 f8 mov %rax,-0x8(%rbp)
11a2: 31 c0 xor %eax,%eax
11a4: 48 8d 45 e0 lea -0x20(%rbp),%rax
11a8: 48 89 c6 mov %rax,%rsi
11ab: 48 8d 05 8e 2e 00 00 lea 0x2e8e(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
11b2: 48 89 c7 mov %rax,%rdi
11b5: e8 a3 00 00 00 call 125d <_ZlsRSoRK3Foo>
11ba: 48 8d 05 9f 2f 00 00 lea 0x2f9f(%rip),%rax # 4160 <global>
11c1: 48 89 c6 mov %rax,%rsi
11c4: 48 8d 05 75 2e 00 00 lea 0x2e75(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
11cb: 48 89 c7 mov %rax,%rdi
11ce: e8 8a 00 00 00 call 125d <_ZlsRSoRK3Foo>
11d3: b8 00 00 00 00 mov $0x0,%eax
11d8: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11dc: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11e3: 00 00
11e5: 74 05 je 11ec <main+0x63>
11e7: e8 94 fe ff ff call 1080 <__stack_chk_fail@plt>
11ec: c9 leave
11ed: c3 ret

可见,编译器“初始化”local的操作仅仅是下移了栈指针,分配内存,其内部数据成员的数值均为未确定,且为调用一个构造函数完成构造工作,此时,编译器并未和我们预期的那样,合成一个默认构造函数并调用之,此时构造函数是trivial的。

c++语法允许我们为class的数据成员加上默认值,此时会有constructor被合成吗?

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
class Foo
{
friend ostream &operator<<(ostream &os, const Foo &foo)
{
cout << foo.i << ' ' << foo.f << ' ' << foo.p;
return os;
}

private:
int i{12};
float f = 3.14;
void *p = (void *)0xffffffffffffffff;
};

Foo global;

int main()
{

Foo local;
cout << local << endl;
cout << global << endl;

return 0;
}

编译运行,和预期一样,此时global和local都会有默认值,:

1
2
3
4
> g++ two.cc -g -o two
> ./two
12 3.14 0xffffffffffffffff
12 3.14 0xffffffffffffffff

编译器是否合成了一个默认构造函数帮我们完成初始化操作?编译,反汇编查看main函数部分:

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
> objdump -d -j .text two
...
0000000000001229 <main>:
1229: f3 0f 1e fa endbr64
122d: 55 push %rbp
122e: 48 89 e5 mov %rsp,%rbp
1231: 48 83 ec 20 sub $0x20,%rsp
1235: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
123c: 00 00
123e: 48 89 45 f8 mov %rax,-0x8(%rbp)
1242: 31 c0 xor %eax,%eax
1244: 48 8d 45 e0 lea -0x20(%rbp),%rax # 获取local地址
1248: 48 89 c7 mov %rax,%rdi # 作为第一个参数
124b: e8 64 01 00 00 call 13b4 <_ZN3FooC1Ev> # 调用默认构造函数
1250: 48 8d 45 e0 lea -0x20(%rbp),%rax
1254: 48 89 c6 mov %rax,%rsi
1257: 48 8d 05 e2 2d 00 00 lea 0x2de2(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
125e: 48 89 c7 mov %rax,%rdi
1261: e8 d6 00 00 00 call 133c <_ZlsRSoRK3Foo>
1266: 48 8b 15 63 2d 00 00 mov 0x2d63(%rip),%rdx # 3fd0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GLIBCXX_3.4>
126d: 48 89 d6 mov %rdx,%rsi
1270: 48 89 c7 mov %rax,%rdi
1273: e8 78 fe ff ff call 10f0 <_ZNSolsEPFRSoS_E@plt>
1278: 48 8d 05 e1 2e 00 00 lea 0x2ee1(%rip),%rax # 4160 <global>
127f: 48 89 c6 mov %rax,%rsi
1282: 48 8d 05 b7 2d 00 00 lea 0x2db7(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
1289: 48 89 c7 mov %rax,%rdi
128c: e8 ab 00 00 00 call 133c <_ZlsRSoRK3Foo>
1291: 48 8b 15 38 2d 00 00 mov 0x2d38(%rip),%rdx # 3fd0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GLIBCXX_3.4>
1298: 48 89 d6 mov %rdx,%rsi
129b: 48 89 c7 mov %rax,%rdi
129e: e8 4d fe ff ff call 10f0 <_ZNSolsEPFRSoS_E@plt>
12a3: b8 00 00 00 00 mov $0x0,%eax
12a8: 48 8b 55 f8 mov -0x8(%rbp),%rdx
12ac: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
12b3: 00 00
12b5: 74 05 je 12bc <main+0x93>
12b7: e8 44 fe ff ff call 1100 <__stack_chk_fail@plt>
12bc: c9 leave
12bd: c3 ret

查看__static_initialization_and_destruction_0(int, int),其初始化全局变量

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
> objdump -d -j .text two
...
00000000000012be <_Z41__static_initialization_and_destruction_0ii>:
12be: f3 0f 1e fa endbr64
12c2: 55 push %rbp
12c3: 48 89 e5 mov %rsp,%rbp
12c6: 48 83 ec 10 sub $0x10,%rsp
12ca: 89 7d fc mov %edi,-0x4(%rbp)
12cd: 89 75 f8 mov %esi,-0x8(%rbp)
12d0: 83 7d fc 01 cmpl $0x1,-0x4(%rbp)
12d4: 75 4a jne 1320 <_Z41__static_initialization_and_destruction_0ii+0x62>
12d6: 81 7d f8 ff ff 00 00 cmpl $0xffff,-0x8(%rbp)
12dd: 75 41 jne 1320 <_Z41__static_initialization_and_destruction_0ii+0x62>
12df: 48 8d 05 8a 2e 00 00 lea 0x2e8a(%rip),%rax # 4170 <_ZStL8__ioinit>
12e6: 48 89 c7 mov %rax,%rdi
12e9: e8 32 fe ff ff call 1120 <_ZNSt8ios_base4InitC1Ev@plt>
12ee: 48 8d 05 13 2d 00 00 lea 0x2d13(%rip),%rax # 4008 <__dso_handle>
12f5: 48 89 c2 mov %rax,%rdx
12f8: 48 8d 05 71 2e 00 00 lea 0x2e71(%rip),%rax # 4170 <_ZStL8__ioinit>
12ff: 48 89 c6 mov %rax,%rsi
1302: 48 8b 05 ef 2c 00 00 mov 0x2cef(%rip),%rax # 3ff8 <_ZNSt8ios_base4InitD1Ev@GLIBCXX_3.4>
1309: 48 89 c7 mov %rax,%rdi
130c: e8 cf fd ff ff call 10e0 <__cxa_atexit@plt>
1311: 48 8d 05 48 2e 00 00 lea 0x2e48(%rip),%rax # 4160 <global>
1318: 48 89 c7 mov %rax,%rdi
131b: e8 94 00 00 00 call 13b4 <_ZN3FooC1Ev> # 调用默认构造函数
1320: 90 nop
1321: c9 leave
1322: c3 ret

出乎意料此时编译器真的替我们合成了一个默认构造函数(反汇编中名称为 <_ZN3FooC1Ev>),查看其内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00000000000013b4 <_ZN3FooC1Ev>:
13b4: f3 0f 1e fa endbr64
13b8: 55 push %rbp
13b9: 48 89 e5 mov %rsp,%rbp
13bc: 48 89 7d f8 mov %rdi,-0x8(%rbp) # 获取第一个参数,即对象地址
13c0: 48 8b 45 f8 mov -0x8(%rbp),%rax # 保存在rax中
13c4: c7 00 0c 00 00 00 movl $0xc,(%rax) # i = 0xc
13ca: 48 8b 45 f8 mov -0x8(%rbp),%rax
13ce: f3 0f 10 05 32 0c 00 movss 0xc32(%rip),%xmm0 # 2008 <_ZN6__pstl9execution2v1L5unseqE+0x1>
13d5: 00
13d6: f3 0f 11 40 04 movss %xmm0,0x4(%rax)
13db: 48 8b 45 f8 mov -0x8(%rbp),%rax
13df: 48 c7 40 08 ff ff ff movq $0xffffffffffffffff,0x8(%rax) # p = 0xff...
13e6: ff
13e7: 90 nop
13e8: 5d pop %rbp
13e9: c3 ret

该构造函数使用我们给定的值默认初始化成员。但是一般来说指针类型是内置类型,编译器并不需要为此合成一个默认构造函数。我猜测是因为指针的赋值使用了一个类型转换,将指针成员默认值改为nullptr,再次编译并反汇编查看main函数部分:

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
0000000000001229 <main>:
1229: f3 0f 1e fa endbr64
122d: 55 push %rbp
122e: 48 89 e5 mov %rsp,%rbp
1231: 48 83 ec 20 sub $0x20,%rsp
1235: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
123c: 00 00
123e: 48 89 45 f8 mov %rax,-0x8(%rbp)
1242: 31 c0 xor %eax,%eax
1244: c7 45 e0 0c 00 00 00 movl $0xc,-0x20(%rbp) # -10(%rbp)为local地址,此指令将0xc赋给i
124b: f3 0f 10 05 b5 0d 00 movss 0xdb5(%rip),%xmm0 # 2008 <_ZN6__pstl9execution2v1L5unseqE+0x1>
1252: 00
1253: f3 0f 11 45 e4 movss %xmm0,-0x1c(%rbp)
1258: 48 c7 45 e8 00 00 00 movq $0x0,-0x18(%rbp) # 将0x0赋给p
125f: 00
1260: 48 8d 45 e0 lea -0x20(%rbp),%rax
1264: 48 89 c6 mov %rax,%rsi
1267: 48 8d 05 d2 2d 00 00 lea 0x2dd2(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
126e: 48 89 c7 mov %rax,%rdi
1271: e8 c7 00 00 00 call 133d <_ZlsRSoRK3Foo>
1276: 48 8b 15 53 2d 00 00 mov 0x2d53(%rip),%rdx # 3fd0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GLIBCXX_3.4>
127d: 48 89 d6 mov %rdx,%rsi
1280: 48 89 c7 mov %rax,%rdi
1283: e8 68 fe ff ff call 10f0 <_ZNSolsEPFRSoS_E@plt>
1288: 48 8d 05 81 2d 00 00 lea 0x2d81(%rip),%rax # 4010 <global>
128f: 48 89 c6 mov %rax,%rsi
1292: 48 8d 05 a7 2d 00 00 lea 0x2da7(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
1299: 48 89 c7 mov %rax,%rdi
129c: e8 9c 00 00 00 call 133d <_ZlsRSoRK3Foo>
12a1: 48 8b 15 28 2d 00 00 mov 0x2d28(%rip),%rdx # 3fd0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GLIBCXX_3.4>
12a8: 48 89 d6 mov %rdx,%rsi
12ab: 48 89 c7 mov %rax,%rdi
12ae: e8 3d fe ff ff call 10f0 <_ZNSolsEPFRSoS_E@plt>
12b3: b8 00 00 00 00 mov $0x0,%eax
12b8: 48 8b 55 f8 mov -0x8(%rbp),%rdx
12bc: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
12c3: 00 00
12c5: 74 05 je 12cc <main+0xa3>
12c7: e8 34 fe ff ff call 1100 <__stack_chk_fail@plt>
12cc: c9 leave
12cd: c3 ret

查看.data段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> objdump -d -j .data two

two: file format elf64-x86-64


Disassembly of section .data:

0000000000004000 <__data_start>:
...

0000000000004008 <__dso_handle>:
4008: 08 40 00 00 00 00 00 00 .@......

0000000000004010 <global>:
4010: 0c 00 00 00 c3 f5 48 40 00 00 00 00 00 00 00 00 ......H@........

global的值被设为给定的默认值。

此时,编译器未合成一个默认构造函数,而是直接在初始化对象的地方直接通过指令设置成员的值。

若我们显式要求编译器合成一个默认构造函数:

1
2
public:
Foo()=default;

反汇编查看main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0000000000001229 <main>:
1229: f3 0f 1e fa endbr64
122d: 55 push %rbp
122e: 48 89 e5 mov %rsp,%rbp
1231: 48 83 ec 20 sub $0x20,%rsp
1235: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
123c: 00 00
123e: 48 89 45 f8 mov %rax,-0x8(%rbp)
1242: 31 c0 xor %eax,%eax
1244: c7 45 e0 0c 00 00 00 movl $0xc,-0x20(%rbp)
124b: f3 0f 10 05 b5 0d 00 movss 0xdb5(%rip),%xmm0 # 2008 <_ZN6__pstl9execution2v1L5unseqE+0x1>
1252: 00
1253: f3 0f 11 45 e4 movss %xmm0,-0x1c(%rbp)
1258: 48 c7 45 e8 00 00 00 movq $0x0,-0x18(%rbp)
125f: 00

其实compiler未替我们合成。但是如果我们自己提供一个空构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0000000000001229 <main>:
1229: f3 0f 1e fa endbr64
122d: 55 push %rbp
122e: 48 89 e5 mov %rsp,%rbp
1231: 48 83 ec 20 sub $0x20,%rsp
1235: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
123c: 00 00
123e: 48 89 45 f8 mov %rax,-0x8(%rbp)
1242: 31 c0 xor %eax,%eax
1244: 48 8d 45 e0 lea -0x20(%rbp),%rax
1248: 48 89 c7 mov %rax,%rdi
124b: e8 64 01 00 00 call 13b4 <_ZN3FooC1Ev>
1250: 48 8d 45 e0 lea -0x20(%rbp),%rax
1254: 48 89 c6 mov %rax,%rsi

还真的有一个默认构造函数。

待解决的疑问

  • 是否编译器合成的是inline类型的构造函数,此时构造函数直接在程序中被替换了,我还不知道inline类型的函数是否在程序中会有一个专门的函数体。但是我做了一个小测试,定义一个类内的inline函数,非常简短

    1
    2
    public:
    int get_i() const {return i;}

    它在程序中是有一个函数体的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    00000000000013c0 <_ZNK3Foo5get_iEv>:
    13c0: f3 0f 1e fa endbr64
    13c4: 55 push %rbp
    13c5: 48 89 e5 mov %rsp,%rbp
    13c8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
    13cc: 48 8b 45 f8 mov -0x8(%rbp),%rax
    13d0: 8b 00 mov (%rax),%eax
    13d2: 5d pop %rbp
    13d3: c3 ret

    所以此时我暂时认定书本上所说,不会合成构造函数

  • global对象在何时何处被初始化。在1__static_initialization_and_destruction中没有调用构造函数初始化global成员。

  • 编译器的行为其实和优化级别、编译器版本、编译器类型都有关系,上述输出结果都是在我的测试环境下表现出来的,当我调整优化级别为O2时,即使在自己定义的构造函数中有简单的语句,都会被编译器优化,而不会合成构造函数……

接下来讨论nontrivial default constructor的四种情况。

带有default constructor的member class object

如果一个class没有任何constructor,但是内含一个member object,后者具备一个default constructor,那么这个class的implicit default constructor就是nontrivial。且合成操作只有在真正需要的时候才会发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Foo
{
public:
Foo() {}
explicit Foo(int) {}
};

class Bar
{
public:
Foo foo{10};
char * str;
};

int main()
{
Bar bar;
cout << bar.str << endl;

return 0;
}

反汇编查看bar构造函数,调用了foo(int)构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
00000000000012b4 <_ZN3BarC1Ev>:
12b4: f3 0f 1e fa endbr64
12b8: 55 push %rbp
12b9: 48 89 e5 mov %rsp,%rbp
12bc: 48 83 ec 10 sub $0x10,%rsp
12c0: 48 89 7d f8 mov %rdi,-0x8(%rbp)
12c4: 48 8b 45 f8 mov -0x8(%rbp),%rax
12c8: be 0a 00 00 00 mov $0xa,%esi # 第二个参数为0xa,即给定的默认值
12cd: 48 89 c7 mov %rax,%rdi # 第一个参数是foo成员的地址
12d0: e8 cd ff ff ff call 12a2 <_ZN3FooC1Ei> # foo(int)
12d5: 90 nop
12d6: c9 leave
12d7: c3 ret

查看foo构造函数,只合成了foo(int),另一个没有使用到,未被合成:

1
2
3
4
5
6
7
8
9
00000000000012a2 <_ZN3FooC1Ei>:
12a2: f3 0f 1e fa endbr64
12a6: 55 push %rbp
12a7: 48 89 e5 mov %rsp,%rbp
12aa: 48 89 7d f8 mov %rdi,-0x8(%rbp)
12ae: 89 75 f4 mov %esi,-0xc(%rbp)
12b1: 90 nop
12b2: 5d pop %rbp
12b3: c3 ret

若main函数中添加一个默认初始化的局部变量,则默认构造函数会被合成:

1
2
3
4
5
6
7
8
int main()
{
Bar bar;
Foo f;
cout << bar.str << endl;

return 0;
}

查看反汇编结果,此时两个构造函数均被合成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
00000000000012ae <_ZN3FooC1Ev>:
12ae: f3 0f 1e fa endbr64
12b2: 55 push %rbp
12b3: 48 89 e5 mov %rsp,%rbp
12b6: 48 89 7d f8 mov %rdi,-0x8(%rbp)
12ba: 90 nop
12bb: 5d pop %rbp
12bc: c3 ret
12bd: 90 nop

00000000000012be <_ZN3FooC1Ei>:
12be: f3 0f 1e fa endbr64
12c2: 55 push %rbp
12c3: 48 89 e5 mov %rsp,%rbp
12c6: 48 89 7d f8 mov %rdi,-0x8(%rbp)
12ca: 89 75 f4 mov %esi,-0xc(%rbp)
12cd: 90 nop
12ce: 5d pop %rbp
12cf: c3 ret

让我们再把注意力放到Bar的合成的default constructor上,其仅调用了foo的对应的构造函数,而未给str安排一个初始值,运行结果如下:

1
2
> ./two
8�y

bar.str的值仍是“垃圾值”。即使我们显式让编译器替我们合成一个constructor,它的编译结果也是一样的:

1
2
3
4
5
6
7
class Bar
{
public:
Bar()=default;
Foo foo{10};
char * str;
};

编译运行:

1
2
> g++ two.cc -O0 -g -o two && ./two
8��

在这种情况下,编译器合成的默认构造函数的行动仅是:如果class A内涵一个或多个member class objects,则class A的每一个constructor必须调用每一个member classes的default constructor。编译器会扩张已经存在的constructors,安插一些代码,使得在user code执行前,先调用必要的default constructors。如果多个class member objects都要求constructor初始化操作,则以它们的生命次序调用各个constructors。我们使用如下程序测试:

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
class Dopey
{
public:
Dopey() {}
};

class Sneezy
{
public:
Sneezy() {}
};

class Bashful
{
public:
Bashful() {}
};

class Show_White
{
public:
Dopey dopey;
Sneezy sneezy;
Bashful bashful;
private:
int mumble;
};

int main()
{
Show_White sw;


return 0;
}

编译,再反汇编查看:

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
51
52
> g++ two.cc -O0 -g -o two && objdump -d -j .text two
...
000000000000123a <_ZN5DopeyC1Ev>:
123a: f3 0f 1e fa endbr64
123e: 55 push %rbp
123f: 48 89 e5 mov %rsp,%rbp
1242: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1246: 90 nop
1247: 5d pop %rbp
1248: c3 ret
1249: 90 nop

000000000000124a <_ZN6SneezyC1Ev>:
124a: f3 0f 1e fa endbr64
124e: 55 push %rbp
124f: 48 89 e5 mov %rsp,%rbp
1252: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1256: 90 nop
1257: 5d pop %rbp
1258: c3 ret
1259: 90 nop

000000000000125a <_ZN7BashfulC1Ev>:
125a: f3 0f 1e fa endbr64
125e: 55 push %rbp
125f: 48 89 e5 mov %rsp,%rbp
1262: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1266: 90 nop
1267: 5d pop %rbp
1268: c3 ret
1269: 90 nop

000000000000126a <_ZN10Show_WhiteC1Ev>:
126a: f3 0f 1e fa endbr64
126e: 55 push %rbp
126f: 48 89 e5 mov %rsp,%rbp
1272: 48 83 ec 10 sub $0x10,%rsp
1276: 48 89 7d f8 mov %rdi,-0x8(%rbp)
127a: 48 8b 45 f8 mov -0x8(%rbp),%rax
127e: 48 89 c7 mov %rax,%rdi
1281: e8 b4 ff ff ff call 123a <_ZN5DopeyC1Ev>
1286: 48 8b 45 f8 mov -0x8(%rbp),%rax
128a: 48 83 c0 01 add $0x1,%rax
128e: 48 89 c7 mov %rax,%rdi
1291: e8 b4 ff ff ff call 124a <_ZN6SneezyC1Ev>
1296: 48 8b 45 f8 mov -0x8(%rbp),%rax
129a: 48 83 c0 02 add $0x2,%rax
129e: 48 89 c7 mov %rax,%rdi
12a1: e8 b4 ff ff ff call 125a <_ZN7BashfulC1Ev>
12a6: 90 nop
12a7: c9 leave
12a8: c3 ret

编译器为Show_White类合成了默认构造函数,其按照声明顺序依次调用三个成员的默认构造函数。

注释掉Sneezy的默认构造函数,再次编译并反汇编:

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
> g++ two.cc -O0 -g -o two && objdump -d -j .text two
...
000000000000123a <_ZN5DopeyC1Ev>:
123a: f3 0f 1e fa endbr64
123e: 55 push %rbp
123f: 48 89 e5 mov %rsp,%rbp
1242: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1246: 90 nop
1247: 5d pop %rbp
1248: c3 ret
1249: 90 nop

000000000000124a <_ZN7BashfulC1Ev>:
124a: f3 0f 1e fa endbr64
124e: 55 push %rbp
124f: 48 89 e5 mov %rsp,%rbp
1252: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1256: 90 nop
1257: 5d pop %rbp
1258: c3 ret
1259: 90 nop

000000000000125a <_ZN10Show_WhiteC1Ev>:
125a: f3 0f 1e fa endbr64
125e: 55 push %rbp
125f: 48 89 e5 mov %rsp,%rbp
1262: 48 83 ec 10 sub $0x10,%rsp
1266: 48 89 7d f8 mov %rdi,-0x8(%rbp)
126a: 48 8b 45 f8 mov -0x8(%rbp),%rax
126e: 48 89 c7 mov %rax,%rdi
1271: e8 c4 ff ff ff call 123a <_ZN5DopeyC1Ev>
1276: 48 8b 45 f8 mov -0x8(%rbp),%rax
127a: 48 83 c0 02 add $0x2,%rax
127e: 48 89 c7 mov %rax,%rdi
1281: e8 c4 ff ff ff call 124a <_ZN7BashfulC1Ev>
1286: 90 nop
1287: c9 leave
1288: c3 ret

Sneezy类的默认构造函数未被合成(trivial的,编译器也不会为我们合成,即使我们显示要求),而Show_White类的默认构造函数仅调用两个成员的默认构造函数,不会负责为mumble指定一个初始值。

替Show_White添加一个成员函数,获取mumble的值,在main中打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Show_White
{
public:
Dopey dopey;
Sneezy sneezy;
Bashful bashful;
int get_mumble() const
{
return mumble;
}
private:
int mumble;
};

int main()
{
Show_White sw;
cout << sw.get_mumble() << endl;

return 0;
}

编译,运行两次:

1
2
3
> g++ two.cc -O0 -g -o two && ./two && ./two 
32729
32646

mumble的值是未定义的。

修改测试程序如下,查看构造函数调用成员的构造函数的顺序是否符合声明顺序:

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
...
class Sneezy
{
public:
Sneezy() {}
Sneezy(int) {}
};
...
class Show_White
{
public:
Dopey dopey;
int mumble{2048};
Sneezy sneezy;
Bashful bashful;
Show_White() : sneezy(10)
{
}
int get_mumble() const
{
return mumble;
}
};
int main()
{
Show_White sw;
cout << sw.mumble << endl;

return 0;
}

我们为mumble指定了初始值,并且特定将其位置调整到了其他类成员之间,编译并反汇编查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
00000000000012d2 <_ZN10Show_WhiteC1Ev>:
12d2: f3 0f 1e fa endbr64
12d6: 55 push %rbp
12d7: 48 89 e5 mov %rsp,%rbp
12da: 48 83 ec 10 sub $0x10,%rsp
12de: 48 89 7d f8 mov %rdi,-0x8(%rbp) # %rdi中是Show_White对象的地址
12e2: 48 8b 45 f8 mov -0x8(%rbp),%rax
12e6: 48 89 c7 mov %rax,%rdi
12e9: e8 b2 ff ff ff call 12a0 <_ZN5DopeyC1Ev> # %rdi同时也是第一个成员的地址,调用Dopey的默认构造函数
12ee: 48 8b 45 f8 mov -0x8(%rbp),%rax
12f2: c7 40 04 00 08 00 00 movl $0x800,0x4(%rax) # %rax+0x4是mumble的地址,初始值为0x800
12f9: 48 8b 45 f8 mov -0x8(%rbp),%rax
12fd: 48 83 c0 08 add $0x8,%rax # %rax+0x8为sneezy成员地址
1301: be 0a 00 00 00 mov $0xa,%esi
1306: 48 89 c7 mov %rax,%rdi
1309: e8 a2 ff ff ff call 12b0 <_ZN6SneezyC1Ei> # 调用sneezy(int)
130e: 48 8b 45 f8 mov -0x8(%rbp),%rax
1312: 48 83 c0 09 add $0x9,%rax # %rax+0x9为bashful成员地址
1316: 48 89 c7 mov %rax,%rdi
1319: e8 a4 ff ff ff call 12c2 <_ZN7BashfulC1Ev> # 调用Bashful默认构造函数
131e: 90 nop
131f: c9 leave
1320: c3 ret

显然,结果和预期完全吻合。

“带有default constructor”的Base class

如果一个不包含任何constructors的class派生自一个“带有default constructor”的base class,则derived class的default constructor会被视为nontrivial,并因此需要被合成出来,负责调用base class的default constructor(按照它们的声明次序)。若该class同时亦存在着“带有default constructors”的member class objects,则那些default constructors在所有base class constructors后被调用。

修改上一个测试程序,Show_White类新增两个带有default constructor的父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base1
{
public:
Base1() {}
};

class Base2
{
public:
Base2() {}
};
...

class Show_White : public Base1, public Base2
{
public:
Dopey dopey;
int mumble{2048};
Sneezy sneezy;
Bashful bashful;
};

编译并反汇编(省略了Show_White类之外的类的default constructor的函数体,因为它们do nothing):

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
00000000000012a0 <_ZN5Base1C1Ev>:
...

00000000000012b0 <_ZN5Base2C1Ev>:
...

00000000000012c0 <_ZN5DopeyC1Ev>:
...

00000000000012d0 <_ZN6SneezyC1Ev>:
...

00000000000012e0 <_ZN7BashfulC1Ev>:
...

00000000000012f0 <_ZN10Show_WhiteC1Ev>:
12f0: f3 0f 1e fa endbr64
12f4: 55 push %rbp
12f5: 48 89 e5 mov %rsp,%rbp
12f8: 48 83 ec 10 sub $0x10,%rsp
12fc: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1300: 48 8b 45 f8 mov -0x8(%rbp),%rax
1304: 48 89 c7 mov %rax,%rdi
1307: e8 94 ff ff ff call 12a0 <_ZN5Base1C1Ev> # call Base1()
130c: 48 8b 45 f8 mov -0x8(%rbp),%rax
1310: 48 89 c7 mov %rax,%rdi
1313: e8 98 ff ff ff call 12b0 <_ZN5Base2C1Ev> # call Base2()
1318: 48 8b 45 f8 mov -0x8(%rbp),%rax
131c: 48 89 c7 mov %rax,%rdi
131f: e8 9c ff ff ff call 12c0 <_ZN5DopeyC1Ev> # call Dopey()
1324: 48 8b 45 f8 mov -0x8(%rbp),%rax
1328: c7 40 04 00 08 00 00 movl $0x800,0x4(%rax) # mumble = 0x800
132f: 48 8b 45 f8 mov -0x8(%rbp),%rax
1333: 48 83 c0 08 add $0x8,%rax
1337: 48 89 c7 mov %rax,%rdi
133a: e8 91 ff ff ff call 12d0 <_ZN6SneezyC1Ev> # call Sneezy()
133f: 48 8b 45 f8 mov -0x8(%rbp),%rax
1343: 48 83 c0 09 add $0x9,%rax
1347: 48 89 c7 mov %rax,%rdi
134a: e8 91 ff ff ff call 12e0 <_ZN7BashfulC1Ev> # call Bashful()
134f: 90 nop
1350: c9 leave
1351: c3 ret

可见,Show_White的默认构造函数完成的工作与预期完全吻合。

“带有virtual function”的class

当类带有虚函数时,其对象内部必须保存关于多态的信息,这些信息一般存放在虚表之中。每个对象中会有一个虚指针指向虚表,因此,即使一个什么都不做的构造函数,编译器也在其中安插了设置虚指针的代码。总的来说,编译期间会发生如下扩张操作:

  • 一个virtual function table(vtbl)会被编译器产生出来,其中包含有虚函数的地址
  • 每个class object中,一个额外的pointer member(vptr)会被合成,内含class vtbl的地址

修改上述的测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
class Show_White
{
public:
// Show_White()=default;
virtual void vfunc1() const
{
cout << "vfunc1" << endl;
}
private:
virtual void vfunc2() const
{}
};

编译并反汇编,查看合成的构造函数内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> g++ two.cc -O0 -g -o two && objdump -d two
...
00000000000011c9 <main>:
11c9: f3 0f 1e fa endbr64
11cd: 55 push %rbp
11ce: 48 89 e5 mov %rsp,%rbp
11d1: 48 83 ec 10 sub $0x10,%rsp
11d5: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
11dc: 00 00
11de: 48 89 45 f8 mov %rax,-0x8(%rbp)
11e2: 31 c0 xor %eax,%eax
11e4: 48 8d 05 7d 2b 00 00 lea 0x2b7d(%rip),%rax # 3d68 <_ZTV10Show_White+0x10>
11eb: 48 89 45 f0 mov %rax,-0x10(%rbp)
11ef: 48 8d 45 f0 lea -0x10(%rbp),%rax
11f3: 48 89 c7 mov %rax,%rdi
11f6: e8 8b 00 00 00 call 1286 <_ZNK10Show_White6vfunc1Ev>
11fb: b8 00 00 00 00 mov $0x0,%eax
1200: 48 8b 55 f8 mov -0x8(%rbp),%rdx
1204: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
120b: 00 00
120d: 74 05 je 1214 <main+0x4b>
120f: e8 ac fe ff ff call 10c0 <__stack_chk_fail@plt>
1214: c9 leave
1215: c3 ret

预期编译器将合成一个Show_White的default constructor,其完成虚指针的设定。但是main函数中并未调用show_white的默认构造函数,且也没有为其合成,而是直接将虚表起始地址加上0x10的偏移赋给了对象内部的虚指针。或者说此默认构造函数是inline的。准确来说,虚表的合成是编译期间完成的,而虚指针的设定是通过安插代码在执行期间完成的。

修改测试程序,使其继续包含基类和类成员对象,此时猜测编译器必然合成default constructor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
class Show_White : public Base1, public Base2
{
public:
Dopey dopey;
int mumble{2048};
Sneezy sneezy;
Bashful bashful;

virtual void vfunc1() const
{
cout << "vfunc1" << endl;
}
private:
virtual void vfunc2() const
{}
};

编译并反汇编查看结果:

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
000000000000135e <_ZN10Show_WhiteC1Ev>:
135e: f3 0f 1e fa endbr64
1362: 55 push %rbp
1363: 48 89 e5 mov %rsp,%rbp
1366: 48 83 ec 10 sub $0x10,%rsp
136a: 48 89 7d f8 mov %rdi,-0x8(%rbp)
136e: 48 8b 45 f8 mov -0x8(%rbp),%rax
1372: 48 89 c7 mov %rax,%rdi
1375: e8 46 ff ff ff call 12c0 <_ZN5Base1C1Ev>
137a: 48 8b 45 f8 mov -0x8(%rbp),%rax
137e: 48 89 c7 mov %rax,%rdi
1381: e8 4a ff ff ff call 12d0 <_ZN5Base2C1Ev>
1386: 48 8d 15 8b 29 00 00 lea 0x298b(%rip),%rdx # 3d18 <_ZTV10Show_White+0x10>
138d: 48 8b 45 f8 mov -0x8(%rbp),%rax
1391: 48 89 10 mov %rdx,(%rax)
1394: 48 8b 45 f8 mov -0x8(%rbp),%rax
1398: 48 83 c0 08 add $0x8,%rax
139c: 48 89 c7 mov %rax,%rdi
139f: e8 3c ff ff ff call 12e0 <_ZN5DopeyC1Ev>
13a4: 48 8b 45 f8 mov -0x8(%rbp),%rax
13a8: c7 40 0c 00 08 00 00 movl $0x800,0xc(%rax)
13af: 48 8b 45 f8 mov -0x8(%rbp),%rax
13b3: 48 83 c0 10 add $0x10,%rax
13b7: 48 89 c7 mov %rax,%rdi
13ba: e8 31 ff ff ff call 12f0 <_ZN6SneezyC1Ev>
13bf: 48 8b 45 f8 mov -0x8(%rbp),%rax
13c3: 48 83 c0 11 add $0x11,%rax
13c7: 48 89 c7 mov %rax,%rdi
13ca: e8 31 ff ff ff call 1300 <_ZN7BashfulC1Ev>
13cf: 90 nop
13d0: c9 leave
13d1: c3 ret

编译器合成了default constructor,其先后完成了Base1默认构造、Base2默认构造、设置虚指针、按照声明次序初始化成员。

“带有virtual base class”的class

测试程序:

1
2
3
4
5
6
7
8
9
10
class Base1
{
public:
Base1() {}
// virtual ~Base1() {}
};

class Show_White : virtual public Base1
{
};

编译并反编译查看main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0000000000001189 <main>:
1189: f3 0f 1e fa endbr64
118d: 55 push %rbp
118e: 48 89 e5 mov %rsp,%rbp
1191: 48 83 ec 10 sub $0x10,%rsp
1195: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
119c: 00 00
119e: 48 89 45 f8 mov %rax,-0x8(%rbp)
11a2: 31 c0 xor %eax,%eax
11a4: 48 8d 45 f0 lea -0x10(%rbp),%rax
11a8: 48 89 c7 mov %rax,%rdi
11ab: e8 9a 00 00 00 call 124a <_ZN10Show_WhiteC1Ev>
11b0: b8 00 00 00 00 mov $0x0,%eax
11b5: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11b9: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11c0: 00 00
11c2: 74 05 je 11c9 <main+0x40>
11c4: e8 b7 fe ff ff call 1080 <__stack_chk_fail@plt>
11c9: c9 leave
11ca: c3 ret

可见编译器确实合成了默认构造函数,查看其内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
000000000000124a <_ZN10Show_WhiteC1Ev>:
124a: f3 0f 1e fa endbr64
124e: 55 push %rbp
124f: 48 89 e5 mov %rsp,%rbp
1252: 48 83 ec 10 sub $0x10,%rsp
1256: 48 89 7d f8 mov %rdi,-0x8(%rbp)
125a: 48 8b 45 f8 mov -0x8(%rbp),%rax
125e: 48 89 c7 mov %rax,%rdi
1261: e8 d4 ff ff ff call 123a <_ZN5Base1C1Ev>
1266: 48 8d 15 f3 2a 00 00 lea 0x2af3(%rip),%rdx # 3d60 <_ZTT10Show_White>
126d: 48 8b 45 f8 mov -0x8(%rbp),%rax
1271: 48 89 10 mov %rdx,(%rax)
1274: 90 nop
1275: c9 leave
1276: c3 ret

首先调用了基类的构造函数,并且设置了对象起始处的虚指针为 <_ZTT10Show_White>,可以通过c++filt查看其名称:

1
2
> c++filt _ZTT10Show_White
VTT for Show_White

Note:c++涉及虚继承的部分比较复杂,尤其是当虚继承、虚函数、多重继承均涉及到的时候,关于虚继承的部分后续blog会再讨论,目前只需要先直到此种情况编译器会合成默认构造函数,其至少需要完成设置虚指针的工作。

copy constructor

default memberwise initialization

如果class没有提供一个explicit copy constructor,当进行拷贝初始化时,其内部是以所谓的default member wise initialization手法完成的。即把每一个内建的或派生的data member的值,从某个object拷贝一份到另一个object身上,但并不会拷贝其中的member class object,而是以递归的方式施行member wise initialization。

copy constructor分为trivial和nontrivial,后者会被合成于程序中,决定一个copyconstructor是否为trivial的标准为,是否class展现出所谓的bitwise copy semantics

bitwise copy semantics

类的初始化和拷贝是逐个成员进行的,即member wise。

当类具备如下特征时,被认为不具备bitwise copy semantics,此时编译器必须为不包含任何构造函数定义的类合成默认构造函数,以及拷贝构造函数。

  • 某个类成员具备默认构造函数(不管是编译器合成的还是显式定义的)
  • 类的基类具备拷贝构造函数(不管是编译器合成的还是显式定义的)
  • 包含虚函数(虚函数可以继承,即使某个类未定义自己的虚函数,但是可以从其他类继承虚函数)
  • 包含虚基类

逐一验证之。

1 内含member object 具有copy constructor

编写如下测试程序:

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
class Word
{
public:
Word() = default;
Word(const Word &w) = default;
~Word() {}

private:
char *str;
int len;
};

class String
{
public:
String() {}
String(const String &) = default;

private:
Word word;
int len;
};

int main()
{
String s1;
String s2 = s1;

return 0;
}

编译并反编译查看main函数:

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
0000000000001189 <main>:
1189: f3 0f 1e fa endbr64
118d: 55 push %rbp
118e: 48 89 e5 mov %rsp,%rbp
1191: 53 push %rbx
1192: 48 83 ec 48 sub $0x48,%rsp
1196: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
119d: 00 00
119f: 48 89 45 e8 mov %rax,-0x18(%rbp)
11a3: 31 c0 xor %eax,%eax
11a5: 48 8d 45 b0 lea -0x50(%rbp),%rax
11a9: 48 89 c7 mov %rax,%rdi
11ac: e8 d1 00 00 00 call 1282 <_ZN6StringC1Ev>
11b1: 48 8b 45 b0 mov -0x50(%rbp),%rax
11b5: 48 8b 55 b8 mov -0x48(%rbp),%rdx
11b9: 48 89 45 d0 mov %rax,-0x30(%rbp)
11bd: 48 89 55 d8 mov %rdx,-0x28(%rbp)
11c1: 48 8b 45 c0 mov -0x40(%rbp),%rax
11c5: 48 89 45 e0 mov %rax,-0x20(%rbp)
11c9: bb 00 00 00 00 mov $0x0,%ebx
11ce: 48 8d 45 d0 lea -0x30(%rbp),%rax
11d2: 48 89 c7 mov %rax,%rdi
11d5: e8 b8 00 00 00 call 1292 <_ZN6StringD1Ev>
11da: 48 8d 45 b0 lea -0x50(%rbp),%rax
11de: 48 89 c7 mov %rax,%rdi
11e1: e8 ac 00 00 00 call 1292 <_ZN6StringD1Ev>
11e6: 89 d8 mov %ebx,%eax
11e8: 48 8b 55 e8 mov -0x18(%rbp),%rdx
11ec: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11f3: 00 00
11f5: 74 05 je 11fc <main+0x73>
11f7: e8 84 fe ff ff call 1080 <__stack_chk_fail@plt>
11fc: 48 8b 5d f8 mov -0x8(%rbp),%rbx
1200: c9 leave
1201: c3 ret

可见,并未合成和调用拷贝构造函数,拷贝构造的过程仅是通过mov指令搬运内存空间的内容。

如下是修改后的测试程序,为Word类显式定义了一个拷贝构造函数:

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
class Word
{
public:
Word() = default;
Word(const Word &w)
: str(w.str), len(w.len)
{
}

~Word() {}

private:
char *str;
int len;
};

class String
{
public:
String() {}
String(const String &) = default;

private:
Word word;
int len;
};

int main()
{
String s1;
String s2 = s1;

return 0;
}

编译并反汇编,查看.text段的结果:

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
0000000000001189 <main>:
1189: f3 0f 1e fa endbr64
118d: 55 push %rbp
118e: 48 89 e5 mov %rsp,%rbp
1191: 53 push %rbx
1192: 48 83 ec 48 sub $0x48,%rsp
1196: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
119d: 00 00
119f: 48 89 45 e8 mov %rax,-0x18(%rbp)
11a3: 31 c0 xor %eax,%eax
11a5: 48 8d 45 b0 lea -0x50(%rbp),%rax
11a9: 48 89 c7 mov %rax,%rdi
11ac: e8 fb 00 00 00 call 12ac <_ZN6StringC1Ev>
11b1: 48 8d 55 b0 lea -0x50(%rbp),%rdx
11b5: 48 8d 45 d0 lea -0x30(%rbp),%rax
11b9: 48 89 d6 mov %rdx,%rsi
11bc: 48 89 c7 mov %rax,%rdi
11bf: e8 18 01 00 00 call 12dc <_ZN6StringC1ERKS_>
11c4: bb 00 00 00 00 mov $0x0,%ebx
11c9: 48 8d 45 d0 lea -0x30(%rbp),%rax
11cd: 48 89 c7 mov %rax,%rdi
11d0: e8 e7 00 00 00 call 12bc <_ZN6StringD1Ev>
11d5: 48 8d 45 b0 lea -0x50(%rbp),%rax
11d9: 48 89 c7 mov %rax,%rdi
11dc: e8 db 00 00 00 call 12bc <_ZN6StringD1Ev>
11e1: 89 d8 mov %ebx,%eax
11e3: 48 8b 55 e8 mov -0x18(%rbp),%rdx
11e7: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11ee: 00 00
11f0: 74 05 je 11f7 <main+0x6e>
11f2: e8 89 fe ff ff call 1080 <__stack_chk_fail@plt>
11f7: 48 8b 5d f8 mov -0x8(%rbp),%rbx
11fb: c9 leave
11fc: c3 ret

main函数确实调用了String类的拷贝构造函数,查看String拷贝构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
00000000000012dc <_ZN6StringC1ERKS_>:
12dc: f3 0f 1e fa endbr64
12e0: 55 push %rbp
12e1: 48 89 e5 mov %rsp,%rbp
12e4: 48 83 ec 10 sub $0x10,%rsp
12e8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
12ec: 48 89 75 f0 mov %rsi,-0x10(%rbp)
12f0: 48 8b 45 f8 mov -0x8(%rbp),%rax
12f4: 48 8b 55 f0 mov -0x10(%rbp),%rdx
12f8: 48 89 d6 mov %rdx,%rsi # %rsi = &rhs->word
12fb: 48 89 c7 mov %rax,%rdi # %rdi = &this->word
12fe: e8 69 ff ff ff call 126c <_ZN4WordC1ERKS_> # call Word::Word(const Word &)
1303: 48 8b 45 f0 mov -0x10(%rbp),%rax # %rax = rhs
1307: 8b 50 10 mov 0x10(%rax),%edx # %edx = rhs->len
130a: 48 8b 45 f8 mov -0x8(%rbp),%rax # %rax = this
130e: 89 50 10 mov %edx,0x10(%rax) # this->len = %edx
1311: 90 nop
1312: c9 leave
1313: c3 ret

可见,合成的String的拷贝构造函数类似:

1
2
3
4
5
String::String(const String & rhs)
{
word.Word::Word(rhs.word);
len = rhs.len;
}

因此该说法成立。

2 基类 具有copy constructor

如下是测试程序,Base类以及其子类Dervied,此时不满足条件一,即其成员均为基本类型,不包含拷贝构造函数,因此编译器也不会为其合成一个拷贝构造函数。

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

class Base
{
public:
Base() = default;
Base(const Base &) = default;
private:
char *str;
int len;
};

class Derived : public Base
{
public:
Derived() = default;
Derived(const Derived &) = default;
private:
double d;
};

int main()
{
String s1;
String s2 = s1;

return 0;
}

编译,反汇编查看.text段情况,此时,Base和Dervied的默认构造函数、拷贝构造函数均未被合成。

为Base类添加拷贝构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
class Base
{
public:
Base() = default;
Base(const Base & b) : str(b.str), len(b.len)
{
}

private:
char *str;
int len;
};

重新编译并反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0000000000001189 <main>:
1189: f3 0f 1e fa endbr64
118d: 55 push %rbp
118e: 48 89 e5 mov %rsp,%rbp
1191: 48 83 ec 40 sub $0x40,%rsp
1195: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
119c: 00 00
119e: 48 89 45 f8 mov %rax,-0x8(%rbp)
11a2: 31 c0 xor %eax,%eax
11a4: 48 8d 55 c0 lea -0x40(%rbp),%rdx
11a8: 48 8d 45 e0 lea -0x20(%rbp),%rax
11ac: 48 89 d6 mov %rdx,%rsi
11af: 48 89 c7 mov %rax,%rdi
11b2: e8 bb 00 00 00 call 1272 <_ZN7DerivedC1ERKS_>
11b7: b8 00 00 00 00 mov $0x0,%eax
11bc: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11c0: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11c7: 00 00
11c9: 74 05 je 11d0 <main+0x47>
11cb: e8 b0 fe ff ff call 1080 <__stack_chk_fail@plt>
11d0: c9 leave
11d1: c3 ret

main函数中调用了Derived类的拷贝构造函数,且可以看到.text段中包含Base和Dervied类的拷贝构造函数:

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
0000000000001242 <_ZN4BaseC1ERKS_>:
1242: f3 0f 1e fa endbr64
1246: 55 push %rbp
1247: 48 89 e5 mov %rsp,%rbp
124a: 48 89 7d f8 mov %rdi,-0x8(%rbp)
124e: 48 89 75 f0 mov %rsi,-0x10(%rbp)
1252: 48 8b 45 f0 mov -0x10(%rbp),%rax
1256: 48 8b 10 mov (%rax),%rdx
1259: 48 8b 45 f8 mov -0x8(%rbp),%rax
125d: 48 89 10 mov %rdx,(%rax)
1260: 48 8b 45 f0 mov -0x10(%rbp),%rax
1264: 8b 50 08 mov 0x8(%rax),%edx
1267: 48 8b 45 f8 mov -0x8(%rbp),%rax
126b: 89 50 08 mov %edx,0x8(%rax)
126e: 90 nop
126f: 5d pop %rbp
1270: c3 ret
1271: 90 nop

0000000000001272 <_ZN7DerivedC1ERKS_>:
1272: f3 0f 1e fa endbr64
1276: 55 push %rbp
1277: 48 89 e5 mov %rsp,%rbp
127a: 48 83 ec 10 sub $0x10,%rsp
127e: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1282: 48 89 75 f0 mov %rsi,-0x10(%rbp)
1286: 48 8b 45 f8 mov -0x8(%rbp),%rax
128a: 48 8b 55 f0 mov -0x10(%rbp),%rdx
128e: 48 89 d6 mov %rdx,%rsi
1291: 48 89 c7 mov %rax,%rdi
1294: e8 a9 ff ff ff call 1242 <_ZN4BaseC1ERKS_> # call Base::Base(const Base &)
1299: 48 8b 45 f0 mov -0x10(%rbp),%rax
129d: f2 0f 10 40 10 movsd 0x10(%rax),%xmm0
12a2: 48 8b 45 f8 mov -0x8(%rbp),%rax
12a6: f2 0f 11 40 10 movsd %xmm0,0x10(%rax) # 拷贝浮点类型成员
12ab: 90 nop
12ac: c9 leave
12ad: c3 ret

Base类的拷贝构造函数完成自身非static成员的拷贝初始化,但如果我们显式定义的Base的拷贝构造为空,则编译器不会默认帮我们拷贝Base的成员,而编译器合成的Derived类的拷贝构造函数,自动调用了Base的拷贝构造函数,且帮我们拷贝初始化了double类型的成员。

因此,条件二成立。

3 带有”virtual function“的class

如下测试程序,父类和子类均包括虚函数:

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
class ZooAnimal
{
public:
ZooAnimal() = default;
~ZooAnimal() = default;
virtual void animate() { cout << "ZooAnimal animate\n"; }
virtual void draw() { cout << "ZooAnimal draw\n"; }

private:
};

class Bear : public ZooAnimal
{
public:
Bear() = default;
void animate() { cout << "Bear animate\n"; }
void draw() { cout << "Bear draw\n"; }
virtual void dance() { cout << "Bear dance\n"; }

private:
};

void draw(ZooAnimal &zoey)
{
zoey.draw();
}

int main()
{
Bear yogi;
Bear winnie = yogi;

ZooAnimal franny = yogi;
draw(yogi); // call Bear::draw()
draw(franny); // call ZooAnimal::draw()

return 0;
}

编译运行,父类对象和子类对象调用各自的虚函数:

1
2
3
> g++ two.cc -O0 -g -o two && ./two 
Bear draw
ZooAnimal draw

反汇编查看main函数部分:

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
00000000000011d3 <main>:
11d3: f3 0f 1e fa endbr64
11d7: 55 push %rbp
11d8: 48 89 e5 mov %rsp,%rbp
11db: 48 83 ec 20 sub $0x20,%rsp
11df: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
11e6: 00 00
11e8: 48 89 45 f8 mov %rax,-0x8(%rbp)
11ec: 31 c0 xor %eax,%eax
11ee: 48 8d 05 43 2b 00 00 lea 0x2b43(%rip),%rax # 3d38 <_ZTV4Bear+0x10>
11f5: 48 89 45 e0 mov %rax,-0x20(%rbp)
11f9: 48 8d 05 38 2b 00 00 lea 0x2b38(%rip),%rax # 3d38 <_ZTV4Bear+0x10>
1200: 48 89 45 e8 mov %rax,-0x18(%rbp)
1204: 48 8d 05 55 2b 00 00 lea 0x2b55(%rip),%rax # 3d60 <_ZTV9ZooAnimal+0x10>
120b: 48 89 45 f0 mov %rax,-0x10(%rbp)
120f: 48 8d 45 e0 lea -0x20(%rbp),%rax
1213: 48 89 c7 mov %rax,%rdi
1216: e8 8e ff ff ff call 11a9 <_Z4drawR9ZooAnimal>
121b: 48 8d 45 f0 lea -0x10(%rbp),%rax
121f: 48 89 c7 mov %rax,%rdi
1222: e8 82 ff ff ff call 11a9 <_Z4drawR9ZooAnimal>
1227: b8 00 00 00 00 mov $0x0,%eax
122c: 48 8b 55 f8 mov -0x8(%rbp),%rdx
1230: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
1237: 00 00
1239: 74 05 je 1240 <main+0x6d>
123b: e8 60 fe ff ff call 10a0 <__stack_chk_fail@plt>
1240: c9 leave
1241: c3 ret

此时编译器并未合成默认构造函数和拷贝构造函数,而是直接完成了虚指针的设置。与条件三稍有出入。值得注意的是,两个Bear对象的虚指针是相同的,这意味着两个对象共享虚表。

4 virtual base class

如下测试程序,Y继承自X。此时X和Y的默认构造函数和拷贝构造函数均为trivial,因此编译器不会合成它们的默认构造函数和拷贝构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class X
{
};

class Y : public X
{
};

int main()
{
X x1;
X x2 = x1;
Y y1;
Y y2 = y1;

return 0;
}

编译,反汇编查看.text段,与我们的预期吻合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00000000000011b3 <main>:
11b3: f3 0f 1e fa endbr64
11b7: 55 push %rbp
11b8: 48 89 e5 mov %rsp,%rbp
11bb: 48 83 ec 10 sub $0x10,%rsp
11bf: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
11c6: 00 00
11c8: 48 89 45 f8 mov %rax,-0x8(%rbp)
11cc: 31 c0 xor %eax,%eax
11ce: b8 00 00 00 00 mov $0x0,%eax
11d3: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11d7: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11de: 00 00
11e0: 74 05 je 11e7 <main+0x34>
11e2: e8 99 fe ff ff call 1080 <__stack_chk_fail@plt>
11e7: c9 leave
11e8: c3 ret

补充测试程序,添加一个类Z,其虚拟继承自X,其与Y的区别仅在于继承是否为虚拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
class Z : virtual public X
{
};

int main()
{
X x1;
X x2 = x1;
Z y1;
Z y2 = y1;

return 0;
}

编译并反汇编,编译器会为Z类合成默认构造函数和拷贝构造函数,且main函数会调用它们:

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
00000000000011b3 <main>:
11b3: f3 0f 1e fa endbr64
11b7: 55 push %rbp
11b8: 48 89 e5 mov %rsp,%rbp
11bb: 48 83 ec 20 sub $0x20,%rsp
11bf: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
11c6: 00 00
11c8: 48 89 45 f8 mov %rax,-0x8(%rbp)
11cc: 31 c0 xor %eax,%eax
11ce: 48 8d 45 e8 lea -0x18(%rbp),%rax
11d2: 48 89 c7 mov %rax,%rdi
11d5: e8 9e 00 00 00 call 1278 <_ZN1ZC1Ev>
11da: 48 8d 55 e8 lea -0x18(%rbp),%rdx
11de: 48 8d 45 f0 lea -0x10(%rbp),%rax
11e2: 48 89 d6 mov %rdx,%rsi
11e5: 48 89 c7 mov %rax,%rdi
11e8: e8 a9 00 00 00 call 1296 <_ZN1ZC1ERKS_>
11ed: b8 00 00 00 00 mov $0x0,%eax
11f2: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11f6: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11fd: 00 00
11ff: 74 05 je 1206 <main+0x53>
1201: e8 7a fe ff ff call 1080 <__stack_chk_fail@plt>
1206: c9 leave
1207: c3 ret

查看合成的两个构造函数:

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
0000000000001278 <_ZN1ZC1Ev>:
1278: f3 0f 1e fa endbr64
127c: 55 push %rbp
127d: 48 89 e5 mov %rsp,%rbp
1280: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1284: 48 8d 15 d5 2a 00 00 lea 0x2ad5(%rip),%rdx # 3d60 <_ZTT1Z>
128b: 48 8b 45 f8 mov -0x8(%rbp),%rax
128f: 48 89 10 mov %rdx,(%rax)
1292: 90 nop
1293: 5d pop %rbp
1294: c3 ret
1295: 90 nop

0000000000001296 <_ZN1ZC1ERKS_>:
1296: f3 0f 1e fa endbr64
129a: 55 push %rbp
129b: 48 89 e5 mov %rsp,%rbp
129e: 48 89 7d f8 mov %rdi,-0x8(%rbp)
12a2: 48 89 75 f0 mov %rsi,-0x10(%rbp)
12a6: 48 8d 15 b3 2a 00 00 lea 0x2ab3(%rip),%rdx # 3d60 <_ZTT1Z>
12ad: 48 8b 45 f8 mov -0x8(%rbp),%rax
12b1: 48 89 10 mov %rdx,(%rax)
12b4: 90 nop
12b5: 5d pop %rbp
12b6: c3 ret

使用命令查看三个名词:

1
2
3
4
5
6
> c++filt _ZN1ZC1Ev
Z::Z()
> c++filt _ZN1ZC1ERKS_
Z::Z(Z const&)
> c++filt _ZTT1Z
VTT for Z

构造函数主要完成VTT的设置(关于虚基类后续会有更多讨论),目前可以理解为,这种情况类似包含虚函数的类,需要做一些信息的处理。

the next…

我们可以总结,默认构造函数和拷贝构造函数,按照memberwise的方式初始化类的非static成员,若类的所有成员都是基本的内置类型,或者是没有默认构造和拷贝构造的类对象,此时其是具有bitwise语义的,可以直接通过mov指令完成初始化和拷贝。当类中存在某些成员或者subobject(即继承自父类的部分),需要调用它们的默认构造和拷贝构造,则编译器会合成此类的默认构造和拷贝构造,它们调用这些成员和父类的相应的构造操作。

如果类中包含虚函数或者其继承自虚基类,此时类中会有编译器安插的特殊成员(虚指针),这些相关的信息需要编译器帮类设置和管理,此时编译器会合成默认构造和拷贝构造。关于这部分的内容,涉及到类对象是如何布局的,以及虚表的管理,后续进一步分析……