【c++】面向对象三大特性之——继承(菱形继承详细讲解)

【c++】面向对象三大特性之——继承(菱形继承详细讲解)
小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
c++系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!

目录


前言

【c++】模板进阶,模板的分离编译重点讲解——书接上文 详情请点击<——
本文由小编为大家介绍——【c++】面向对象三大特性之——继承

一、继承的概念及定义

继承的概念

继承机制是面向对象程序设计使代码可以复用最重要的手段。它允许程序员在保持原有类特性的基础上进行扩展,增加功能,产生新的类,这个新的类就叫做派生类。继承体现了面向对象程序设计的层次结构,体现了由简单到复杂的过程。以前我们接触的复用都是函数复用(函数调用,模板的使用)。现在的继承是类设计层次的复用继承的思想是属性相同,例如有两个类分别为学生和老师,他们都是人,如果单独进行设计都要设计姓名,年龄,身高等,那么为了类设计层次的复用,把这些相同的属性抽象为一个类名为人,那么学生和老师就可以继承人的属性,例如姓名,年龄,身高等,从而进行类设计层次的复用并且继承后,人的成员(成员变量和成员函数)都会变成学生和老师成员的一部分
classperson{public:voidPrint(){ cout << _age << endl; cout << _name << endl;}protected:int _age =18; string _name ="xiaowang";};classstudent:publicperson{protected:int _stuid =111111;};classteacher:publicperson{protected:int _teaid =222222;};intmain(){ student s; teacher t; s.Print(); t.Print();return0;}
运行结果如下体现了成员函数的复用


2. 调用监视窗口,可以体现成员变量的复用

继承定义

定义格式
person是父类,又称基类。student是子类,又称派生类继承方式分为public继承,protected继承,private继承继承使用方法:class 子类名 :继承方式 父类名
//派生类 :继承方式 基类classstudent:publicperson{protected:int _stuid =111111;};
继承方式和访问限定符
继承基类访问方式的变化
基类成员\继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见
  1. 基类的private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象,但是语法上限制派生类对象无论是在类里面还是在类外面都不可以访问基类的私有成员。这个基类的private成员跟派生类中原有的private成员不同,派生类原有的private成员在类里面可以访问,在类外不可访问
  2. 由于基类的private成员在派生类中是不能访问的,所以如果基类的成员不想在派生类继承后在派生类的类外面直接访问,但可以在派生类的类里面进行访问,那么就将基类的成员定义为protected成员(protected成员在基类中的里面可以访问,在基类的外面不可以访问)。由此可见protected保护成员限定符是因为继承才出现的
  3. 总结一下表格,在基类中的private私有成员无论是以什么方式被派生类继承,其在派生类中都是不可见的,基类其它成员在派生类的访问方式是取后面两个的关键字权限小的那一个(成员在基类的访问限定符,继承方式),关键字权限大小关系:public>protected>private
  4. 如果不加继承方式,那么使用关键字class时,默认继承方式是private继承。使用关键字struct时,默认继承方式是public继承。不过在实际的使用中最好显示写出继承方式
  5. 在实际的使用场景中,使用最多的是public继承,而protected/private继承的使用场景非常少。同时我们同样不建议使用protected/private继承,因为protected/private继承进行继承下来的成员,只能在派生类的类里面进行使用,可维护性不强
派生类进行公有继承
classperson{public:voidPrint(){ cout << _age << endl; cout << _name << endl;}protected:int _age =18;private: string _name ="xiaowang";};classstudent:publicperson{public:voidfun(){ _age =20;Print();}protected:int _stuid =111111;};intmain(){ student s; s.fun(); s.Print();return0;}
运行结果如下基类的public成员可以在派生类的里面和外面进行调用基类的protected成员只能在派生类里面进行访问,在派生类外面不能进行访问基类的private成员在派生类的里面和外面都不可以进行访问

派生类进行保护继承或私有继承
classperson{public:voidPrint(){ cout << _age << endl; cout << _name << endl;}protected:int _age =18;private: string _name ="xiaowang";};//class student :private personclassstudent:protectedperson{public:voidfun(){ _age =21;Print();}protected:int _stuid =111111;};intmain(){ student s; s.fun();return0;}
运行结果如下基类的public成员在派生类里面可以进行访问,在派生类外面不可以进行访问基类的protected成员在派生类里面可以进行访问,在派生类外面不可以进行访问基类的private成员在派生类的里面和外面都不可以进行访问

