深度解读《深度探索C++对象模型》之C++对象的构造过程(二)

目录

虚继承下的对象构造


接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎左下角点击关注!也可以关注公众号:iShare爱分享,或文章末尾扫描二维码,自动获得推文和全部的文章列表。

        第一篇讲解了C++对象的一般构造过程和成员初始化列表,以及在有虚函数的情况下,虚表指针vptr的设置实现分析,上一篇请从这里阅读:

        深度解读《深度探索C++对象模型》之C++对象的构造过程(一) 

        这一篇将继续讲解在虚继承的情况下的对象的构造流程。 

虚继承下的对象构造

        具有虚基类时,对象的构造过程特殊的地方在于虚基类的构造,由谁来负责构造虚基类?由于虚继承的共享性,即所有的派生类都共享同一个虚基类子对象,为避免重复构造虚基类,所以C++规定应该由最派生类(即继承体系下最末端的类)来负责构造,处于继承体系中间位置的类不需要构造虚基类,当它处于最派生类的位置时就需要去构造虚基类,意思就是说,当一个类有一个虚基类时,当它作为另一个类的基类时,这时它不需要负责构造虚基类,因为它只是一个子对象而不是一个完整的对象,当使用它来定义一个对象时,这时它处于最末端,构造的是一个完整的对象,所以这时它需要负责构造虚基类。这就引出一个问题:对于同一个类的构造函数,它需要根据不同的条件实施不同的行为,即有时需要构造虚基类,有时不需要。编译器应该如何处理这个问题?我们以下面的代码为例:

#include <cstdio>

class Grand {
public:
    Grand(int i): g(i) { printf("%s: g = %d\n", __PRETTY_FUNCTION__, g); }
private:
    int g;
};

class Base1: virtual public Grand {
public:
    Base1(int a, int b): Grand(a), b1(b) { printf("%s: b1 = %d\n", __PRETTY_FUNCTION__, b1); }
private:
    int b1;
};

class Base2: virtual public Grand {
public:
    Base2(int a, int b): Grand(a), b2(b) { printf("%s: b2 = %d\n", __PRETTY_FUNCTION__, b2); }
private:
    int b2;
};

class Derived: public Base1, public Base2 {
public:
    Derived(): Grand(0), Base1(1, 2), Base2(3, 4), d{0} { }
private:
    int d;
};

int main() {
    Derived d;
    Base1 b1(10, 20);
    Base2 b2(30, 40);

    return 0;
}

        先来看下它的输出:

// Derived d 的输出:
Grand::Grand(int): g = 0
Base1::Base1(int, int): b1 = 2
Base2::Base2(int, int): b2 = 4
// Base1 b1(10, 20) 的输出:
Grand::Grand(int): g = 10
Base1::Base1(int, int): b1 = 20
// Base2 b2(30, 40) 的输出:
Grand::Grand(int): g = 30
Base2::Base2(int, int): b2 = 40

        从输出结果可以看到,定义Derived类对象d时,虚基类Grand只被构造了一次,Base1类和Base2类的构造函数中调用Grand类的构造函数没有执行,不会存在重复构造的问题,当定义Base1类和Base2类的对象时,虚基类Grand的构造函数则分别被Base1类和Base2类的构造函数调用。

        如何实现这个功能?有一个方法就是在Base1和Base2类的构造函数中插入一个参数,以指示是否需要调用Grand类的构造函数,默认值是true,修改函数的声明得由编译器来实行,不需要程序员插手,比如将Base1类的构造函数改造成如下的伪代码:

Base1(Base1* this, int a, int b,bool most_derived = true) {
    if (most_derived != false)
    	this->Grand::Grand(a);
    
    this->b1 = b;
    printf("%s: b1 = %d\n", __PRETTY_FUNCTION__, b1); 
}

        而Derived类的构造函数则修改成如下的伪代码:

Derived(Derived* this, bool most_derived = true) {
    if (most_derived != false)
    	this->Grand::Grand(0);

    this->Base1::Base1(1, 2, false);
	this->Base2::Base2(3, 4, false);
	this->d = 0;
}

        这样就可以满足需要:当使用“Derived d”定义对象时,将在Derived类的构造函数中调用Grand的构造函数,这时传给Base1和Base2类的构造函数的参数most_derivedfalse,它们将不会调用Grand的构造函数;而当使用“Base1 b1”定义对象时,Base1类的构造函数中就会调用Grand类的构造函数。

        这个方案看起来像完美地解决了需求,但实际上并不是最高效的做法,因为编译器得修改构造函数的定义,每次运行时需要判断条件。更高效的做法是将构造函数一分为二,一个用于构造作为子对象时调用,一个用于构造完整对象时使用。这个也是目前主流编译器的做法,第一种方法目前的编译器应该没有使用了。来看看Clang编译器将上面代码生成的汇编代码:

main:		# @main
    # 略...
    call    Derived::Derived() [complete object constructor]
    # 略...
    call    Base1::Base1(int, int) [complete object constructor]
    # 略...
    call    Base2::Base2(int, int) [complete object constructor]
    # 略...

Derived::Derived() [complete object constructor]:	# @Derived::Derived() [complete object constructor]
    # 略...
    call    Grand::Grand(int) [base object constructor]
    # 略...
    call    Base1::Base1(int, int) [base object constructor]
    # 略...
    call    Base2::Base2(int, int) [base object constructor]
    # 略...

Base1::Base1(int, int) [base object constructor]:	# @Base1::Base1(int, int) [base object constructor]
		# 略...

Base1::Base1(int, int) [complete object constructor]: # @Base1::Base1(int, int) [complete object constructor]
    # 略...
    call    Grand::Grand(int) [base object constructor]
    # 略...

Base2::Base2(int, int) [base object constructor]:	# @Base2::Base2(int, int) [base object constructor]
  	# 略...

Base2::Base2(int, int) [complete object constructor]: # @Base2::Base2(int, int) [complete object constructor]
    # 略...
    call    Grand::Grand(int) [base object constructor]
    # 略...

        编译器生成了Base1类和Base2类的两个类型的构造函数:“base object constructor”和“complete object constructor”,分别在不同的地方调用,当它们作为Derived类的子对象时,调用的是“base”版本的,如在Derived类的构造函数中那样,当用它们定义一个完整的对象时,调用的则是“complete”版本的,如在main函数中的那样,这两种构造函数的区别就是“complete”版本的会去调用Grand类的构造函数,其它的代码则是一样的。代码中Derived类的构造函数没有两个版本,是因为暂时不需要,编译器就没有生成出来,当它作为另一个类的基类时,那时才会生成出来。

(未完待续。。。敬请点击左下角的关注以获得及时更新)


本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“AI与编程之窗”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI与编程之窗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值