7.4 虚基类

 

7.4 虚基类

在前面讲过的例7.9中,由类A、类B1和类B2以及类C组成了类继承的层次结构。在该结构中,类A是类C的公共基类,在派生类中访问公共基类的成员时可能出现二义性,需要用适当的类名限定加以避免。另外,在对派生类C的对象初始化时,要调用两次公共基类的构造函数,对类A中的数据成员进行两次初始化。为了彻底避免在这种结构中的二义性,并在创建派生类对象时对公共基类的数据成员只进行一次初始化,必须将这个公共基类设定为虚基类。这便是引进虚基类的原因。

7.4.1 虚基类的引入和说明

前面简单介绍了引进虚基类的原因。实际上,引进虚基类的真正目的是为了解决二义性问题。

虚基类说明格式如下:

virtual 继承方式 基类名

其中,virtual是虚基类的关键字。虚基类的说明是在定义派生类时,写在派生类名的后面。例如:

class A {
public:
    void f();
protected:
    int a;
};

class B : virtual public A {
protected:
    int b;
};

class C : virtual public A {
protected:
    int c;
};

class D : public B, public C {
public:
    int g();
private:
    int d;
};

由于使用了虚基类,因此类A、类B、类C和类D之间的关系可用DAG图示法表示,如图7-5所示。

由于使用了虚基类,因此将两个公共基类合并成为一个类。这便是虚基类的作用,这样也就消除了合并之前可能出现的二义性。这时,类D对象初始化时只需调用一次类A的构造函数。因此,下面的引用都是正确的:

D n;
n.f(); // 对f()引用是正确的
void D::g() {
    f(); // 对f()引用是正确的
}

下面的程序段是正确的:

D n;
A *pa;
pa = &n;

其中,pa是指向类A对象的指针,n是类D的一个对象,&n是对象n的地址。pa = &n;是让pa指针指向类D的对象,这是正确的,并且也无二义性。

虚基类指针

引进虚基类后,派生类(即子类)的对象中只存在一个虚基类的子对象。当一个类有虚基类时,编译系统将为该类的对象定义一个指针成员,让它指向虚基类的子对象。该指针称为虚基类指针。在前面列举的关系中,各类的存储结构如图7-6所示。

7.4.2 含有虚基类的派生类的构造函数

前面讲过,为了初始化基类的子对象,派生类的构造函数要调用基类的构造函数。对于虚基类来讲,由于派生类的对象中只有一个虚基类子对象。为保证虚基类子对象只被初始化一次,这个虚基类构造函数必须只被调用一次。由于继承结构的层次可能很深,规定将在建立对象时所指定的类称为最派生类。C++语言规定,虚基类子对象是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。如果一个派生类有一个直接或间接的虚基类,那么派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未被列出,则表示使用该虚基类的默认构造函数来初始化派生类对象中的虚基类子对象。

从虚基类直接或间接继承的派生类中的构造函数的成员初始化列表中要列出对这个虚基类构造函数的调用。于是,只有用于建立对象的那个最派生类的构造函数调用虚基类的构造函数,而该派生类的直接基类中所列出的对这个虚基类的构造函数调用在执行中被忽略,这样便保证了对虚基类的子对象只初始化一次。

C++语言又规定,在一个成员初始化列表中出现对虚基类和非虚基类构造函数的调用,则虚基类的构造函数先于非虚基类的构造函数执行。

下面举一个例子说明具有虚基类的派生类的构造函数的用法。

[例7.10] 分析下列程序的输出结果
#include <iostream>
using namespace std;

class A {
public:
    A(const char *s) {
        cout << s << endl;
    }
    ~A() {}
};

class B : virtual public A {
public:
    B(const char *s1, const char *s2) : A(s1) {
        cout << s2 << endl;
    }
};

class C : virtual public A {
public:
    C(const char *s1, const char *s2) : A(s1) {
        cout << s2 << endl;
    }
};

class D : public B, public C {
public:
    D(const char *s1, const char *s2, const char *s3, const char *s4) : B(s1, s2), C(s1, s3), A(s1) {
        cout << s4 << endl;
    }
};

int main() {
    D *ptr = new D("class A", "class B", "class C", "class D");
    delete ptr;
    return 0;
}

执行该程序输出如下结果:

class A
class B
class C
class D

说明

该程序中,定义了类A、类B、类C和类D,它们之间的关系如图7-7所示。

在派生类B和C中使用了虚基类,使得建立的D类对象中只创建一次虚基类子对象。在派生类B、C、D的构造函数的成员初始化列表中都包含了对虚基类A的构造函数调用。

在建立类D对象时,只有类D的构造函数的成员初始化列表中列出的虚基类构造函数被调用,并且仅调用一次,而类D的基类的构造函数的成员初始化列表中列出的虚基类构造函数不被执行。这一点从该程序的输出结果可以看出。

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

夏驰和徐策

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

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

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

打赏作者

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

抵扣说明:

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

余额充值