上一篇我们介绍了类对象占用空间的大小,这一节开始我们重新认识一下构造函数。
2.0 默认缺省构造函数认识
在我们学习c++的时候,如果我们忘记写构造函数,老师都会说,编译器会生成一个默认的缺省函数。
2.0.0 例子
那结果是不是这样的呢?我们下面来一探究竟:
上代码:
#include <iostream>
#include <stdio.h>
class A
{
public:
// A()
// {
// m_a = 0;
// m_b = 0;
// }
int get_a()
{
return m_a;
}
int get_b()
{
return m_b;
}
private:
int m_a;
int m_b;
};
int main(int argc, char **argv)
{
A a;
int aa = a.get_a();
int bb = a.get_b();
printf("a = %d b = %d\n", aa, bb);
return 0;
}
我们先把有构造函数的代码打开吧,因为我们现在也不知道构造函数长啥样。
编译,然后我们反汇编一下:
main:
.LFB1026:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $48, %rsp
movl %edi, -36(%rbp)
movq %rsi, -48(%rbp)
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
leaq -16(%rbp), %rax
movq %rax, %rdi
call _ZN1AC1Ev
leaq -16(%rbp), %rax
movq %rax, %rdi
call _ZN1A5get_aEv
我们在main函数中找到了_ZN1AC1Ev这个构造函数,这次是我明确定义了构造函数,那等下我不定义构造函数,看看还有没有生成默认构造函数:
main:
.LFB1023:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $48, %rsp
movl %edi, -36(%rbp)
movq %rsi, -48(%rbp)
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
leaq -16(%rbp), %rax
movq %rax, %rdi
call _ZN1A5get_aEv
movl %eax, -24(%rbp)
leaq -16(%rbp), %rax
发现这一波操作下来没有发现_ZN1AC1Ev,所以这一波我们又被老师骗了。构造函数不是这样生成的,让我当年那么相信老师,结果又被老师骗了。(这时肯定有人出来说,是我当年听课不认真,这个确实当年上课不认真,所以现在才要好好补基础,说多都是累啊)
2.0.1 什么时候需要生成默认构造函数
那在什么时候会生成默认构造函数呢?
我们来看一下《深入探索c++对象模型》中说的"default constructors 在需要的时候被编译器产生出来"。
这时候就有点意思了,“在需要的时候”,被谁需要?做什么事情?
在2.0.0的例子中,为什么不生成构造函数?因为那是我们程序员需要,不是编译器需要,程序员需要一个构造函数把m_a和m_b初始化为0。
如果是程序员需要,那就需要程序员自己定义一个构造函数,然后把变量进行初始化。
那些,什么时候才会合成一个默认的构造函数呢?当编译器需要的时候!!!!!
这话说的跟没说一样,那编译器啥时候才需要?合成的默认构造函数里面做了什么操作?
下面我们就带着问题来探索一般。
2.1 "带有Default Construct0r"的Member Class Object
这个就不翻译成中文了,就跟侯捷老师一样吧。
这个是什么意思呢?可以解释一下:
如果一个类A没有任何构造函数,但它内含一个成员变量是类B,而类B有默认构造函数,那么编译器会为类A合成一个默认构造函数,这个默认构造函数负责调用类B的构造函数。
2.1.1 没有写构造函数
老规矩,上代码:
#include <iostream>
#include <stdio.h>
// 内含一个成员类B
class B
{
public:
B() // 类B需要一个构造函数
{
m_a = 0;
m_b = 0;
}
private:
int m_a;
int m_b;
};
// 我们就写一个没有构造函数的类A
class A
{
public:
int get_a()
{
return m_a;
}
int get_b()
{
return m_b;
}
private:
B b; // 类A包含类B
int m_a;
int m_b;
};
int main(int argc, char **argv)
{
A a; // 定义一个类A的变量
return 0;
}
我们来反汇编看看:
main:
.LFB1026:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $48, %rsp
movl %edi, -36(%rbp)
movq %rsi, -48(%rbp)
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
leaq -32(%rbp), %rax
movq %rax, %rdi
call _ZN1AC1Ev
movl $0, %eax
movq -8(%rbp), %rdx
xorq %fs:40, %rdx
je .L5
call __stack_chk_fail
前面一大堆不认识的,我们就认准这个_ZN1AC1Ev,果然是生成了一个默认的构造函数,函数的内容是啥:
_ZN1AC2Ev:
.LFB1028:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN1BC1Ev
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
前面一大推也不知道, 就记住了_ZN1BC1Ev这个,这个就是类B的构造函数。
所以编译器会自己合成一个构造函数,并且调用类B的构造函数。
2.1.2 如果存在构造函数
2.1.1中,我们是没有实现构造函数,那我们如果写了一个构造函数的情况下,编译器是会自己合成一个构造函数,还是在我们实现的构造函数中插入代码?这个答案已经显而易见了,不过我们还是来探索一波。
整个代码我就不贴上来,像上一篇一样贴太多代码,这就贴变化的部分:
// 我们就写一个没有构造函数的类A
class A
{
public:
A() // 新增加的部分
{
m_a = 0;
m_b = 0;
}
int get_a()
{
return m_a;
}
int get_b()
{
return m_b;
}
private:
B b;
int m_a;
int m_b;
};
我们继续反汇编查看:
main函数就不看,反正就是调用类A的构造函数,我们直接看类A的构造函数里面有啥:
_ZN1AC2Ev:
.LFB1025:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN1BC1Ev
movq -8(%rbp), %rax
movl $0, 8(%rax)
movq -8(%rbp), %rax
movl $0, 12(%rax)
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
是不是明显发现,编译器会在我们写的代码前面添加调用类B的构造函数。
这种情况就是如果我们有写构造函数,编译器就会在我们的构造函数之前,默认添加调用类B的构造函数。
2.1.3 多个类存在,构造函数调用顺序
这个问题应该不难,大家应该都直接是按顺序调用的,这个写个例子看看就可以了。
#include <iostream>
#include <stdio.h>
class C
{
public:
C()
{
}
};
// 内含一个成员类B
class B
{
public:
B()
{
m_a = 0;
m_b = 0;
}
private:
int m_a;
int m_b;
};
// 我们就写一个没有构造函数的类A
class A
{
public:
A()
{
m_a = 0;
m_b = 0;
}
A(int a)
{
m_a = a;
m_b = 0;
}
int get_a()
{
return m_a;
}
int get_b()
{
return m_b;
}
private:
B b;
C c;
int m_a;
int m_b;
};
int main(int argc, char **argv)
{
A a;
return 0;
}
我们直接看反汇编结果,其实也打印在构造函数中打印:
_ZN1AC2Ev:
.LFB1028:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN1BC1Ev
movq -8(%rbp), %rax
addq $8, %rax
movq %rax, %rdi
call _ZN1CC1Ev
movq -8(%rbp), %rax
movl $0, 12(%rax)
movq -8(%rbp), %rax
movl $0, 16(%rax)
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
从反汇编的角度就能看出,如果不相信可以自己变换b,c的位置,自己测试一波,我这里就不测试了,再测试就又水很多字了。
2.2 "带有Default Constructor"的Base Class
这个是什么意思呢?
这个其实跟2.1的也差不多,解释是:一个类A继承一个类B,类B中有构造函数,这时候编译器会给类A合成一个默认的构造函数。
感觉跟上面的差不多:
2.2.1 没有写构造函数
#include <iostream>
#include <stdio.h>
using namespace std;
class B
{
public:
B()
{
}
};
class A : public B
{
};
int main(int argc, char **argv)
{
A a;
return 0;
}
反汇编查看:
main:
.LFB1024:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
leaq -9(%rbp), %rax
movq %rax, %rdi
call _ZN1AC1Ev
movl $0, %eax
movq -8(%rbp), %rdx
xorq %fs:40, %rdx
je .L5
call __stack_chk_fail
我们来看类A的构造函数里面有啥:
_ZN1AC2Ev:
.LFB1026:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN1BC2Ev
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
类B的构造函数就不用看,也没啥好看的,哈哈哈。
2.2.2 如果存在构造函数
感觉我就是一个无情的复制粘贴机器,如果吧,不写的话,就又感觉少了点啥,写吧,好像也是赋值粘贴,惆怅。
class A : public B
{
public:
A(int a)
{
}
};
int main(int argc, char **argv)
{
A a(1);
return 0;
}
反汇编:
_ZN1AC2Ei:
.LFB1025:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %rdi, -8(%rbp)
movl %esi, -12(%rbp)
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN1BC2Ev
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
明显也是插入了调用类B的构造函数。
2.2.3 多继承时,构造函数调用顺序
#include <iostream>
#include <stdio.h>
using namespace std;
class C
{
public:
C()
{
}
};
class B
{
public:
B()
{
}
};
class A : public C, public B
{
public:
A(int a)
{
}
};
int main(int argc, char **argv)
{
A a(1);
return 0;
}
直接反汇编查看:
_ZN1AC2Ei:
.LFB1028:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %rdi, -8(%rbp)
movl %esi, -12(%rbp)
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN1CC2Ev
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN1BC2Ev
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
确实也是按照继承的顺序的。
|