每日一个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::"BA 的派生类

核心问题

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

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

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

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

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

解决方案:虚继承

由于多继承会造成菱形继承问题,那么C++ 提供虚继承 机制就是解决菱形继承的办法。虚继承通过让中间基类(BC)共享同一个间接基类(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. 中间基类(BC)的对象中会增加一个 vbptr 指针,指向虚基类表;

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

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

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

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

接口多继承的安全场景

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

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;
}

总结

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

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

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

相关推荐
信奥卷王4 分钟前
2025年9月GESPC++四级真题解析(含视频)
数据结构·c++·算法
朔北之忘 Clancy6 分钟前
第一章 顺序结构程序设计(2)
c++·算法·青少年编程·竞赛·教材·考级·讲义
Ccjf酷儿13 分钟前
C++语言程序设计 (郑莉)第十一章 流类库与输入/输出
开发语言·c++
CSDN_RTKLIB16 分钟前
【字符编码】constexpr、char[]赋值汉字
c++
啟明起鸣21 分钟前
【C++ 面向对象编程】补档:线程池和 MySQL 连接池的设计模式分析
开发语言·c++·mysql
郝学胜-神的一滴38 分钟前
使用QVideoWidget实现高效视频播放:从基础到高级应用
开发语言·c++·qt·程序人生·音视频
Cher ~39 分钟前
【数据结构】stl 容器
开发语言·数据结构·c++
IT永勇39 分钟前
c++设计模式-观察者模式
c++·观察者模式·设计模式
余衫马1 小时前
Qt for Python:PySide6 入门指南
开发语言·c++·python·qt
C+++Python1 小时前
C++分布式语音识别
c++·分布式·语音识别