继承基础知识中主要通过单继承来介绍继承的概念与使用,而这篇文章将具体介绍C++另一种继承方式:多继承。

不同于单继承一个子类只能直接继承自一个父类,多继承允许一个子类同时直接继承多个父类,多继承中子类会获得所有父类的属性和方法,也能添加自身特有内容或重写方法,灵活整合多个父类。

一.多继承介绍

多继承的出现

众所周知,C++是面向对象的编程,当单继承被设计出之后,自然而然就会想“一个对象不一定只会有一种对象的属性啊”,就像蝙蝠既有“飞行动物”属性又有“哺乳动物”属性,那么,多继承这种“更加贴近实际生活”的方式显然更加符合“面向对象”的需求。

于是出现以下这段代码:

// 父类1:哺乳动物
class Mammal 
{
protected:
    string name;
public:
    Mammal(string n) : name(n) {}
    void breastFeed()
    {
        cout << name << "用乳汁喂养幼崽" << endl;
    }
};

// 父类2:飞行动物
class FlyingAnimal 
{
protected:
    string name;  // 与Mammal存在同名成员
public:
    FlyingAnimal(string n) : name(n) {}
    void fly() 
    {
        cout << name << "在空中飞行" << endl;
    }
};

// 子类:蝙蝠   (多继承继承方式)
class Bat : public Mammal, public FlyingAnimal
{
public:
    // 初始化列表需显式调用两个父类的构造函数
    Bat(string n) : Mammal(n), FlyingAnimal(n) {}
    
    // 特有方法
    void hang() 
    {   // 显式指定父类成员
        cout << Mammal::name << "倒挂在洞穴里休息" << endl;  
    }
};

