C++虚继承


title: 虚继承
date: 2020-09-14 20:26:37
tags: c++
categories: c++

虚继承是由多继承和多重继承引发的一些问题。先说明虚继承和虚函数是两个不同的概念。下面看一个例子:

1、菱形继承

两个派生类继承同一个基类,又有某个类同时继承者两个派生类,这种继承被称为菱形继承或者钻石继承。

典型菱形继承示例

在这里插入图片描述

菱形继承问题:

  • 假设动物类中有个属性m_age。羊继承了动物的数据,驼同样继承了动物的数据,当草羊驼使用数据时,就会产生二义性。

  • 羊驼继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

案例

#include <iostream>
using namespace std;

//基类 动物类
class Animal
{
    public:
        int m_age;
};
// 羊类
class Sheep:public Animal{};
//驼类
class Tuo:public Animal{};

class SheepTuo :public Sheep, public Tuo{};

int main(int argc, char const *argv[])
{
    SheepTuo st;
    //st.m_age = 10;
    /*编译器报错,产生二义性。因为m_age属性既有继承自Tuo类,也有继承自Sheep类。
    如果直接使用st.m_age编译器不知道到底访问谁。但是可以通过下面方式区分访问*/ 
    st.Sheep::m_age = 10;
    st.Tuo::m_age = 20;
    /*虽然加上作用域可以访问到该属性,但是不是我们本质想要的结果。显然m_age属性
    在SheepTuo类中存在两份。而实际我们只需要一份该属性就行。菱形继承导致了该数据有两份。
    这样做显示浪费了我们的资源。后面可以使用虚继承解决该问题*/
    return 0;
}

在上面案例中,就是一个菱形继承问题。基类animal类中有个属性m_age,我们在SheepTuo类中想通过对象st以st.m_age = 10方式访问m_age时编译器报错,提示二义性。因为在SheepTuo类中继承了来自Sheep类中的m_age属性,也继承了Tuo类中m_age的属性。如果直接访问m_age属性编译器并不知道你要访问哪个,就会报错。但是我们可以在访问前面加上作用域限定符,告诉编译器我们访问那个。好了现在通过以上方式是能访问到m_age属性了,但是对象st的年龄到底是多少岁呢?可以看到在SheepTuo类中存在两份年龄属性。实际上我们只需要一份该数据即可。显然菱形继承承导致了该数据有两份。浪费了我们的资源。后面可以使用虚继承解决该问题

借助visual studio 2017提供的工具查看一下SheepTuo类的对象模型

在这里插入图片描述

通过对象模型看到,在SheepTuo类中确实存在两份m_age属性。一份是Sheep继承自Animal的,另一份是Tuo继承自Animal的。造成了内存资源的浪费。

2、虚继承

为了解决多继承时的命名冲突和冗余数据问题,C++提出了虚继承,使得在派生类中只保留一份间接基类的成员。虚继承只需在继承方式前加virtual关键词即可。此时公共的父类Animal称为虚基类

还是上面这个案例,使用虚继承看看。

#include <iostream>
using namespace std;

//基类 动物类
class Animal
{
    public:
        int m_age;
};
// 羊类
class Sheep:virtual public Animal{};
//驼类
class Tuo:virtual public Animal{};
//羊驼类
class SheepTuo :public Sheep, public Tuo{};

int main(int argc, char const *argv[])
{
    /*使用虚继承后st对象中m_age属性就只有一个了,继承自其它类的m_age共享一份数据*/
    SheepTuo st;
    st.m_age = 10;  //因为虚继承,m_age就只有一份了共享了,因此可以使用该方式访问了
    cout<<"Sheep::m_age ="<<st.Sheep::m_age<<endl;
    //由于共享数据,st.m_age改变后使用Sheep::m_age方式访问它,就是改变后的值
    st.Sheep::m_age = 20;
    cout<<"Tuo::m_age ="<<st.Tuo::m_age<<endl;
     //由于共享数据,Sheep::m_age改变后使用Tuo::m_age方式访问它,就是改变后的值
    st.Tuo::m_age = 20;
    cout<<"st.m_age ="<<st.m_age<<endl;


    return 0;
}

