每日一个C++知识点|菱形继承

继承是C++面向对象的核心特性之一,说明类与类之间的特性是可以继承的,这大大提高了代码的复用性,优化了程序结构。但是滥用继承也会导致菱形继承的多继承问题。

菱形继承

什么是菱形继承呢?指一个派生类同时继承两个直接基类,这两个直接基类又继承自同一个间接基类,最终形成 "菱形" 的继承结构。

下面用代码展示菱形继承的结构示例:

cpp 复制代码
// 顶层基类
class A {
public:
    int a;
    A(int val) : a(val) {}
};

// 中间基类 B,继承 A
class B : public A {
public:
    B(int val) : A(val) {}
};

// 中间基类 C,继承 A
class C : public A {
public:
    C(int val) : A(val) {}
};

// 最终派生类 D,同时继承 B 和 C
class D : public B, public C {
public:
    // 问题1:初始化 A 时,B 和 C 都会分别初始化 A,导致 A 被初始化两次
    D(int val1, int val2) : B(val1), C(val2) {}
};

int main() {
    D d(1, 2);
    // 问题2:访问 a 时,编译器无法确定是 B::A::a 还是 C::A::a,直接报错
    // cout << d.a << endl; 
    // 必须显式指定,但这违背了"单一继承"的逻辑,且数据冗余(d 中有两个 a)
    cout << d.B::a << endl; // 输出 1
    cout << d.C::a << endl; // 输出 2
    return 0;
}

上述问题中,A为顶级基类,B和C继承A,初始化 A 时,B 和 C 都会分别初始化 A,导致 A 被初始化两次;访问 a 时,编译器无法确定是 B::A::a 还是 C::A::a,直接报错

注:"B::A::a"的含义是有两层:

  1. "A::a"表示 "类 A 中的成员变量 a"

  2. "B::"B 是 A 的派生类

核心问题

菱形继承的核心问题是间接基类的成员会被多次复制,导致数据冗余、二义性,甚至逻辑错误。

数据冗余表现在间接基类 A 的成员因为B和C的缘故会在最终派生类 D 中存在两份,浪费内存;

二义性则表现在直接访问 D 对象的 A 成员时,编译器无法区分是 B 继承的 A 还是 C 继承的 A,就会造成编译报错;

逻辑错误:若 A 有虚函数,多态调用时可能因重复的基类指针导致行为异常。原因是非虚继承的菱形结构中,最终派生类会包含两份 A 的虚指针(vptr),多态调用时无法确定该用哪一个,导致调用结果不符合预期,甚至崩溃。

菱形继承的内存布局如下图所示:

解决方案:虚继承

由于多继承会造成菱形继承问题,那么C++ 提供虚继承 机制就是解决菱形继承的办法。虚继承通过让中间基类(B、C)共享同一个间接基类(A)的实例,从而消除数据冗余和二义性

在中间基类继承顶层基类时,添加 virtual 关键字,用代码举例如下:

cpp 复制代码
// 顶层基类(不变)
class A {
public:
    int a;
    A(int val) : a(val) {}
};

// 中间基类 B:虚继承 A
class B : virtual public A {
public:
    // 虚继承下,B 的构造函数不再直接初始化 A(A 的初始化由最终派生类负责)
    B() {}
};

// 中间基类 C:虚继承 A
class C : virtual public A {
public:
    C() {}
};

// 最终派生类 D:必须直接初始化虚基类 A
class D : public B, public C {
public:
    // 核心:虚基类 A 的构造由最终派生类 D 统一初始化,避免重复
    D(int val) : A(val), B(), C() {}
};

int main() {
    D d(10);
    // 无歧义:d 中只有一份 A::a
    cout << d.a << endl; // 输出 10
    cout << d.B::a << endl; // 仍可显式访问,结果同上
    cout << d.C::a << endl; // 结果同上
    return 0;
}

通过上述代码,我们深入分析虚继承的底层原理,虚继承是通过虚基类表(vbtable) 和虚基类指针(vbptr) 实现的:

  1. 中间基类(B、C)的对象中会增加一个 vbptr 指针,指向虚基类表;

  2. 虚基类表存储当前对象到虚基类(A)实例的偏移量;

  3. 最终派生类(D)中只保留一份 A 的实例,B 和 C 的 vbptr 都指向这同一个实例。

非虚继承的内存布局示意图如下所示: 虚继承的内存布局示意图如下所示:

虽然虚继承可以解决菱形继承的问题,但在现实开发中,为了减少不必要的麻烦,尽量避免使用多继承。

接口多继承的安全场景

若顶层基类是纯虚类,即使是菱形继承结构,也无数据冗余(因为纯虚类无成员变量),此时无需虚继承,用代码举例如下:

cpp 复制代码
// 纯虚接口 A
class A {
public:
    virtual void func() = 0;
    virtual ~A() = default;
};

class B : public A {
public:
    void func() override { cout << "B::func" << endl; }
};

class C : public A {
public:
    void func() override { cout << "C::func" << endl; }
};

class D : public B, public C {
public:
    // 必须重写 func,否则 D 仍是抽象类(解决二义性)
    void func() override { B::func(); }
};

int main() {
    D d;
    d.func(); // 输出 B::func,无歧义
    return 0;
}

总结

菱形继承的核心问题是间接基类成员重复,虚继承通过共享基类实例解决该问题,实际开发中应优先避免多继承。

以上就是本文的所有内容,如果本文对你有帮助的话欢迎点赞收藏哦~

感兴趣的朋友也欢迎关注哟我将会持续输出编程开发的内容

相关推荐
一拳一个呆瓜6 小时前
【STL】文件系统导航
c++·stl
All for pursuit.6 小时前
【动态规划-21】312.戳气球
数据结构·c++·算法·leetcode·动态规划
栈帧之外6 小时前
【C++】STL之set
开发语言·c++
zhanghaha13147 小时前
C++构造函数的一些其他应用(逐词逐句保姆讲解)
c++
jimy18 小时前
c++的 Type Traits (二、更详细版本)
开发语言·c++
高晶9 小时前
ARM 嵌入式 Linux 系统启动流程实战指南
设计模式·程序员
楠风测开9 小时前
MySQL 慢查询 + AI 智能分析
程序员
用户4702784004059 小时前
C语言入门:分支与循环语句详解
程序员
传人9 小时前
macSvn 使用体验:Mac 上的 SVN 版本管理工具,真的比命令行方便吗?
程序员
crawler9 小时前
别再死磕纯技术了:2026 年程序员的 7 条真实出路
程序员