从零开始C++----七.继承相关模型,解析多继承与菱形继承问题(下篇)

系列文章目录

提示:可以学完上篇,再学下篇哦!
从零开始C++----七.继承及相关模型和底层(上篇)


文章目录

系列文章目录

前言

一、多继承及其菱形继承问题

1.继承模型

[1.1 直接继承](#1.1 直接继承)

[1.2 多重继承](#1.2 多重继承)

[1.3 菱形继承](#1.3 菱形继承)

[1.3.1 数据冗余](#1.3.1 数据冗余)

[1.3.2 访问二义性](#1.3.2 访问二义性)

[1.3.3 数据如何传?](#1.3.3 数据如何传?)

菱形继承构造时,最顶层基类(Person)只能构造一次!

[1.4 虚继承](#1.4 虚继承)

二、多继承的空间分布

1.例子a

2.例子b


前言

提示:这里可以添加本文要记录的大概内容:

例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

一、多继承及其菱形继承问题

1.继承模型

继承模型共4类:直接继承(单一继承),多重继承,菱形继承(钻石继承),虚继承(解决菱形继承问题)

接下来足逐一讲解:

1.1 直接继承

单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 基类
class Person {
protected:
    string _name;
public:
    Person(const string& name)
        : _name(name) {}
};

// 派生类:Student 继承 Person
class Student : public Person {
protected:
    int _stuId;
public:
    Student(const string& name, int id)
        : Person(name), _stuId(id) {}
};

// 派生类:PostGraduate 继承 Student(多级继承)
class PostGraduate : public Student {
private:
    string _researchTopic;
public:
    PostGraduate(const string& name, int id, const string& topic)
        : Student(name, id), _researchTopic(topic) {}

    void PrintInfo() {
        cout << "姓名:" << _name 
             << ",学号:" << _stuId 
             << ",研究方向:" << _researchTopic << endl;
    }
};

int main() {
    PostGraduate pg("小明", 2023001, "量子计算");
    pg.PrintInfo();
    return 0;
}

1.2 多重继承

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Student {
protected:
    int _stuId;
public:
    Student(int id) : _stuId(id) {}
    void Study() { cout << "我在上课学习" << endl; }
};

class Teacher {
protected:
    string _course;
public:
    Teacher(const string& course) : _course(course) {}
    void Teach() { cout << "我在教" << _course << endl; }
};

// 多重继承:同时继承 Student 和 Teacher
class Assistant : public Student, public Teacher {
public:
    Assistant(int id, const string& course)
        : Student(id), Teacher(course) {}

    void Work() {
        Study();
        Teach();
        cout << "我是助教,既上课又教课" << endl;
    }
};

int main() {
    Assistant a(2023001, "C++程序设计");
    a.Work();
    return 0;
}

1.3 菱形继承

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
protected:
    string _name;
public:
    Person(const string& name) : _name(name) {}
    void PrintName() { cout << "姓名:" << _name << endl; }
};

// Student 继承 Person
class Student : public Person {
protected:
    int _stuId;
public:
    Student(const string& name, int id)
        : Person(name), _stuId(id) {}
};

// Teacher 继承 Person
class Teacher : public Person {
protected:
    string _course;
public:
    Teacher(const string& name, const string& course)
        : Person(name), _course(course) {}
};

// Assistant 同时继承 Student 和 Teacher(菱形继承)
class Assistant : public Student, public Teacher {
public:
    Assistant(const string& name, int id, const string& course)
        : Student(name, id), Teacher(name, course) {}

    // 二义性问题:直接调用 PrintName() 会报错,必须指定类域
    void PrintInfo() {
        // 错误:PrintName();
        Student::PrintName();  // 必须指定是 Student 的 PrintName
        cout << "学号:" << _stuId << endl;
        cout << "课程:" << _course << endl;
    }
};

int main() {
    Assistant a("小红", 2023001, "数据结构");
    a.PrintInfo();
    return 0;
}

直接继承和多重继承的内存分布很好理解,唯独菱形继承有些难以琢磨,我们来仔细分析一下

图中继承关系很直观:

这是Assistant 类对象的内存结构,它继承自 StudentTeacher,而这两个类又都继承自 Person,这样就会造成两个问题

1.3.1 数据冗余

同样问题也很明显,Assistant 对象里,有两份 _name, 一份来自 Student 继承的 Person,这两份内容完全相同,却占了双倍内存,造成浪费

1.3.2 访问二义性
cpp 复制代码
Assistant a;
cout << a._name; // 编译报错!

以上代码会直接报错,因为编译器不知道你要访问的是 Student 里的 _name,还是 Teacher 里的 _name

而解决的方法就是虚继承

这里在介绍虚继承前,先明白一下,这个是怎么传的

1.3.3 数据如何传?
cpp 复制代码
Assistant a("小红", 2023001, 10086, "数据结构");

① Person(最顶层)

cpp 复制代码
Person(string name) 
    : _name(name) 
{}

② Student

cpp 复制代码
Student(string name, int num) 
    : Person(name), _num(num) 
{}

③ Teacher

cpp 复制代码
Teacher(string name, int id) 
    : Person(name), _id(id) 
{}

④ Assistant(重点!)

cpp 复制代码
Assistant(string name, int num, int id, string major)
    : Person(name),    // 必须直接传给最顶层!
      Student(name, num), 
      Teacher(name, id), 
      _major(major)
{}

它会按下面顺序执行构造

  1. Person ("小红") → 给 name = "小红"

  2. Student ("小红", 2023001) → 给 num = 2023001

  3. Teacher ("小红", 10086) → 给 id = 10086

  4. Assistant 自己 → 给 major = "数据结构"

注意:

菱形继承构造时,最顶层基类(Person)只能构造一次!

  • 虽然 StudentTeacher 都写了 Person(name)
  • 但编译器只会执行一次 Person 构造
  • 必须由 最后的孙子类 Assistant 直接调用

另外,不是说规整的菱形就是菱形继承,以下这图也是菱形继承


1.4 虚继承

有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承

cpp 复制代码
class Person
{
public:
	string _name; // 姓名
	/*int _tel;
	int _age;
	string _gender;
	string _address;*/
	// ...
};

// 使用虚继承Person类
class Student : virtual public Person
{
protected:
	int _num; //学号
};

// 使用虚继承Person类
class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};

// 教授助理
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	// 使用虚继承,可以解决数据冗余和二义性
	Assistant a;
	a._name = "peter";

	return 0;
}

因为 Student 和 Teacher 都用了 virtual 继承 Person 所以这是 虚继承 → 解决菱形继承的数据冗余和二义性,即在继承的基类前面加一个"virtual"

  1. 基类 Person
cpp 复制代码
class Person
{
public:
    string _name; // 姓名
};

最顶层的类,只有一个成员:姓名 _name

  1. Student 虚继承 Person
cpp 复制代码
class Student : virtual public Person
{
protected:
    int _num; //学号
};
  • virtual public Person = 虚继承
  • 作用:告诉编译器 我和 Teacher 共享同一个 Person,不要给我存两份
  1. Teacher 虚继承 Person
cpp 复制代码
class Teacher : virtual public Person
{
protected:
    int _id; // 职工编号
};
  • 和上面一样,也是虚继承
  • 同样共享 Person
  1. Assistant 继承 Student + Teacher
cpp 复制代码
class Assistant : public Student, public Teacher
{
protected:
    string _majorCourse; // 主修课程
};
  • 多重继承
  • 因为父类都是虚继承 ,所以 Assistant 里 只有一份 Person

main 函数里发生了什么?

cpp 复制代码
int main()
{
    Assistant a;

    a._name = "peter";

    return 0;
}

1.如果不用虚继承

a._name 会报错!因为有 两份 _name

  • Student 里有一份
  • Teacher 里有一份编译器不知道你要哪一个!

2.用了虚继承

a._name = "peter" 正确!因为 只有一份 _nameStudent 和 Teacher 共享同一个 Person

总结:在菱形继承中,当中间类(StudentTeacher)都用 virtual 虚继承了顶层基类(Person),那么它们的派生类(Assistant)里,只会共享唯一一份顶层基类的成员,不会出现多份拷贝

如果上面的都理解了,那么下面这一题就能理解:

cpp 复制代码
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{}

	string _name; // 姓名
};

class Student : virtual public Person
{
public:
	Student(const char* name, int num)
		:Person(name)
		,_num(num)
	{}
protected:
	int _num; //学号
};

class Teacher : virtual public Person
{
public:
	Teacher(const char* name, int id)
		:Person(name)
		, _id(id)
	{}
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
public:
	Assistant(const char* name1, const char* name2, const char* name3)
		:Person(name3)
		,Student(name1, 1)
		,Teacher(name2, 2)
	{}
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	// 思考一下这里a对象中_name是"张三", "李四", "王五"中的哪一个?
	Assistant a("张三", "李四", "王五");

	return 0;
}

答案是王五,这里不过多解释

二、多继承的空间分布

1.例子a

我们先用一道题来引入这一点
多继承中指针偏移问题?下⾯说法正确的是( )
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

cpp 复制代码
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };

int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;

	return 0;
}

这一题选"C"

cpp 复制代码
#include <iostream>
using namespace std;

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };

