惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Visual Studio Blog
大猫的无限游戏
大猫的无限游戏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
腾讯CDC
S
Securelist
Know Your Adversary
Know Your Adversary
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - 【当耐特】
V2EX - 技术
V2EX - 技术
J
Java Code Geeks
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Hugging Face - Blog
Hugging Face - Blog
Cisco Talos Blog
Cisco Talos Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
阮一峰的网络日志
阮一峰的网络日志
美团技术团队
W
WeLiveSecurity
S
SegmentFault 最新的问题
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
宝玉的分享
宝玉的分享
博客园 - Franky
S
Security Affairs
博客园 - 司徒正美
V
V2EX
K
Kaspersky official blog
T
Threatpost
NISL@THU
NISL@THU
博客园 - 叶小钗
Help Net Security
Help Net Security
PCI Perspectives
PCI Perspectives
IT之家
IT之家
小众软件
小众软件
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Last Week in AI
Last Week in AI
Jina AI
Jina AI
爱范儿
爱范儿
罗磊的独立博客
N
News and Events Feed by Topic
博客园 - 三生石上(FineUI控件)
有赞技术团队
有赞技术团队
P
Proofpoint News Feed
L
LINUX DO - 热门话题
Google Online Security Blog
Google Online Security Blog
雷峰网
雷峰网
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint

博客园 - thammer

03 — std::vector 进阶篇 02 — 迭代器基础 01 — std::vector 基础篇 Day 17:Promise 基础 Day 16 - 读懂 TypeScript 声明文件(.d.ts) Day 15 - ArkTS 模块系统 Day 14 - ArkTS 错误处理 Day 13 - ArkTS 类型系统进阶 Day 12 - 泛型与阶段三总结 Day 11 - Interface 与类型契约 Day 10 - 继承与多态 Day 09 - Class 基础 Day 08 - 函数进阶与阶段总结 Day 07 - 函数类型与高阶函数 Day 06 - 函数基础 Day 05 - 数组和元组深入 Day 04 - 循环与迭代 Day 03 - 条件控制 Day 02 - 运算符与表达式 Day 01 - 变量与类型基础 Day 00 - 环境准备 C++虚函数 C++成员访问权限以及继承权限 C++ 成员初始化机制总结笔记
C++ 多继承
thammer · 2026-02-25 · via 博客园 - thammer

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)直接初始化,且只构造一次。
  • 顺序可简述为:先虚基类(从左到右),再非虚基类,最后派生类;析构相反。