二、基类和派生类对象的赋值转换

  1. 派生类对象可以赋给基类的对象/基类的指针/基类的引用。这里实际上是赋值兼容,发生了切片,也叫做切割。可以把派生类对象理解为两部分,基类的那一部分和派生类自己的那一部分。切片或切割也就是将派生类对象中的基类的那一部分切割出来进行赋值
  2. 基类对象不可以赋值给派生类对象

基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用。因为基类的指针或引用是有可能指向派生类的父类的那一部分。所以这种方式必须是基类的指针或引用指向派生类对象的时候才最安全。关于这种方式小编不进行代码演示,在后文的多态文章中,小编会进行补充讲解

在这里插入图片描述

我们已知的当类型不兼容的赋值方式可以进行强制类型转换或隐式类型转换,但是这里是一个特例由于发生了赋值兼容转换,所以切割或切片不产生消耗,即不会产生临时变量

在这里插入图片描述
classperson{public:int _age =18; string _name ="xiaowang";};classstudent:publicperson{protected:int _stuid =111111;};intmain(){ person p; student s; s._age =20; s._name ="zhangshan"; p = s; person* ptr =&s; person& rp = s;return0;}
运行结果如下赋值前的情况


2. 赋值成功,赋值后的情况

三、继承中的作用域

  1. 在继承体系中的基类和派生类都有其独立的作用域
  2. 当派生类和基类中有同名成员时,派生类中的同名成员将屏蔽基类中的同名成员,直接对当前派生类的同名成员进行访问,这种情况叫做隐藏,也叫做重定义(在派生类的成员函数中,可以使用 基类::同名成员 的形式直接访问基类的同名成员)
  3. 注意,如果是派生类和基类中的成员函数构成隐藏的话,只要函数名相同就构成隐藏
  4. 但是注意,在实际的继承体系中,最好不要定义同名成员
classperson{public:voidfun(){ cout <<"基类:"<< _age << endl;}protected:int _age =18; string _name ="xiaowang";};classstudent:publicperson{public:voidfun(){ person::fun(); cout <<"派生类:"<< _age << endl << endl; _age =200; person::fun(); cout <<"派生类:"<< _age << endl << endl; person::_age =180; person::fun(); cout <<"派生类:"<< _age << endl;}protected:int _age =20;};intmain(){ student s; s.fun();return0;}
测试结果如下

四、派生类的默认成员函数

6个默认成员函数,当我们不写的时候,编译器会默认帮我们生成这六个成员函数,那么在派生类中,这几个成员函数是如何生成的呢?
  1. 派生类的构造函数在走初始化列表时会优先自动调用基类的默认构造函数完成基类那一部分成员的初始化,如果基类没有默认成员函数,那么在派生类的构造函数中必须显示调用基类的构造函数
classPerson{public:Person(constchar* name ="peter"):_name(name){ cout <<"Person()"<< endl;}protected: string _name;};classStudent:publicPerson{public:Student(constchar* name,int id):_id(id){ cout <<"Student()"<< endl;}protected:int _id;};intmain(){ Student s("zhangshan",111111);return0;}
运行结果如下基类有默认构造函数的情况下,定义派生类对象,调用派生类的构造函数时,编译器走派生类的初始化列表前会优先调用基类的默认构造函数完成基类那一部分成员的初始化,调用完基类的默认构造函数后再对自身的成员变量进行初始化

classPerson{public:Person(constchar* name):_name(name){ cout <<"Person()"<< endl;}protected: string _name;};classStudent:publicPerson{public:Student(constchar* name,int id):Person(name),_id(id){ cout <<"Student()"<< endl;}protected:int _id;};intmain(){ Student s("zhangshan",111111);return0;}
运行结果如下当基类没有默认构造函数的时候,定义派生类对象,调用派生类的构造函数,编译器在走初始化列表时,编译器只能够去调用基类的默认构造函数,当基类没有默认构造函数的时候,这时候需要我们显示在初始化列表像定义匿名对象一样,传入参数进行显示调用基类的构造函数完成基类那部分成员的初始化,显示调用完基类的构造函数后再对自身的成员变量进行初始化

  1. 派生类的拷贝构造函数必须显示调用基类的拷贝构造函数完成基类的拷贝初始化,因为拷贝构造函数也属于构造函数,如果没有显示调用,那么编译器就会去调用默认构造函数去完成对基类那部分的初始化,这时候基类中的数据不是我们原想要进行拷贝的数据,而是初始化的数据,所以我们必须要显示调用基类的拷贝构造函数