int main()
{
    Derive d;
    Base1* p1 = &d;
    Base2* p2 = &d;
    Derive* p3 = &d;
    cout << "p1(Base1*) 地址: " << p1 << endl;
    cout << "p2(Base2*) 地址: " << p2 << endl;
    cout << "p3(Derive*)地址: " << p3 << endl;

     return 0;
}

我们来看一下运行结果:

  • p3Derive*,指向整个对象的起始地址
  • p1Base1*,指向 DeriveBase1 子对象的地址,也就是整个对象的起始地址
  • p2Base2*,指向 DeriveBase2 子对象的地址,这个地址比对象起始地址偏移了一个 Base1 的大小(sizeof(int)

关键结论:

多重继承中,只有第一个基类的子对象地址和派生类对象的起始地址是相同的,其他基类子对象的地址都会发生偏移

因此这里只要我交换一下继承的顺序,那答案也会发生变化,变成****p2 == p3 != p1

多重继承时,只有排在第一个的基类,它的子对象地址和派生类对象的起始地址相同,其他基类都会因为前面的成员而发生地址偏移。


2.例子b

这张图展示的是 C++ 标准库 I/O 流体系的整体设计 ,它的核心是一个典型的多重 + 菱形继承结构 ,而且还是虚继承,非常经典

我们先来了解一个概念,什么是"抽象"类,"抽象"二字的理解

在 C++ 中,包含至少一个纯虚函数的类,就是抽象类

它的核心特点:

  1. 不能直接实例化对象(不能用它创建变量)
  2. 作用是:定义一套通用的接口(规则),让子类必须去实现这些接口
  3. 通常作为继承体系的基类,用来规范子类的行为

那什么是存虚函数?

格式:virtual 返回值 函数名(参数) = 0;

它表示:这个函数没有具体实现,必须由子类来重写

由于还未讲过重写,这里只提一下,只需先记住格式即可

相关推荐
devil-J2 小时前
vue3+three.js中国3D地图
开发语言·javascript·3d
Xiaoᴗo.2 小时前
C语言2.0---------
c语言·开发语言·数据结构
ghie90902 小时前
MATLAB 解线性方程组的迭代法
开发语言·算法·matlab
进击的荆棘2 小时前
递归、搜索与回溯——二叉树中的深搜
数据结构·c++·算法·leetcode·深度优先·dfs
人道领域2 小时前
【LeetCode刷题日记】:151翻转字符串的单词(两种解法)
java·开发语言·算法·leetcode·面试
XS0301062 小时前
Java 基础(五)值传递
java·开发语言
会编程的土豆2 小时前
【日常做题】栈 中缀前缀后缀
开发语言·数据结构·算法
阿扬ABCD2 小时前
python项目:外星人入侵小游戏
开发语言·python·pygame
进击的荆棘2 小时前
递归、搜索与回溯——回溯
数据结构·c++·算法·leetcode·dfs