上面的使用虚继承解决了菱形继承带来的问题,在本例中派生类SheepTuo中就只保留了一份成员变量m_age,直接访问也不会有歧义。

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 Aniaml 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

虚继承
虚继承
Animal类
Sheep类
Tuo类
SheepTuo类
3、虚继承对象模型(底层实现)

上面看了在菱形继承时,不使用虚继承下对象的内存模型。下面看看使用了虚继承后的对象模型。

借助visual studio 2017提供的工具查看一下SheepTuo类的对象模型

cl /d1 reportSingleClassLayout查看的类名 所属文件名

在这里插入图片描述
在这里插入图片描述

左边为未使用虚继承时的对象模型,右边为虚继承后的对象模型。

在这里插入图片描述

使用虚继承后值只保留了虚基类Animal中m_age属性,Sheep类和Tuo类中多了一个虚基类指针(vbptr),它们指向各自的虚基类表(vbtable)。在vatable中存在一个偏移量,虚基类指针通过该类的地址加上偏移量即可访问到m_age属性。

4、虚继承下的构造函数
#include <iostream>
using namespace std;

//虚基类A
class A
{
public:
    A(int a):m_a(a){};
    int m_a;
};

//直接派生类B
class B: virtual public A
{
public:
    B(int a, int b):A(a),m_b(b){};
    void display()
    {
        cout<<"m_a="<<m_a<<", m_b="<<m_b<<endl;
    }
    int m_b;
};

//直接派生类C
class C: virtual public A
{
public:
    C(int a, int c):A(a),m_c(c){};
    void display()
    {
        cout<<"m_a="<<m_a<<", m_b="<<m_c<<endl;
    }

    int m_c;
};


//间接派生类D
class D: public B, public C
{
public:
    D(int a, int b, int c, int d): A(a), B(90, b), C(100, c), m_d(d){};

    void display()
    {
        cout<<"m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
    }

    int m_d;
};


int main(){
    B b(10, 20);
    b.display();
   
    C c(30, 40);
    c.display();

    D d(50, 60, 70, 80);
    d.display();
    return 0;
}
/*
m_a=10, m_b=20
m_a=30, m_b=40
m_a=50, m_b=60, m_c=70, m_d=80*/

在虚继承中,虚基类是由最终的派生类初始化的,换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。对最终的派生类来说,虚基类是间接基类,而不是直接基类。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。

采用了虚继承时,虚基类 A 在最终派生类 D 中只保留了一份成员变量 m_a,如果由 B 和 C 初始化 m_a,那么 B 和 C 在调用 A 的构造函数时很有可能给出不同的实参,这个时候编译器就会犯迷糊,不知道使用哪个实参初始化 m_a。

为了避免出现这种矛盾的情况,C++干脆规定必须由最终的派生类 D 来初始化虚基类 A,直接派生类 B 和 C 对 A 的构造函数的调用是无效的。在第 42 行代码中,调用 B 的构造函数时试图将 m_a 初始化为 90,调用 C 的构造函数时试图将 m_a 初始化为 100,但是输出结果有力地证明了这些都是无效的,m_a 最终被初始化为 50,这正是在 D 中直接调用 A 的构造函数的结果。

另外需要关注的是构造函数的执行顺序。虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的。

修改本例中第 42 行代码,改变构造函数出现的顺序:

D::D(int a, int b, int c, int d): B(90, b), C(100, c), A(a), m_d(d){ }

虽然我们将 A() 放在了最后,但是编译器仍然会先调用 A(),然后再调用 B()、C(),因为 A() 是虚基类的构造函数,比其他构造函数优先级高。如果没有使用虚继承的话,那么编译器将按照出现的顺序依次调用 B()、C()、A()。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值