【C++】继承知识点总结

简介: 一. 继承的概念及定义比如我们要定义学生类(Student)和老师类(Teacher),作为人这两个类共有的基本属性包括姓名,年龄等。写两个类就要各自都声明姓名和年龄这两个成员变量,能不能单独写一个Person类里面只有姓名和年龄这两个成员变量,让学生类和老师类去继承Person类,这样就不用单独地再去声明姓名和年龄了。

一. 继承的概念及定义


比如我们要定义学生类(Student)和老师类(Teacher),作为人这两个类共有的基本属性包括姓名,年龄等。写两个类就要各自都声明姓名和年龄这两个成员变量,能不能单独写一个Person类里面只有姓名和年龄这两个成员变量,让学生类和老师类去继承Person类,这样就不用单独地再去声明姓名和年龄了。


1.继承的概念


继承(inheritance)机制是面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,而继承是类设计层次的复用。


2. 继承的定义


2.1 定义格式


下图我们看到Person是父类(也称作基类)。Student是子类(也称作派生类)。

25c05ff010894d1695d43591473eec35.png


继承后父类的成员(包括成员变量和成员函数)都会成为子类的一部分:

20210611205404854.png

2.2 继承关系和访问限定符


30514b151b724078a5bff4e7a05c6571.png

2.3 继承后基类成员访问方式的变化


  1. 基类的private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  2. 如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 总结:基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min权限最小的(成员在基类的访问限定符,继承方式),其中public > protected > private

image.png


2.4 几点补充


  • 子类的关键字为class时默认的继承方式是private,为struct时默认的继承方式是public,不过最好显示的写出继承方式。

2021061121145612.png

  • 在实际运用中一般使用都是public继承,几乎很少使protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在子类的类里面使用,实际中扩展维护性不强。


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


1. 概念


子类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把子类中属于基类那部分切过去。

20210611212718869.png

演示一下

a15b96b8e953455d8ddc2e7ab7e5394e.png

2. 补充说明


既然子类可以给基类赋值,那么反过来可不可以呢?

基类的指针是指向派生类对象时,基类的指针可以通过强制类型转换赋值给子类的指针。

20210611220042679.png


三. 同名父类成员在子类中的隐藏


1. 概念


在继承体系中基类和子类都有独立的作用域。子类和基类中有同名成员时,在子类内部子类成员将屏蔽对基类同名成员的直接访问,这种情况叫隐藏,也叫重定义。(想要访问必须指定基类的类域 基类::基类成员名称 来显示访问基类的同名成员)。

隐藏顾名思义就是在子类对象中,子类的同名成员隐藏了基类的同名成员,但基类的同名成员依然存在于子类对象中,只不过直接调用的默认是子类的同名成员,想要调用基类的同名成员,需要专门指定基类的作用域来调用。从内容上看好像子类对基类的同名成员进行了重定义一样。


2. 成员变量的隐藏


变量名相同,就构成隐藏(重定义)

a637ed63ceaa4328bc26e25abdbd6400.png


3. 成员函数的隐藏


函数名相同,就构成隐藏(重定义)

2021061213022437.png

PS:区分成员函数的重定义和函数重载

image.png


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


类有6个默认成员函数,包括构造函数、拷贝构造函数、赋值运算符的重载、析构函数等,那么在派生类中,如何去关联基类的默认成员函数呢?


1. 派生类的构造函数


派生类的构造函数必须先调用基类的构造函数初始化基类的那一部分成员。如果基类没有自定义的默认构造函数,或有显示调用基类构造函数的需要,则必须在派生类构造函数的初始化列表位置显示调用基类的构造函数,以完成基类部分成员的初始化。

2fd52ac23d83448cae5a15fca575702b.png

编译器默认生成的构造函数不能被继承下来的问题

e184b37d4dcb4ae39917f721b6e279f7.png

为什么一定要在初始化列表里显示调用基类构造函数?

在子类的构造函数的括号内显示调用基类的构造函数行不行?不行的,要求必须在初始化列表里显示调用目的是保证先构造基类成员,再构造子类成员。另外在括号里面给值其实已经不是初始化了,而是对已经初始化过的变量重新赋值。

81ff545530984ec2897b376cd04df4ed.png


2. 派生类的析构函数


派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象在析构时先清理派生类成员再清理基类成员的后进先出顺序,所以我们没必要显示调用基类的析构函数,如果调用了的话可能还会出错(特别是父类的析构函数中涉及到内存管理时)。

20210612140702508.png

派生类的构造函数和析构函数设计原理

在子类的构造函数中,因为有显示调用基类构造函数的需求(比如我们可能要传参来构造基类成员),并且为了保证基类成员先于子类成员之前构造,所以要求基类的构造函数在子类的初始化列表里调用。

析构函数一般的话都是对象生命周期结束后自动调用,以完成清理工作,没有传参显示调用需要,并且为了保证子类成员先析构,基类成员后析构,所以设计子类的析构函数里只用完成自己成员的清理就行,结束后系统会自动调用父类的析构函数来完成父类成员的清理。


3. 派生类的拷贝构造函数


同构造函数一样,派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化,再初始化派生类成员。初始化列表必须显示调用基类的拷贝构造,不然的话基类成员会通过默认的拷贝构造初始化。

通过下面的例子来说明基类的拷贝构造是否需要显示调用的问题

case 1

基类有拷贝构造函数,和自己实现的非默认构造函数

20210612145800377.png

