C++多继承

文章目录

C++多继承

复杂菱形继承及菱形虚拟继承

单继承、多继承、菱形继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

cpp 复制代码
class Student{
protected:
    int _num;
};

class Teacher{
protected:
    int _id;
};

class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

菱形继承:菱形继承是多继承的一种特殊情况。

cpp 复制代码
class Person {
public:
    string _name;
};

class Student : public Person {
protected:
    int _num;
};

class Teacher : public Person {
protected:
    int _id;
};

class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

单继承在上一章节我们已经了解过了,本章节我们详细了解一下多继承和多继承中的菱形继承。

多继承我们可以认为是你父母生下了你,你继承了你父母的基因;菱形继承就好比你爷爷奶奶的两个孩子结婚生下了你,我们知道正常非近亲结婚生子,孩子一般不会出现问题的而近亲结婚生子这个孩子大概率是有问题的。

而多继承中的菱形继承就相当于我们的近亲结婚生子,为什么呢?

  • 因为菱形继承我们的源头都是同一个,那么我们在继承和静态的时候了解过,static只会实例出一份来,但是正常来说菱形继承的大部分数据不会是static,也就是说它的所有派生类都可以去访问并修改数据,那么在多继承中这份数据应该是每人独一份呢?还是每人共享一份呢?我们看一下下面这个代码就知道了:
cpp 复制代码
class Person {
public:
    string _name;
};

class Student : public Person {
protected:
    int _num;
};

class Teacher : public Person {
protected:
    int _id;
};

class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

int main()
{
    Assistant a1;
    // a1._name = "张三"; // 具有二义性不知道访问谁
    a1.Student::_name = "学生张三";   //指定访问解决了二义性但是没有解决数据冗余的问题
    a1.Teacher::_name = "教师张三";   

    cout << "a1.Student::_name = " << a1.Student::_name << endl;
    cout << "a1.Teacher::_name = " << a1.Teacher::_name << endl;
    cout << "&a1.Student::_name = " << &a1.Student::_name << endl;
    cout << "&a1.Teacher::_name = " << &a1.Teacher::_name << endl;
}

答案是独占,那么这会有什么问题呢?

  • 独占,就像同一句话在不同场景里面表达的意思会有所不同,那么这在我们生活中确实没什么太大影响,但是在计算机中影响就比较大了,因为它只能按语法和内存模型严格判断。独占会带来两个严重问题:
    • 二义性:Assistant 同时继承了 Student 和 Teacher,而这两个基类又都继承自Person。因此 Assistant 对象里有两份 Person 子对象,_name 也就有两份。如果你直接写 a1._name = "张三";,编译器在编译阶段 就不知道你要访问的是 Student 路径上的 _name,还是 Teacher 路径上的 _name,于是直接报错。C++ 提供的解决办法是用作用域限定符显式调用 ,比如 a1.Student::_name = "学生张三";。但这只是绕开了二义性,并没有解决数据重复的问题。
    • 数据冗余:通常某些冗余设计是为了提高程序的容错性 或鲁棒性,但是菱形继承的冗余并非有意设计,而是继承关系导致的重复数据。在多继承中,如果多个派生类都继承同一个基类,且没有使用虚拟继承,那么每个派生类都会包含一份基类子对象。继承层次越深、派生类越多,最底层的派生类就会包含多份基类数据以及自身的成员,造成内存膨胀,管理起来也极易出错。

那么该怎么解决上述问题呢?我们接着来看。

菱形虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和

Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

虚拟继承格式:

cpp 复制代码
class 派生类 : virtual 继承方式 基类
{};
cpp 复制代码
class Person {
public:
    string _name;
};

class Student : virtual public Person {
protected:
    int _num;
};

class Teacher : virtual public Person {
protected:
    int _id;
};

class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

void Test()
{
    Assistant a;
    a._name = "peter";
}

虚拟继承原理

那么虚拟继承是怎么解决二义性和数据冗余的呢?

为了便于大家的理解我们这里使用下面这个例子来演示并通过查看内存的方式来向大家展示一下虚拟继承是如何解决二义性和数据冗余的:

cpp 复制代码
class A 
{ 
public: 
    int _a; 
};

// class B : public A
class B : virtual public A 
{ 
public: 
    int _b; 
};
// class C : public A
class C : virtual public A 
{ 
public: 
    int _c; 
};

