C++语言学习(十六)——多继承

2021-07-14 16:05

阅读:780

标签:sizeof   函数   内存模型   out   语法规则   因此   eof   虚基类表指针   覆盖   

C++语言学习(十六)——多继承

一、多继承简介

1、多继承简介

C++语言支持多继承,一个子类可以有多个父类,子类拥有所有父类的成员变量,子类继承所有父类的成员函数,子类对象可以当作任意父类对象使用。

2、多继承语法规则

class Derived : public BaseA,
                   public BaseB,
                   public BaseC   
{

};

3、多继承派生类的内存布局

通过多重继承得到的派生类对象可能具有不同的地址。

#include 

using namespace std;

class BaseA
{
public:
    BaseA(int a)
    {
        ma = a;
    }
private:
    int ma;
};

class BaseB
{
public:
    BaseB(int b)
    {
        mb = b;
    }
private:
    int mb;
};

class Derived : public BaseA,public BaseB
{
public:
    Derived(int a, int b, int c):BaseA(a),BaseB(b)
    {
        mc = c;
    }
private:
    int mc;
};

struct Test
{
    int a;
    int b;
    int c;
};

int main(int argc, char *argv[])
{
    Derived d(1,2,3);
    cout a b c a b c a b 

上述代码中,Derived类对象的内存布局如下:
技术分享图片
Derived类对象从基类继承而来的处成员变量将根据继承的声明顺序进行依次排布。基于赋值兼容原则,如果BaseA类型指针pa、BaseB类型指针pb都指向子类对象d,pa将得到BaseA基类成员变量ma的地址,即子类对象的地址;pb将得到BaseB类成员变量mb的地址;因此,pa与pb的地址不相同。

4、菱形多继承导致的成员冗余

技术分享图片
上述类图中,Teacher类和Student类都会继承People的成员,Doctor会继承Teacher类和Student类的成员,因此Doctor将会有两份继承自顶层父类People的成员。

#include 
#include 

using namespace std;

class People
{
public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    void print()
    {
        cout 

二、虚继承

1、虚继承简介

在多继承中,保存共同基类的多份同名成员,可以在不同的数据成员中分别存放不同的数据,但保留多份数据成员的拷贝,不仅占有较多的存储空间,增加了成员的冗余,还增加了访问的困难。C++提供了虚基类和虚继承机制,实现了在多继承中只保留一份共同成员。
C++对于菱形多继承导致的成员冗余问题的解决方案是使用虚继承。
虚继承中,中间层父类不再关注顶层父类的初始化,最终子类必须直接调用顶层父类的构造函数。
虚继承的语法如下:
class 派生类名:virtual 继承方式 基类名

2、虚继承示例

#include 
#include 

using namespace std;

class People
{
public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    void print()
    {
        cout 

上述代码中,使用虚继承解决了成员冗余的问题。
虚继承解决了多继承产生的数据冗余问题,但是中间层父类不再关心顶层父类的初始化,最终子类必须直接调用顶层父类的构造函数。

三、多继承派生类的对象模型

1、多继承派生类对象的内存布局

技术分享图片
上述类图中,Derived类继承自BaseA和BaseB类,funcA和funcB为虚函数,Derived对象模型如下:
技术分享图片

#include 
#include 

using namespace std;

class BaseA
{
public:
    BaseA(int a)
    {
        m_a = a;
    }
    virtual void funcA()
    {
        cout a b c vptrA vptrB 

2、菱形继承派生类对象的内存布局

菱形继承示例代码如下:

#include 
#include 

using namespace std;

class People
{
public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    void print()
    {
        cout name1 age1 research name2 age2 major subject 

上述代码中,底层子类对象的内存局部如下:
技术分享图片
底层子类对象中,分别继承了中间层父类从顶层父类继承而来的成员变量,因此内存模型中含有两份底层父类的成员变量。
如果顶层父类含有虚函数,中间层父类会分别继承顶层父类的虚函数表指针,因此,底层子类对象内存布局如下:
技术分享图片

#include 
#include 

using namespace std;

class People
{
public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    virtual void print()
    {
        cout vptr1 name1 age1 research vptr2 name2 age2 major subject 

3、虚继承派生类对象的内存布局

虚继承是解决C++多重继承问题的一种手段,虚继承的底层实现原理与C++编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4(8)字节)和虚基类表(不占用类对象的存储空间)(虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
在虚继承情况下,底层子类对象的布局不同于普通继承,需要多出一个指向中间层父类对象的虚基类表指针vbptr。
vbptr是虚基类表指针(virtual base table pointer),vbptr指针指向一个虚基类表(virtual table),虚基类表存储了虚基类相对直接继承类的偏移地址;通过偏移地址可以找到虚基类成员,虚继承不用像普通多继承维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

#include 
#include 

using namespace std;

class People
{
public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    void print()
    {
        cout vbptr_left vbptr_left research vbptr_right vbptr_right major subject name age 

上述代码没有虚函数,在G++编译器打印结果如上,底层子类对象的内存布局如下:
技术分享图片

#include 
#include 

using namespace std;

class People
{
public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    virtual void print()
    {
        cout vbptr_left vbptr_left vbptr_left+8) vbptr_left+16) vbptr_left+24) research vbptr_right major subject vptr_base name age 

上述代码中,使用了虚继承,因此不同的C++编译器实现原理不同。
对于GCC编译器,People对象大小为char + int + 虚函数表指针,Teacher对象大小为char+虚基类表指针+A类型的大小,Student对象大小为char+虚基类表指针+A类型的大小,Doctor对象大小为char + int +虚函数表指针+char+虚基类表指针+char+虚基类表指针+char*。中间层父类共享顶层父类的虚函数表指针,没有自己的虚函数表指针,虚基类指针不共享,因此都有自己独立的虚基类表指针。
VC++、GCC和Clang编译器的实现中,不管是否是虚继承还是有虚函数,其虚基类指针都不共享,都是单独的。对于虚函数表指针,VC++编译器根据是否为虚继承来判断是否在继承关系中共享虚表指针。如果子类是虚继承拥有虚函数父类,且子类有新加的虚函数时,子类中则会新加一个虚函数表指针;GCC编译器和Clang编译器的虚函数表指针在整个继承关系中共享的。
G++编译器对于类的内存分布和虚函数表信息命令如下:

g++ -fdump-class-hierarchy main.cpp
cat main.cpp.002t.class

VC++编译器对于类的内存分布和虚函数表信息命令如下:
cl main.cpp /d1reportSingleClassLayoutX
Clang编译器对于类的内存分布和虚函数表信息命令如下:
clang -Xclang -fdump-record-layouts

4、多继承派生类的虚函数表

所有的虚函数都保存在虚函数表中,多重继承可能产生多个虚函数表。多继承中,当子类对父类的虚函数重写时,子类的函数覆盖父类的函数在对应虚函数表中的虚函数位置;当子类有新的虚函数时,新的虚函数被加到第一个基类的虚函数表的末尾。当dynamic_cast对子类对象进行转换时,子类和第一个基类的地址相同,不需要移动指针,但当dynamic_cast转换子类到其他父类时,需要做相应的指针调整。

四、多继承的指针类型转换

1、多继承中指针类型转换的陷阱

C++语言中,通常对指针进行类型转换,不会改变指针的值,只会改变指针的类型(即改变编译器对该指针指向内存的解释方式),但在C++多重继承中并不成立。

#include 

using namespace std;

class BaseA
{
public:
    BaseA(int value = 0)
    {
        data = value;
    }
    virtual void printA()
    {
        cout 

上述代码中,指向Derived对象的指针转换为基类BaseA和BaseB后,指针值并不相同。dpd指针、bpa指针与bpb指针相差8个字节的地址空间,即BaseA类虚函数表指针与data成员占用的空间。
将一个派生类的指针转换成某一个基类指针,C++编译器会将指针的值偏移到该基类在对象内存中的起始位置。


cout 

上述代码打印出1,C++编译器屏蔽了指针的差异,当C++编译器遇到一个指向派生类的指针和指向其某个基类的指针进行==运算时,会自动将指针做隐式类型提升以屏蔽多重继承带来的指针差异。

2、多继承中派生类、基类指针类型转换

派生类对象指针转换为不同基类对象指针时,C++编译器会按照派生类声明的继承顺序,转换为第一基类时指针不变,以后依次向后偏移前一基类所占字节数。
多继承下,指针类型转换需要考虑this指针调整的问题。

五、多继承应用示例

多继承中,如果中间层父类有两个以上父类实现了虚函数,会造成子类产生多个虚函数表指针,可以使用dynamic_cast关键字作类型转换。
工程实践中通常使用单继承某个类和实现多个接口解决多继承的问题。
代码实例:

#include 
#include 

using namespace std;

class Base
{
protected:
    int mi;
public:
    Base(int i)
    {
        mi = i;
    }
    int getI()
    {
        return mi;
    }
    bool equal(Base* obj)
    {
        return (this == obj);
    }
};

class Interface1
{
public:
    virtual void add(int i) = 0;
    virtual void minus(int i) = 0;
};

class Interface2
{
public:
    virtual void multiply(int i) = 0;
    virtual void divide(int i) = 0;
};

class Derived : public Base, public Interface1, public Interface2
{
public:
    Derived(int i) : Base(i)
    {
    }
    void add(int i)
    {
        mi += i;
    }
    void minus(int i)
    {
        mi -= i;
    }
    void multiply(int i)
    {
        mi *= i;
    }
    void divide(int i)
    {
        if( i != 0 )
        {
            mi /= i;
        }
    }
};

int main()
{
    Derived d(100);
    Derived* p = &d;
    Interface1* pInt1 = &d;
    Interface2* pInt2 = &d;

    cout getI() = " getI() add(10);
    pInt2->divide(11);
    pInt1->minus(5);
    pInt2->multiply(8);

    cout getI() = " getI() equal(dynamic_cast(pInt1)) equal(dynamic_cast(pInt2)) 

在程序设计中最好不要出现多继承,要有也是继承多个作为接口使用抽象类(只声明需要的功能,没有具体的实现)。因为出现一般的多继承本身就是一种不好的面向对象程序设计。

C++语言学习(十六)——多继承

标签:sizeof   函数   内存模型   out   语法规则   因此   eof   虚基类表指针   覆盖   

原文地址:http://blog.51cto.com/9291927/2164576


评论


亲,登录后才可以留言!