case 2

基类有拷贝构造函数,和自己实现的默认构造函数

cc15da51593f48628e0cbc8a96dad251.png总结

对于基类的拷贝构造,同构造函数一样也需要自己实现,编译器默认生成的不会继承下来,而且在派生类的拷贝构造里一定要通过切片显示调用。注意构造函数和拷贝构造是函数重载的关系。


4. 派生类的赋值重载


同上面的一样,必须自己实现基类的赋值重载,因为编译器默认生成的不会被继承到派生类,又因为基类和派生类的赋值重载构成了隐藏(因为他们函数名相同都是operator=)所以必须要以声明基类作用域的方式来显示调用基类的赋值重载。

通过下面的例子来看看基类的赋值重载是否会自动调用

20210612154236937.png

总结

基类的赋值重载也必须通过切片显示调用,并且要声明类域,因为基类的赋值重载被子类隐藏了。


5. 补充说明


  1. 前面介绍的四个成员函数,除了析构函数,其他的最好都显示调用基类的成员函数来完成相应的基类成员的处理。
  2. 基类和子类的赋值运算符重载因为函数名相同而成隐藏关系,在子类中调用基类的赋值重载时必须指定基类类域。
  3. 派生类对象初始化先调用基类构造再调派生类构造。
  4. 派生类对象析构清理先调用派生类析构,结束后再由编译器自动调基类的析构。


五. 继承与友元


友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。相当于你父亲的朋友不一定是你的朋友

class Student;
class Person
{
public:
  // Person类的友元
  friend void Display(const Person& p, const Student& s);
protected:
  string _name; // 姓名
};
class Student : public Person
{
protected:
  int _num; // 学号
};
void Display(const Person& p, const Student& s)
{
  cout << p._name << endl;
  cout << s._num << endl;
}
int main()
{
  Person p;
  Student s;
  // 编译不通过,不能访问Student::_num
  Display(p, s);
}


六. 继承与静态成员


基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都共用一个static成员实例。

c1dddb56230f45ea8d5cb2d7cb2dd5c3.png


七. 菱形继承及菱形虚拟继承


1. 继承关系分类


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

20210613093443624.png

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

20210613094206303.png

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

20210613094456218.png


2. 菱形继承带来的问题


从上图可以看出菱形继承具有二义性和数据冗余的问题,在Assistant对象中Person成员就有两份。

26b3f8491854480c9c61008b3f1aa001.png


3. 虚拟继承


虚拟继承可以解决菱形继承带来的的二义性和数据冗余的问题。如下图的继承关系,当Student和Teacher继承Person时使用虚拟继承,即可解决问题。

2021061310151517.png


4. 虚拟继承解决数据冗余和二义性的原理


为了研究虚拟继承原理,我们给出了一个简化的菱形继承的继承体系,再借助内存窗口观察对象成员的模型。

非虚拟菱形继承时

a031ae4ae68042a6a139d14f8efd7e9a.png


虚拟菱形继承时

463c6d1407e145f3a5c5744d96308254.png


扩展说明

问题:既然虚拟继承解决了虚基类(就是有多份的重复继承的那个类)成员的二义性和数据冗余问题,把两个同样的虚基类成员和到一起算一个。那虚基表(存指向公共基类的偏移量)存在有什么用呢?

原因:菱形虚拟继承时,vs编译器把虚基类成员放到整个对象的尾部,虚基表中存有偏移量,来计算到公有的虚基类对象的位置。存在下面的这种情况,切割发生时,d需要去找出B/C中的属于A类的成员赋值过去。此时靠虚基表确定到A类成员的偏移量然后切割过去。

D d;
B b = d;
C c = d;


八. 继承和组合


1. 继承和组合的区别

eba7c22e328d47c3bc442e16faf2057f.png


2. 什么时候用继承?什么时候用组合?


符合has - a就用继承,符合is - a就用组合。如果都可以优先用组合,因为组合的耦合度低,代码维护性好。


相关文章
|
1天前
|
编译器 C++
|
6天前
|
程序员 编译器 C++
【c++】继承
【c++】继承
7 0
|
6天前
|
C++
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
10 0
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
|
7天前
|
安全 Java 程序员
【C++航海王:追寻罗杰的编程之路】继承你学会了么?
【C++航海王:追寻罗杰的编程之路】继承你学会了么?
6 0
|
7天前
|
C++
【C++】学习笔记——继承_2
【C++】学习笔记——继承_2
12 1
|
7天前
|
程序员 编译器 C++
【C++】学习笔记——继承_1
【C++】学习笔记——继承_1
8 0
|
8天前
|
编译器 C++
C++中的继承
C++中的继承
17 1
|
11天前
|
C++
C++一分钟之-继承与多态概念
【6月更文挑战第21天】**C++的继承与多态概述:** - 继承允许类从基类复用代码,增强代码结构和重用性。 - 多态通过虚函数实现,使不同类对象能以同一类型处理。 - 关键点包括访问权限、构造/析构、菱形问题、虚函数与动态绑定。 - 示例代码展示如何创建派生类和调用虚函数。 - 注意构造函数初始化、空指针检查和避免切片问题。 - 应用这些概念能提升程序设计和维护效率。
21 2
|
12天前
|
编译器 C++ Windows
C++ 链式继承下的虚函数列表
C++ 链式继承下的虚函数列表
|
13天前
|
编译器 C++
C++:继承性_程序
C++:继承性_程序
11 1