class D : public B, public C 
{ 
public: 
    int _d; 
};

int main() {
    D d;
    d.B::_a = 1;
    d.C::_a = 2; 
    d._b = 3;
    d._c = 4;
    d._d = 5;

    return 0;
}

下图是菱形继承的内存对象成员模型:这里可以看到数据冗余

下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。

虚拟继承解决了二义性和数据冗余,根据上面的图片我们知道都是在D的内存中包含的B/C,那么既然都是在D的内存里,为什么B/C还要去找属于自己的A,那么我们看下面这个伪代码是否能为你解惑:

cpp 复制代码
D d;
B b = d;
C c = d;

当D给B/C赋值时是不是要找出B/C成员中的A才能赋值过去?

  • 是的,用 D 对象给 B/C 对象赋值时,需要找出 B/C 成员中的 A 才能正确赋值。为什么呢?
  • 我们可以把D想象是一个大箱子,里面有三个小格子:B 格子、C 格子、A 格子。
    • 但 A 格子只有一个,放在箱子最底下。
    • B 格子里有一张纸条,写着"A 在我下面 20 厘米"。
    • C 格子里也有一张纸条,写着"A 在我下面 10 厘米"。
  • 现在你要把 D 箱子里的 B 格子单独拿出来,复制成一个独立的 B 小箱子。这个 B 小箱子必须包含一个 A 小格子(因为 B 是 A 的儿子)。所以你打开 B 格子,看到纸条,按照"下面 20 厘米"找到了大箱子底部的 A 格子,把它也复制到 B 小箱子里。
  • 同理,复制C格子也是一样的

总结:虚拟继承是为了解决菱形继承的二义性和数据冗余的,它通过虚基表的方式让所有派生类能够找到源头的基类。

下面是上面的Person关系菱形虚拟继承的原理解释:

继承的总结和反思

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。

继承 vs 组合

  • public 继承是一种 is-a 关系:派生类对象是一个基类对象。
  • 组合是一种 has-a 关系:B 组合了 A,B 对象中有一个 A 对象。
  • 继承是白箱复用:基类内部细节对子类可见,破坏封装,基类改变会影响派生类,依赖强、耦合高。
  • 组合是黑箱复用:通过组装对象获得新功能,要求对象有良好接口,内部细节不可见,类间依赖弱、耦合低,有助于保持封装。

实际应尽量多用组合,耦合低、易维护;但适合继承的关系用继承,实现多态也必须继承。能用组合就用组合。

cpp 复制代码
// Car和BMW Car和Benz构成is-a的关系
class Car{
protected:
    string _colour = "白色";   // 颜色
    string _num = "陕ABIT00";  // 车牌号
};

class BMW : public Car{
public:
    void Drive() {cout << "好开-操控" << endl;}
};

class Benz : public Car{
public:
    void Drive() {cout << "好坐-舒适" << endl;}
};

// Tire和car构成has-a的关系

class Tire{
protected:
    string _brand = "Michelin";  // 品牌
    size_t _size = 17;           // 尺寸

};

class Car{
protected:
    string _colour = "白色";     // 颜色
    string _num = "陕ABIT00";    // 车牌号
    Tire _t;                     // 轮胎
};

完

相关推荐
lie..1 小时前
30天从零开始学AI应用开发(Day 19):项目二完结:RAG 知识库问答系统,把自己攒的资料变成私人顾问
开发语言·人工智能·python
SWAGGY..1 小时前
【C++进阶】:(7)红黑树的原理与 C++ 实现:结构设计、插入调整及性质验证
android·java·开发语言·c++·算法
(Charon)2 小时前
【C++面试】堆内存与栈内存:string、vector和对象到底存在哪里
开发语言·c++·面试
谢亮_vipxieliang2 小时前
Go defer、panic与recover核心知识点
开发语言·后端·golang
xlq223222 小时前
全面复习3
c++
迪康Defender2 小时前
终端安全事后追溯能力解析:本地审计模块设计思路与落地实践
运维·开发语言·人工智能·安全·php·安全威胁分析·运维开发
(Charon)2 小时前
【C++面试】迭代器失效是什么?vector、list、map中如何避免迭代器失效
c++·算法·面试
yujunl2 小时前
开发一个可以用APIFOX来调试的接口
开发语言
苏辰澈2 小时前
C++自己的族谱规则:继承详解下
c语言·开发语言·c++·visualstudio