欢迎光临
我们一直在努力

C++多继承、菱形继承与虚继承

一、 C++多继承基础语法与同名成员处理

1.1 多继承语法

C++允许一个类继承多个类。

class Base1 { public: int m_A; };
class Base2 { public: int m_B; };
// 多继承语法:class 子类 : 继承方式 父类1, 继承方式 父类2 …
class Son : public Base1, public Base2 { public: int m_C; };

1.2 同名成员引发的二义性

如果父类 Base1 和 Base2 中存在同名成员(例如都有 int m_A;),子类 Son 访问该成员时,编译器无法确定是哪个父类的成员,就会产生二义性(Ambiguity)。

  • 解决方式:访问时必须加上作用域限定符 ::。

Son s;
// s.m_A = 100; // 报错:引用不明确(二义性)
s.Base1::m_A = 100; // 成功,指定访问 Base1 的 m_A
s.Base2::m_A = 200; // 成功,指定访问 Base2 的 m_A

1.3 同名成员函数的隐藏

如果子类出现了和父类同名的成员函数,子类中的同名函数会隐藏掉父类中所有的同名成员函数(包括重载版本)

class Base {
public:
void func() { cout << "Base func" << endl; }
void func(int a) { cout << "Base func(int)" << endl; }
};
class Son : public Base {
public:
void func() { cout << "Son func" << endl; }
};
// 调用
Son s;
s.func(); // 调用子类的 func()
// s.func(100); // 报错!父类带参的 func(int) 被隐藏了
s.Base::func(100); // 成功,必须加作用域

补充:静态成员(static)遇到同名情况,处理方式与非静态成员完全一致。

1.4 继承方式对权限的影响

  • 公有继承 (public):父类成员权限保持不变。

  • 保护继承 (protected):父类中 public 和 protected 成员到子类中全变为 protected。

  • 私有继承 (private):父类中 public 和 protected 成员到子类中全变为 private。

  • 注意:无论哪种继承方式,父类中的 private 成员子类始终无法访问(编译器隐藏了,但实际上确实被继承了下来,占用内存)。

二、 继承中的对象模型与构造析构顺序

2.1 构造与析构顺序

  • 构造顺序:先调用父类构造函数,再调用子类构造函数。

  • 析构顺序:与构造相反,先调用子类析构函数,再调用父类析构函数。

2.2 查看对象模型

可以使用VS自带的开发人员命令提示工具查看类的内存布局:

cl /d1 reportSingleClassLayout类名 文件名.cpp

通过查看对象模型可知,父类中所有的非静态成员属性都会被继承到子类中。子类对象的大小(sizeof)通常包含了所有父类非静态数据成员加上子类自身数据成员的总和(注意内存对齐)。

三、 菱形继承(钻石继承)与虚继承

3.1 什么是菱形继承?

两个派生类继承同一个基类,同时又有某个类同时继承这两个派生类。形状如同钻石。

       Animal (m_Age)
       /          \\
   Sheep        Tuotuo
 (m_Age)       (m_Age)
       \\          /
        SheepTuo