classPerson{public:Person(constchar* name ="peter"):_name(name){ cout <<"Person()"<< endl;}Person(const Person& p):_name(p._name){ cout <<"Person(const Person& p)"<< endl;}protected: string _name;};classStudent:publicPerson{public:Student(constchar* name,int id):Person(name),_id(id){ cout <<"Student()"<< endl;}Student(const Student& s):Person(s)//实际上是将派生类对象赋值给基类对象的引用,发生了赋值兼容转换,即切片和切割,_id(s._id){ cout <<"Student(const Student& s)"<< endl;}protected:int _id;};intmain(){ Student s("zhangshan",111111); Student s1(s);return0;}
运行结果如下

  1. 派生类的operator=必须要显示调用基类的operator=完成对基类的赋值
classPerson{public:Person(constchar* name ="peter"):_name(name){ cout <<"Person()"<< endl;}Person(const Person& p):_name(p._name){ cout <<"Person(const Person& p)"<< endl;} Person&operator=(const Person& p){ cout <<"Person operator=(const Person& p)"<< endl;if(this!=&p) _name = p._name;return*this;}protected: string _name;};classStudent:publicPerson{public:Student(constchar* name,int id):Person(name),_id(id){ cout <<"Student()"<< endl;}Student(const Student& s):Person(s),_id(s._id){ cout <<"Student(const Student& s)"<< endl;} Student&operator=(const Student& s){ cout <<"Student& operator=(const Student& s)"<< endl;if(this!=&s){ Person::operator=(s);//这里必须使用基类::operator=的方式进行调用 _id = s._id;//如果直接调用operaotr=会产生一直去调用派生类的}//operator=引发无穷递归调用,导致栈溢出return*this;}protected:int _id;};intmain(){ Student s("zhangshan",111111); Student s1(s); Student s2("lisi",222222); s1 = s2;return0;}
运行结果如下

  1. 派生类的析构函数会在被调用完成之后自动调用基类的析构函数完成对基类成员的清理。因为这样才能保证派生类先清理派生类的自己成员的那一部分,再清理派生类中的基类成员的那一部分的顺序。同时再派生类清理自己成员的那一部分的时候,有可能访问了基类成员,所以在对派类清理自己成员的时候不可以显示调用基类的析构函数完成派生类中的基类那一部分的成员的清理
  2. 派生类的析构函数会在被调用完成之后自动调用基类的析构函数这种方式有效的保证了在栈上先定义后析构,后定义,先析构的顺序,在定义派生类成员的时候,先调用派生类中基类那一部分进行初始化,在去完成对派生类自己那一部分的初始化,之后生命周期结束,派生类自己那一部分由于是后定义的,所以先调用派生类的析构函数进行析构,派生类中的基类那一部分由于是先定义的,所以后析构,派生类那一部分析构完成,编译器自动调用基类的析构函数完成派生类中基类那一部分成员的清理
  3. 由于后续的一些场景,析构函数需要构成重写,重写的条件之一是函数名相同(小编后续文章会进行讲解),所以那么编译器会对析构函数的函数名进行重写,那么会统一修饰为destructor(),所以父类析构函数不加virtual的情况下,子类的析构函数和父类析构函数构成隐藏
classPerson{public:Person(constchar* name ="peter"):_name(name){ cout <<"Person()"<< endl;}Person(const Person& p):_name(p._name){ cout <<"Person(const Person& p)"<< endl;} Person&operator=(const Person& p){ cout <<"Person operator=(const Person& p)"<< endl;if(this!=&p) _name = p._name;return*this;}~Person(){ cout <<"~Person()"<< endl;}protected: string _name;};classStudent:publicPerson{public:Student(constchar* name,int id):Person(name),_id(id){ cout <<"Student()"<< endl;}Student(const Student& s):Person(s),_id(s._id){ cout <<"Student(const Student& s)"<< endl;} Student&operator=(const Student& s){ cout <<"Student& operator=(const Student& s)"<< endl;if(this!=&s){ Person::operator=(s); _id = s._id;}return*this;}~Student(){ cout <<"~Student()"<< endl;}protected:int _id;};intmain(){ Student s("zhangshan",111111); Student s1(s); Student s2("lisi",222222); s1 = s2;return0;}
运行结果如下

五、继承与友元

  1. 友元关系不能继承,也就是说基类的友元函数不能访问的基类的派生类的保护和私有成员
  2. 如果基类的友元函数想要访问基类的派生类的私有和保护成员,应该声明为派生类的友元函数
classPerson{friendvoidPrint(const Person& p);public:protected:int _age =18;private: string _name ="xiaowang";};voidPrint(const Person& p){ cout << p._age << endl; cout << p._name << endl;}classStudent:publicPerson{friendvoidPrint(const Person& p);public:protected:int _stuid =111111;};intmain(){ Student s; Person p;Print(p);Print(s);return0;}
运行结果如下

六、继承与静态成员

  1. 基类定义了static静态成员,那么在整个继承体系里就只有一个这样的成员。无论派生出多少个子类都只有一个static静态成员
  2. 那么我们可以根据这个特性以及派生类在进行构造的时候会去优先调用基类的默认构造函数完成派生类中基类那部分成员的初始化,在基类定义静态成员变量,那么同时在基类的默认构造函数使用static静态成员变量用于统计基类进行构造的总次数即统计出了基类及其派生类的总个数
classPerson{public:Person(){ _count++;}staticint _count;};int Person::_count =0;classStudent:publicPerson{public:Student(){}};intmain(){ Person p; Student s1; Student s2; Student s3; cout << Person::_count << endl;return0;}
运行结果如下定义了1个基类,3个派生类,基类及其对应的派生类共计4个,结果正确

七、菱形继承和菱形虚拟继承

单继承

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

在这里插入图片描述
classA{public:int _a;};classB:publicA{public:int _b;};classC:publicB{public:int _c;};intmain(){ C c; c._a =1; c._b =2; c._c =3;return0;}
运行结果如下,对象模型如下

多继承

  1. 多继承:一个子类有两个或以上直接父类时,称这种继承关系为多继承
  2. 多继承的使用方法是在子类的位置对多个父类使用逗号,进行间隔,其余方式public形式不变,进行继承

以下图为例,根据class C :public A, public B语句中的继承对象A, B出现先后的顺序在内存中依次放置对象A,对象B,多继承中先继承对象A的放在对象C存储的内存空间的开头,对象A之后是放置的对象B

在这里插入图片描述
classA{public:int _a;};classB{public:int _b;};classC:publicA,publicB{public:int _c;};intmain(){ C c; c._a =1; c._b =2; c._c =3;return0;}
运行结果如下,对象模型如下

菱形继承

菱形继承是多继承的一种特殊情况,由于继承关系上类似菱形,所以就称这种特殊的多继承为菱形继承

在这里插入图片描述
classA{public:int _a;};classB:publicA{public:int _b;};classC:publicA{public:int _c;};classD:publicB,publicC{public:int _d;};intmain(){ D d;return0;}
数据模型如下那么在对象d中就会有两个_a的成员变量这就存在了数据冗余问题,如果要进行调用_a,那么编译器就不知道要调用哪一个_a,这就存在了二义性问题


2. 由于存在两个_a,存在二义性问题,进行调用_a编译器不知道会调用哪一个_a,那么编译进行语法检查的时候编译器会直接进行报错

缓解办法
  1. 缓解办法,采用显示指定访问哪个父类成员可以缓解二义性问题,但是针对数据冗余的问题无法解决
intmain(){ D d; d.B::_a =1; d.C::_a =2;return0;}
运行结果如下这样可以访问到两个_a,但是这种的显示指定父类的成员调用方式还需要写父类::这种形式,同时针对数据冗余还无法解决,对于d对象仅需要一个_a即可,不需要两个_a,同时我们想要直接使用d._a去访问_a,那么该如何去做呢?

解决办法
  1. 虚拟继承可以解决菱形继承的数据冗余和二义性问题。以上述的对象模型为例,在B和C继承A的时候采用虚拟继承,即可解决问题。虚拟继承是特定的解决菱形继承的方式,在其它场景不要进行使用
  2. 这种虚拟继承的使用方式是在菱形继承中进行多继承的前父类的位置(一个子类有两个即以上的父类,这多个父类)使用virtual关键字修饰,在在B和C继承A的时使用virtual关键字修饰,具体操作方式如下图示例
classA{public:int _a;};classB:virtualpublicA{public:int _b;};classC:virtualpublicA{public:int _c;};classD:publicB,publicC{public:int _d;};intmain(){ D d; d.B::_a =1; d.C::_a =2; d._a =3;return0;}
对象模型如下这样d对象中就只有一个_a,父类B和C中的_a以及d中的_a都是一个_a


2. 显示使用父类指定访问成员访问修改的是共同的_a


3. 显示使用父类指定访问成员访问修改的是共同的_a


4. 使用d._a的方式访问到的是同一个_a,即在d对象中就存放一个_a,这样就巧妙的解决了数据冗余和二义性问题

菱形虚拟继承的底层

小编使用上文代码示例的菱形继承的对象模型和菱形虚拟继承的对象进行比对讲解
在这里插入图片描述

继承和组合

public继承是一种is-a的关系,也就是说每个派生类的对象都是一个基类对象
组合是一种has-a的关系,也就是说有两个对象A和B,B组合了A,也就是说每个B对象中都有一个A对象
classA{};classB:publicA//B和A是继承关系,B继承了A{};classC//A和C是组合关系,C组合了A{private: A _a;};
在实际应用中优先使用类和类之间的组合而不是类继承继承允许你根据基类的实现去定义基类的实现。通过这种复用基类方式生成派生类的方式通常称为白箱复用。在这种继承方式中,基类的内部实现细节对派生类可见。继承的方式一定程度上破坏了基类的封装性,在派生类中可以访问使用基类的公有和保护成员,那么基类的改变,对派生类的影响就很大。基类和派生类之间的依赖关系很高,耦合度高。组合同样也是一种复用方式。组合要求被组合的对象具有良好定义的接口。这种复用方式被称为黑盒复用。假设B组合了A,那么B仅可以访问A类的公有成员,那么A类的改变,对B的影响不是很大,B对A的依赖性不是很强,所以耦合度低

所以根据上面的结论,继承的耦合度高,组合的耦合度低,由于在软件设计层次追求的是高内聚,即类内的关联很强,低耦合,即类外的关联性很弱所以当这个类的实现可以使用继承去实现又可以使用组合去实现的时候,优先使用组合当这个类比较适合使用继承的去实现时候就使用继承,当比较适合使用组合去实现的时候就使用组合那么同时要实现多态的时候就必须使用继承。

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

Read more

深入剖析Linux文件系统数据结构实现机制

深入剖析Linux文件系统数据结构实现机制

深入剖析Linux文件系统数据结构实现机制 1. 文件系统全景: 从用户视角到磁盘结构 1.1 一切皆文件的哲学 在 Linux 中, 所有资源都被视为文件. 无论是普通文本文件、目录, 还是硬件设备、进程间通信管道, 甚至是网络连接, 都通过统一的文件接口进行访问 这种设计哲学简化了系统架构, 使得对各类资源的操作可以通过相同的系统调用完成. 当你访问 /dev/sda 时, 实际上是在与磁盘设备交互;当你向 /proc/1/status 写入时, 实际上是在与进程1通信 1.2 文件系统层次架构 Linux 文件系统的设计采用了经典的分层架构, 从用户空间到物理磁盘, 每一层都有明确的职责和抽象: 物理存储 设备驱动层 块设备层 页缓存/缓冲区缓存 具体文件系统 虚拟文件系统 VFS 系统调用接口 用户空间 用户应用程序 系统调用如open/read/

By Ne0inhk
堆(Heap)的实现:基于完全二叉树的顺序存储与调整算法(含完整代码)

堆(Heap)的实现:基于完全二叉树的顺序存储与调整算法(含完整代码)

目录 🧠 一、核心认知 ⚙️ 二、核心操作本质 🔼 三、向上调整(AdjustUp) 🔽 四、向下调整(AdjustDown) 🌟 设计巧妙点(重点) 📌 三种情况全覆盖 ⚠️ 五、关键易错点总结 🚀 六、堆的核心接口实现 🧩 七、内存管理(非常重要) 🧠 八、我的关键问题总结 🧠 九、进阶理解(非常关键) 🚀 十、拓展 & 面试考点   🧠 一、核心认知 堆本质不是“树”,而是: 数组 + 完全二叉树的映射关系 --- 📌 完全二叉树的数组映射 父节点: (i - 1) / 2 左孩子: 2*i + 1 右孩子: 2*

By Ne0inhk
【嵌入式】直流无刷电机FOC控制算法全解析

【嵌入式】直流无刷电机FOC控制算法全解析

【嵌入式】直流无刷电机FOC控制算法全解析——原理、代码与实战 文章目录 * 【嵌入式】直流无刷电机FOC控制算法全解析——原理、代码与实战 * 引言 * 一、电机基础:从结构到控制原理铺垫 * 1. 无刷电机核心构成与优势 * 2. BLDC与PMSM的核心区别 * 3. 传统控制与FOC的本质差异 * 二、FOC核心原理:从坐标系变换到SVPWM实现 * 1. FOC算法整体框架(流程图) * 2. 核心坐标变换:从三相到两相的降维魔法 * (1)Clarke变换:静止坐标系降维 * (2)Park变换:静止到旋转的解耦 * 3. SVPWM:空间矢量的脉冲合成 * (1)基本电压矢量与扇区划分 * (2)扇区判断逻辑 * (3)矢量作用时间计算 * (4)PWM占空比生成 * 三、硬件与软件实战:STM32F103实现FOC控制 * 1. 硬件系统架构(

By Ne0inhk