目录
接下来我将持续更新“深度解读《深度探索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_derived为false,它们将不会调用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与编程之窗”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。