引发的问题:

  • 数据冗余:SheepTuo 继承了 Sheep 和 Tuotuo,而它们各自有一份 Animal 的数据,导致 SheepTuo 中保存了两份 m_Age。

  • 二义性:访问 st.m_Age 时,编译器不知道取哪一份。

  • 3.2 虚继承(virtual inheritance)

    利用虚继承可以解决菱形继承问题。在继承前加上 virtual 关键字,此时 Animal 被称为虚基类。

    class Animal { public: int m_Age; };
    class Sheep : virtual public Animal {}; // 虚继承
    class Tuotuo : virtual public Animal {}; // 虚继承
    class SheepTuo : public Sheep, public Tuotuo {};

    注意:要解决菱形继承的二义性,所有通向公共基类的路径都必须使用虚继承。如果只有 Sheep 虚继承,而 Tuotuo 普通继承,编译器依然无法合并数据,依旧存在两份 m_Age,直接访问会产生二义性。

    3.3 虚继承的底层实现(vbptr 与 vbtable)

    虚继承底层依赖:

    • vbptr (Virtual Base Pointer):虚基类指针。含有虚基类的对象中会多出一个指针(通常位于对象内存布局的最顶部或尾部),指向虚基类表。

    • vbtable (Virtual Base Table):虚基类表。记录了当前类到虚基类的偏移量。

    • 访问虚基类成员时,需要通过 vbptr 查表计算地址,会有轻微的性能开销。

    四、 虚继承下的初始化规则(最派生类负责制)

    这是虚继承中最容易踩坑的地方:在虚继承中,虚基类的初始化由“最终派生类(Most Derived Class)”全权负责。中间类构造函数中对虚基类的初始化请求会被编译器强制忽略。

    场景演示:

    class Animal {
    public:
    int mAge;
    Animal(int age) : mAge(age) { cout << "Animal构造, age=" << age << endl; } // 没有默认构造
    };

    class Sheep : virtual public Animal {
    public:
    Sheep() : Animal(18) { cout << "Sheep构造" << endl; } // 被忽略!
    };

    class Tuotuo : virtual public Animal {
    public:
    Tuotuo() : Animal(28) { cout << "Tuotuo构造" << endl; } // 被忽略!
    };

    class Student : public Sheep, public Tuotuo {
    public:
    // 【核心】必须显式初始化虚基类 Animal
    // 传入的值将决定最终的 mAge
    Student() : Animal(99), Sheep(), Tuotuo() {
    cout << "Student构造, 最终 mAge=" << mAge << endl;
    }
    };

    运行结果分析:

  • 构造顺序:虚基类优先初始化 -> 然后按声明顺序初始化非虚基类。

  • 首先执行 Student 构造函数中的 Animal(99),此时 mAge 被初始化为 99。

  • 接着执行 Sheep(),内部尝试 Animal(18),但发现 Animal 已经被最派生类 Student 初始化了,编译器直接跳过。

  • 接着执行 Tuotuo(),内部尝试 Animal(28),同样跳过。

  • 最终 Student 对象中的 mAge 为 99。

  • 关于“如果两个路径赋不同的值,子类继承哪一个”的解答:

    都不会继承! 子类中的 mAge 值完全取决于最派生类(Student)构造函数中显式传递的参数。

    • 如果最派生类没有显式初始化会怎样?

      • 如果虚基类 Animal 有默认构造函数,则使用默认值(如 0)。

      • 如果虚基类 Animal 没有默认构造函数(只提供了带参构造),则代码编译报错。提示 constructor for 'Student' must explicitly initialize the base class 'Animal' which does not have a default constructor。

    补充:在虚继承机制下修改共享数据

    因为只有一份 m_Age 数据,所以以下操作修改的是同一块内存:

    Student st;
    st.Sheep::mAge = 18; // 给共享的 mAge 赋值为 18
    st.Tuotuo::mAge = 28; // 给同一个 mAge 赋值为 28
    // 由于操作的是同一份数据,后一次赋值会覆盖前一次,最终 st.mAge == 28

    五、 建议总结

  • 实际开发不建议使用多继承。多继承会让代码结构极其复杂,容易引发二义性和内存布局混乱。

  • 谨慎使用虚继承。虚继承虽然解决了数据冗余和二义性,但付出了性能代价(多了一个 vbptr 指针的空间开销,以及运行时通过查表间接寻址的时间开销)。且虚基类的初始化规则较难把控。

  • 现代C++设计原则:优先使用组合(Composition)或接口抽象(Interface)来代替多重继承。如果确实需要多继承,建议只继承纯虚函数的接口类(类似于Java的interface),避免继承带有数据成员的实体类。

    • 赞(0)
      未经允许不得转载:171主机测评 » C++多继承、菱形继承与虚继承
      分享到: 更多 (0)

      评论 抢沙发

      • 昵称 (必填)
      • 邮箱 (必填)
      • 网址