C++面向对象:多重继承
继承格式
class <派生类名> : <继承权限1> <基类名1>, <继承权限2> <基类名2>, … {
    <派生类新定义成员>
};
构造顺序
定义派生类对象时,构造函数的执行顺序和派生类定义时继承的顺序保持一致。
析构顺序
销毁派生类对象时,析构函数的执行顺序和派生类定义时继承的顺序相反。
基类中同名成员
基类中存在同名的变量时,在派生类对象中必须要在变量名前加上作用域才能访问。
class Father {
public:
    std::string name;
    Father () { name = "father"; }
};
class Mother {
public:
    std::string name;
    Mother() { name = "mother"; }
};
class Son : public Mother, public Father { };
int main()
{
    Son son;
    // 编译报错
    std::cout << son.name << std::endl;
    // 正确用法
    std::cout << son.Father::name << std::endl;
    std::cout << son.Mother::name << std::endl;
}
内存布局
在多继承场景中,如果基类中都有虚函数,则派生类中会存在多个虚表指针。
#include <iostream>
#include <string>
class Father {
public:
    virtual void Func1() {}
    virtual void Func2() {}
};
class Mother {
public:
    virtual void Func() {};
};
class Son : public Mother, public Father { };
int main()
{
    Father father;
    std::cout << sizeof(father) << std::endl; // 输出8
    Mother mother;
    std::cout << sizeof(mother) << std::endl; // 输出8
    Son son;
    std::cout << sizeof(son) << std::endl;    // 输出16
}
菱形继承
graph TD
    A[祖父类]
    B[父类1]
    C[父类2]
    D[子类]
    A --> B
    A --> C
    B --> D
    C --> D
菱形继承产生两个问题:
- 数据冗余:子类对象会包含两份祖父类的成员变量。不仅浪费了内存,还会导致数据不一致。
- 二义性:当通过 子类对象访问祖父类的成员时,编译器不知道该走哪条路径:是走父类1继承的那份,还是父类2继承的那份?这会引发编译错误。
为了解决菱形继承带来的问题,C++ 引入了虚拟继承。它告诉编译器,父类1 和 父类2 在继承 祖父类 时,后续的派生类都能共享 祖父类 类的唯一一份副本。
class Grandparent {
public:
    int value;
    Grandparent(int v) : value(v) {}
};
// 使用virtual进行虚拟继承
class Parent1 : virtual public Grandparent {
public:
    Parent1(int v) : Grandparent(v) {}
};
// 使用virtual进行虚拟继承
class Parent2 : virtual public Grandparent {
public:
    Parent2(int v) : Grandparent(v) {}
};
class Child : public Parent1, public Parent2 {
public:
    // 子类的构造函数必须直接负责初始化Grandparent
    Child(int v) : Grandparent(v), Parent1(v), Parent2(v) {}
};
虚拟继承的原理
- 单一实例:通过虚拟继承,祖父类的成员在子类对象中只会存在一份,这解决了数据冗余问题。
- 构造函数的调用:在虚拟继承中,最底层的派生类(子类)必须负责直接调用虚基类(祖父类)的构造函数。- Parent1和- Parent2的构造函数中对- Grandparent的调用会被忽略。
- 如果 Child没有调用Grandparent的构造函数,编译器会报错。
- 确保了 Grandparent在整个继承体系中只会被构造一次。
 
- 内存布局:编译器会在派生类对象(Parent1、Parent2和Child)中添加一个虚拟基类表指针(vbtpr)。用于在运行时找到唯一的祖父类子对象。
int main()
{
    std::cout << sizeof(Grandparent) << std::endl;  // 输出4
    std::cout << sizeof(Parent1) << std::endl;      // 输出16
    std::cout << sizeof(Parent2) << std::endl;      // 输出16
    std::cout << sizeof(Child) << std::endl;        // 输出24
}