深入探索c++对象模型(二、构造函数语义<上>)

上一篇我们介绍了类对象占用空间的大小,这一节开始我们重新认识一下构造函数。

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		// 类A的构造函数
	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			// 类B的构造函数
	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		// 调用类B的构造函数
	movq	-8(%rbp), %rax
	movl	$0, 8(%rax)		// 新加的m_a=0;
	movq	-8(%rbp), %rax
	movl	$0, 12(%rax)    // 新加的m_b = 0;
	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		// 类B的构造函数
	movq	-8(%rbp), %rax
	addq	$8, %rax
	movq	%rax, %rdi
	call	_ZN1CC1Ev		// 类C的构造函数
	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		// 默认生成的类A构造函数
	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		// 类B的构造函数
	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		// 插入了调用类B的构造函数
	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		// 类C的构造函数
	movq	-8(%rbp), %rax
	movq	%rax, %rdi
	call	_ZN1BC2Ev		// 类B的构造函数
	nop
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc

确实也是按照继承的顺序的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值