如果一个类不包含任何构造函数,当需要时,编译器会自动合成一个,这句看似司空见惯的法则,其实和第一感觉不同。关键在于这个需要时 ,因为有时候其实编译器并不要专门为一个类合成构造函数。我们假设有一个类,其内部只有普通的内置类型成员,当有一个该类类型的局部对象时,其内部数据成员(如果有)的值保持随意就好,当有一个该类类型的全局对象时,其处于.bss段,直接默认清零即可,拷贝构造函数也是类似,将一个对象占有的空间的内容逐个字节拷贝即可,此时构造函数被称为trivial ,即无用的。但是某些时候,当我们进行默认初始化,或者拷贝初始化时,如果我们没有显式定义构造函数和拷贝构造函数,此时编译器必须为我们合成,完成必需的工作,比如一个带有虚函数的类,编译器必须合成一个构造函数完成虚指针的设定,此时构造函数被称为nontrivial 。
只有nontrivial default constructor才会被编译器合成,本文讨论何时编译器会真正合成默认&拷贝构造函数。
我使用的编译环境:
1 2 3 4 5 6 7 > uname -a Linux net 5.19.0-35-generic > 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) { 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 1 e fa endbr64 122 d: 55 push %rbp 122 e: 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 123 c: 00 00 123 e: 48 89 45 f8 mov %rax,-0x8 (%rbp) 1242 : 31 c0 xor %eax,%eax 1244 : c7 45 e0 0 c 00 00 00 movl $0xc ,-0x20 (%rbp) # -10 (%rbp)为local地址,此指令将0xc 赋给i 124b : f3 0f 10 05 b5 0 d 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 8 d 45 e0 lea -0x20 (%rbp),%rax 1264 : 48 89 c6 mov %rax,%rsi 1267 : 48 8 d 05 d2 2 d 00 00 lea 0x2dd2 (%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4 > 126 e: 48 89 c7 mov %rax,%rdi 1271 : e8 c7 00 00 00 call 133 d <_ZlsRSoRK3Foo> 1276 : 48 8b 15 53 2 d 00 00 mov 0x2d53 (%rip),%rdx # 3f d0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GLIBCXX_3.4 > 127 d: 48 89 d6 mov %rdx,%rsi 1280 : 48 89 c7 mov %rax,%rdi 1283 : e8 68 fe ff ff call 10f 0 <_ZNSolsEPFRSoS_E@plt> 1288 : 48 8 d 05 81 2 d 00 00 lea 0x2d81 (%rip),%rax # 4010 <global> 128f : 48 89 c6 mov %rax,%rsi 1292 : 48 8 d 05 a7 2 d 00 00 lea 0x2da7 (%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4 > 1299 : 48 89 c7 mov %rax,%rdi 129 c: e8 9 c 00 00 00 call 133 d <_ZlsRSoRK3Foo> 12 a1: 48 8b 15 28 2 d 00 00 mov 0x2d28 (%rip),%rdx # 3f d0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GLIBCXX_3.4 > 12 a8: 48 89 d6 mov %rdx,%rsi 12 ab: 48 89 c7 mov %rax,%rdi 12 ae: e8 3 d fe ff ff call 10f 0 <_ZNSolsEPFRSoS_E@plt> 12b 3: b8 00 00 00 00 mov $0x0 ,%eax 12b 8: 48 8b 55 f8 mov -0x8 (%rbp),%rdx 12b c: 64 48 2b 14 25 28 00 sub %fs:0x28 ,%rdx 12 c3: 00 00 12 c5: 74 05 je 12 cc <main+0xa3 > 12 c7: e8 34 fe ff ff call 1100 <__stack_chk_fail@plt> 12 cc: c9 leave 12 cd: 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的值被设为给定的默认值。
此时,编译器未合成一个默认构造函数,而是直接在初始化对象的地方直接通过指令设置成员的值。
若我们显式要求编译器合成一个默认构造函数:
反汇编查看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安排一个初始值,运行结果如下:
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> 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> 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> 1324: 48 8b 45 f8 mov -0x8(%rbp),%rax 1328: c7 40 04 00 08 00 00 movl $0x800 ,0x4(%rax) 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> 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> 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 : 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 () {} }; 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); draw (franny); 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 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 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(即继承自父类的部分),需要调用它们的默认构造和拷贝构造,则编译器会合成此类的默认构造和拷贝构造,它们调用这些成员和父类的相应的构造操作。
如果类中包含虚函数或者其继承自虚基类,此时类中会有编译器安插的特殊成员(虚指针),这些相关的信息需要编译器帮类设置和管理,此时编译器会合成默认构造和拷贝构造。关于这部分的内容,涉及到类对象是如何布局的,以及虚表的管理,后续进一步分析……