int main() 
{
    Bat bat("蝙蝠");
    bat.breastFeed();  // 调用Mammal的方法
    bat.fly();         // 调用FlyingAnimal的方法
    bat.hang();        // 调用自身方法
    return 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.

设计之初,子类同时获得了所有父类的属性和方法确实很好的与实际生活相对应,但多继承渐渐引发了更多问题......

class Person
{
public:
	int _age;
};

class Student: public Person //学生继承了人的属性
{
public:
protected:
	int _sid;//学生学号
};

class Teacher :public Person//老师继承了人的属性
{
public:
	int _id;//教师工号
};
// 助教:即是学生,又是老师
class Assistant : public Student, public Teacher
{
public:
	string _major;
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

当尝试用Assistant类创建一个对象as :

多继承详细介绍_组合

as里包含了所有父类成员没问题,但我们发现,里面包含了两份Person类的(分别由Student和Teacher继承而来),这是否是我们需要的?

当我们访问Person成员(_age)时,该访问Student的,还是Teacher的?

对于一个人而言,他的年龄是确定的,我们是否有必要储存两份相同的数据?

这就是多继承引发的问题之一:出现菱形继承从而导致数据冗余与二义性

二.菱形继承

1.概念:

菱形继承子类继承的两个父类,共同源于同一个基类,形成菱形结构,导致基类成员被多次继承,引发访问歧义。

多继承详细介绍_虚继承_02

通俗来说,类似这种二次/多次继承了同一个类的结构被称为菱形继承。

2.菱形继承产生问题

像上面提出的疑问,当我们在尝试访问重复父类的成员_age时,会出现这种现象:

多继承详细介绍_多继承_03

_age分别被Student与Teacher部分重复储存,且在访问_age时不得不明确指明类域,这就是数据冗余与二义性的含义:

数据冗余:指多次存储相同数据

二义性:访问不明确

多继承详细介绍_组合_04

为了解决这一问题,C++官方给出一种方法:虚继承

3.问题解决:虚继承的应用

关键字:virtual

class A{ public: int _a=0;};
class B :virtual public A{ public: int _b = 0;};//虚继承方式virtual
class C :virtual public A{ public: int _c = 0; };//虚继承方式virtual
class D :public B,public C{ public: int _d = 0;};
  • 1.
  • 2.
  • 3.
  • 4.

虚继承能够使“多个中间类在同一个最终对象里共享基类”;当B,C通过virtual继承A,B,C拥有的不再是A的“副本”,而是一个“指向A”的指针,使得当D继承B,C时它们能够共用同一份A。

也就是说:virtual使得A在被继承时不再是“副本”,而是获取能够能指向A的指针,当(B,C)类同时被再次继承时,共享A的数据更准确地来讲,虚继承让派生类(B,C)不再直接包含基类A的完整副本,而通过“间接引用“的方式关联到A。

多继承详细介绍_虚继承_05

d中只有一份A类成员,一改全改,可通过d直接访问。

4.虚继承原理

那么“共享“是如何实现的?A在D中是怎样储存的?当B/C切片时,是如何得到A的部分?

解决这几个问题,就需要更细致的了解虚继承的原理。

我们通过&d查看在内存中d成员储存分布

多继承详细介绍_虚函数表_06

A被放在整个类最下边,B,C中没有C的内容,反而多了两个指针?再根据指针查找:

多继承详细介绍_多继承_07

两个地址分别指向两个16进制数字:14(20),0c(12),这两个代表什么?

多继承详细介绍_多继承_08

在观察d内存分布,发现B到A地址差为14(十六进制),C到A恰好是0c(十六进制),那么B,C储存的两个指针指向的两个值是否是B,C距离A的相对偏移值

确实如此。

B,C中两个指针指向各自的虚基表(存储14和0c的图),虚基表中存储着B,C相对A的偏移量。

虚基表(virtual base table)是虚继承中用于定位共享基类实例的特殊表格:

- 由谁持有:虚继承基类的派生类(如菱形继承中的B、C)会包含一个“虚基指针”,指向自身的虚基表。

- 存储内容:主要记录从当前类(B或C)到共享基类(A)实例的偏移量(位置距离)。

- 作用:当最终派生类(D)创建时,B和C通过各自的虚基指针找到虚基表,利用偏移量定位到D中唯一的A实例,避免多份基类副本的混乱。

以这种方式不仅能实现数据“共享”,避免出现多个A“副本”问题,而且在切割/切片时,通过偏移量可以直接找到A部分,灵活适应对象布局。

5.虚继承总结:

虚继承是C++解决“菱形继承”问题的机制,总结:

- 问题:多继承中,派生类可能间接多次继承同一基类,导致基类成员冗余和访问二义性。

- 解决:中间派生类继承基类时加 virtual ,使最终派生类仅保留一份基类实例。

- 实现:通过虚基指针指向虚基表,表中存储偏移量,定位共享的基类实例。

- 作用:消除数据冗余,解决访问歧义,让复杂多继承结构合理运行。

多继承详细介绍_多继承_09

三.多继承总结

多继承是指一个类同时继承多个基类的机制,核心总结:

- 功能:让派生类同时拥有多个基类的成员和行为,增强代码复用。

- 问题:可能出现“菱形继承”(同一基类被间接多次继承),导致成员冗余和访问二义性。(一般不建议使用菱形继承)

- 应对:通过虚继承(中间类加 virtual )解决菱形继承问题,确保基类实例唯一。

- 注意:过度使用会增加代码复杂度,需谨慎设计继承层次。

四.扩展概念--组合

“高内聚,低耦合”是软件设计的核心原则,简单来说就是一个模块要专注于完成单一任务,而模块之间的依赖应该尽可能减少且简单,避免过度关联。目的是让系统更易维护扩展,一方修改对其它地方影响小。

对于继承来说,派生类以基类紧密相连,基类的修改会对派生类产生很大影响,这也一定程度破坏了派生类的封装;

组合,是另一种让B类“继承”A类的方法:

class A
{public: int _a;};
class B
{
public:
	//组合
	A a;//通过在B类中创建A对象,复用到A的功能
	int _b;
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

由于直接使用A的接口,组合类之间没有很深的依赖关系,耦合度更低,更加符合软件设计核心;

有一种被广泛认可的说法:

  • 继承:是一种“is a”的关系,如:狗is a 动物,派生类(狗)继承基类(动物)属性行为。
  • 组合:是一种“has a”的关系,如:狗has a 尾巴,狗类包含尾巴类,强调部分与整体的关联

根据上述规则合理使用继承/组合,当继承和组合都可用时,建议优先使用组合。