深度探索对象模型【好书】上有一节讲的是什么时候编译器会合成一个构造函数,上面列出了四种情况。因为初始化类的成员变量是程序员的责任,所以不要指望编译器合成的构造函数会做这些工作。在其他情况下,合成出来的构造函数也没什么用。就写两个栗子并反汇编简单看下其中一种情况:
3 class CStudent
4 {
5 public:
6 void ShowInfo()
7 {
8 //TODO
9 }
10 private:
11 int m_iNumber;
12 char m_cSex;
13 int m_iGrade;
14 };
16 int main()
17 {
18 CStudent stu[5];
19 return 0;
20 }
如果我们在main中定义 CStudent stu[5],则反汇编出来的结果是:
804857d: push %ebp
804857e: mov %esp,%ebp
8048580: sub $0x40,%esp
8048583: mov $0x0,%eax
esp桢指针往低地址减了64个字节,因为对齐的原因,一个CStudent实例会占用12个字节,加上预留的。然后什么也没用做。
再看下加了一个虚函数后的代码和汇编:
3 class CStudent
4 {
5 public:
6 virtual void DoNothing()
7 {
8 //TODO
9 }
10
11 void ShowInfo()
12 {
13 //TODO
14 }
15 private:
16 int m_iNumber;
17 char m_cSex;
18 int m_iGrade;
19 };
20
21 int main()
22 {
23 CStudent stu[5];
24 return 0;
25 }
80485e5: sub $0x60,%esp //开辟空间
80485e8: lea 0x10(%esp),%eax //取stu首地址,stu[0]在低地址
80485ec: mov $0x4,%esi //调用5次
80485f1: mov %eax,%ebx //stu首地址
80485f3: jmp 8048603 //跳转8048603
80485f5: mov %ebx,(%esp) // esp存储stu首地址
80485f8: call 8048676 <_ZN8CStudentC1Ev>
80485fd: add $0x10,%ebx //对stu + 16 = stu[1],以此类推
8048600: sub $0x1,%esi // esi = 3,2,1,0 -1
8048603: cmp $0xffffffff,%esi //-1 == esi?
8048606: jne 80485f5 //不等于跳转到80485f5
8048608: mov $0x0,%eax
804860d: lea -0x8(%ebp),%esp
08048676 <_ZN8CStudentC1Ev>:
8048676: push %ebp
8048677: mov %esp,%ebp
8048679: mov 0x8(%ebp),%eax
804867c: movl $0x8048728,(%eax)
8048682: pop %ebp
8048683: ret
现在sizeof(CStudeng) = 16B了,多了个指针成员。其中ZN8CStudentC1Ev是编译器为我们合成的默认构造函数,可以使用c++flit查看,
mov 0x8(%ebp),%eax,为什么是加8,加8取到对象的首地址,因为调用函数需要压栈返回地址和ebp。只做了给每个对象的前四个字节设置$0x8048728,这个是指向虚函数表的值了,为什么是它?他表示什么?这个会在下下一篇中解释到。编译器合成的仅仅设置了这个成员,其他几个数据成员并没有初始化【如果有定义构造函数的话,就不会合成了,而是会扩张每个,在user code前安插编译器的代码,千万不能调用memset等这样清数据的函数,会清了vptr值】。
这就是编译器做了它认为该做的了。像那些含有类实例的类实例,前者有默认构造函数而后者没有,也会合成等等。
下篇打算介绍重载带有默认形参的虚函数的一些不同之处。