C++ 多继承

1. 多继承的概念

定义:一个派生类,同时继承多个基类

语法格式

class 派生类名 : 访问修饰符 基类1, 访问修饰符 基类2, ... 
{
    // 派生类成员
};

2. 多继承时构造/析构顺序

规则:

  • 构造顺序:从左到右,先构造基类,再构造派生类
  • 析构顺序:从右到左,先析构派生类,再析构基类

例子:

#include <iostream>

//阅读能力
class ReadAbility
{
public:
    ReadAbility()
    {
        std::cout << "ReadAbility" << std::endl;
    }

    ~ReadAbility()
    {
        std::cout << "~ReadAbility" << std::endl;
    }
};

//写作能力
class WriteAbility
{
public:
    WriteAbility()
    {
        std::cout << "WriteAbility" << std::endl;
    }

    ~WriteAbility()
    {
        std::cout << "~WriteAbility" << std::endl;
    }
};

//人类具备阅读和写作能力
class Human : public ReadAbility, public WriteAbility
{
public:
    Human()
    {
        std::cout << "Human" << std::endl;
    }

    ~Human()
    {
        std::cout << "~Human" << std::endl;
    }
};

int main()
{
    Human human;
    return 0;
}

运行上面的例子,输出结果如下:

ReadAbility
WriteAbility
Human
~Human
~WriteAbility
~ReadAbility

3. 多继承中的一些陷阱

3.1 二义性问题

class ReadAbility
{
public:
    void executeAbility()
    {
        std::cout << "doRead" << std::endl;
    }
};

class WriteAbility
{
public:
    void executeAbility()
    {
        std::cout << "doWrite" << std::endl;
    }
};

class Human : public ReadAbility, public WriteAbility
{
};

int main()
{
    Human human;

    // human.executeAbility(); 错误,有二义性,2个基类中如果出现同名成员,这样写不知道到底调用哪个
    //=============================================================================
    // 这种写法叫做"作用域解析符调用"或"显式基类成员访问"
    // 使用基类名::成员名的方式来明确指定调用哪个基类的成员函数
    // 用于解决多重继承中的二义性问题
    human.ReadAbility::executeAbility();
    return 0;
}

当两个基类中出现同名成员且都可访问时,派生类对象访问该成员时就会出现二义性问题。它无法确定应该调用哪个基类的成员函数。除非使用作用域解析符调用,否则编译器会报错。

不过在单继承中会出现类似现象,只是它不会引起编译问题,而是默认访问的是自己的成员。例如:

class Base
{
public:
    int a;
};

class Derived : public Base
{
public:
    int a;
};

int main()
{
    Derived d;
    d.a = 1;       // 派生类的a
    d.Base::a = 2; // 显式基类成员访问的写法,访问基类的a
    return 0;
}

3.2 菱形继承问题

所谓菱形继承,是指孙子类以多继承的方式,继承了两个或两个以上的父类,而这2个父类又同时继承了一个祖先类,示意图如下:

        Top
        /  \
       /    \
    Left   Right
       \    /
        \  /
       Bottom

3.2.1 二义性问题

class Top
{
public:
    int a;
};

class Left : public Top
{
};

class Right : public Top
{
};

class Bottom : public Left, public Right
{
public:
    void foo()
    {
        // a = 1; 错误,二义性问题,无法确定是Left继承自基类Top的a还是Right继承自基类Top的a
        
        // 解决方法:使用作用域解析符调用
        Left::a = 1;
        Right::a = 2;
    }
};

二义性引起编译错误,只能通过作用域解析符调用成员加以区分。

3.2.2 内存数据冗余

通过作用域解析符调用成员虽然能够解决编译问题,但是菱形继承还有个致命缺陷,就是继承基类对象冗余。还是上面的例子,如果在Top类的构造中加入调试打印:

class Top
{
public:
    Top()
    {
        std::cout << "Top" << std::endl;
    }
    ~Top()
    {
        std::cout << "~Top" << std::endl;
    }

    int a;
};

在它的孙子类构造/析构时,会分别出现两次构造/析构调用。这是因为在菱形继承的情况下,它的孙子类的内存布局里面会出现两个祖先类对象,分别继承自它的两个父类。虽然依然可以通过作用域解析符调用,避免二义性,但是内存浪费了,而且要小心的使用同一套作用域解析符。

为了解决这个问题,C++引入了虚继承。所谓虚继承,就是如果多个父类拥有共同祖先,并且他们会被一个子类进行多继承,那么在父类继承祖先类时,使用virtual关键字修饰继承。

示例:

#include <iostream>

class Top
{
public:
    int a;
};

class Left : virtual public Top
{
};

class Right : virtual public Top
{
};

class Bottom : public Left, public Right
{
public:
    void foo()
    {
        a = 1;
    }
};

int main()
{
    Bottom b;
    std::cout << &b.Left::a << std::endl;
    std::cout << &b.Right::a << std::endl;
    return 0;
}

运行程序,可以看到a地址相同,也就是他们是同一个,这样Bottom对象里面就只有一个a了,不再有内存数据冗余,也就不再有二义性。那虚继承是怎么实现的呢?它是通过一个叫虚基类表(vbtable)实现的。这里不对详细的实现做深入的阐述,后面文章再深入分析。只要知道它是通过一个指向虚基类表的指针,这个指针指向的内存布局遵守约定的规则,特别是包含一些访问成员的偏移量和基类成员的起始地址等。

4. 虚继承对构造/析构函数顺序的影响

虚继承下的构造/析构顺序:

  • 虚基类(如 Top)由最派生类(如 Bottom)直接初始化,且只构造一次。
  • 顺序可简述为:先虚基类(从左到右),再非虚基类,最后派生类;析构相反。
posted @ 2026-02-25 10:55  thammer  阅读(3)  评论(0)    收藏  举报