每日一个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;
}

总结

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

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

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

相关推荐
.简.简.单.单.2 小时前
Design Patterns In Modern C++ 中文版翻译 第十章 外观模式
c++·设计模式·外观模式
十五年专注C++开发2 小时前
Jieba库: 一个中文分词领域的经典库
c++·分布式·自然语言处理·中文分词
_OP_CHEN2 小时前
【C++数据结构进阶】从 Redis 底层到手写实现!跳表(Skiplist)全解析:手把手带你吃透 O (logN) 查找的神级结构!
数据结构·数据库·c++·redis·面试·力扣·跳表
菜菜的院子2 小时前
vcpkg配置
c++
唐叔在学习3 小时前
Pyinstaller进阶之构建管理大杀器-SPEC文件
后端·python·程序员
我的offer在哪里3 小时前
c++的回调函数
开发语言·c++
Hello eveybody3 小时前
C++四级考试要点
开发语言·c++
夏幻灵3 小时前
obj 文件
c++