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

总结

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

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

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

相关推荐
小欣加油13 小时前
leetcode1926 迷宫中离入口最近的出口
数据结构·c++·算法·leetcode·职场和发展
星恒随风13 小时前
C++ 类和对象入门(五):初始化列表、explicit 和 static 成员详解
开发语言·c++·笔记·学习·状态模式
浪客灿心14 小时前
项目篇:模块设计与实现
数据库·c++
牛油果子哥q14 小时前
【C++ STL vector】C++ STL vector 终极精讲:动态数组底层原理、两倍扩容机制、迭代器失效、增删查改、性能剖析与工程避坑指南
开发语言·c++
为何创造硅基生物16 小时前
独占指针的创建std::make_unique 本身自带堆出现
c++
kyle~16 小时前
ROS 2 与 Isaac Sim 联合仿真(一)体系架构、环境选型与基础通信闭环
c++·机器人·nvidia·仿真·ros2
努力努力再努力wz17 小时前
【内存管理与高并发内存池系列】从 mmap 到 malloc:文件映射、匿名映射与 glibc 内存分配机制详解
linux·c语言·数据结构·数据库·c++·qt·链表
八解毒剂17 小时前
数据结构-平衡二叉树——对二叉搜索树的优化
数据结构·c++·算法
起床困难户57517 小时前
条款20:协助完成返回值优化
c++
啦啦啦啦啦zzzz18 小时前
算法总结(二分查找、双指针)
c++·算法