Cpp7 — 继承和多态

 继承 -------- 面向对象的三大特性之一

面向对象的三大特性:封装、继承、多态

封装:把数据和方法都封装在一起,想给你访问的变成共有,不想给访问的,写成私有。

继承:继承是类设计层次的复用

多态:

继承方式:

如果不写继承方式,class默认是私有继承,struct默认是公有继承(即继承的访问限定符)

protected/private:在类外面不能访问,在类里面可以访问

Cpp7 — 继承和多态_第1张图片

 公有继承、保护继承、私有继承,总的来说就是取小的那个就对了

私有成员的意义:不想被子类继承的成员可以设计成私有。但是不代表子类没有继承私有

私有和保护的区别:基类中想给子类复用,但是又不想暴露直接访问的成员时,就应该定义成保护

继承中的作用域

C++定义以后会产生域,C语言是局部域,全局域,有些域会影响生命周期,有些域不会,

类域不影响生命周期,只是影响访问。父类和子类是可以定义同名变量的,子类访问时先访问的是子类的,如果就是想访问父类的,则要指定作用域

实际中尽量不要定义同名的成员。(父类和子类是在各自的作用域,不是同一作用域)。

如果同名,例如子类和父类都定义了name,子类会继承父类的name,会有两个name。这时会隐藏父类的。

Cpp7 — 继承和多态_第2张图片

以下程序,两个func的关系是什么?两个func构成函数重载嘛?

Cpp7 — 继承和多态_第3张图片

 class a

{

public:

       void func()

{};

}

class b : public a

{

public:

       void func(int i)

       {}

}

不构成重载,因为函数重载要求在同一作用域。这两个func构成隐藏关系(如果是成员函数的隐藏,只需要函数名相同就构成隐藏)。

注意:对象中不存储成员函数,只存储成员变量。

Cpp7 — 继承和多态_第4张图片

对象中不存储成员函数,只存储成员变量:继承后类的大小的改变:多出了父类的成员变量。父类和子类的成员函数还是在各自的代码段中的。

赋值兼容转换

公有继承的情况下:

子类对象可以赋值给父类对象/指针/引用,这个叫做赋值兼容转换,也叫切割或切片(这里虽然是不同类型,但是不是隐式类型转换)

我们之前讲的同类型变量可以赋值,不同类型间赋值要进行强制类型转换或隐式类型转换,

赋值时不用加ocnst就说明了它没有发生隐式类型转换(子类到父类的时候不是隐式类型转换)

这个过程也叫切割或切片 

这里虽然是不同类型,但是不是隐式类型转换,也不是强制类型转换,这里算是一个特殊支持,语法天然支持的。因为person& rp = sobj;这句代码能通过就说明了它不是隐式类型转换,因为产生的临时变量具有常性,要实现转换的话,应该加const才可以,但是这里不加都可以

不是说切出来拿走,而是切出来拷贝给父类Cpp7 — 继承和多态_第5张图片

但是下面的代码不需要加const就可以编译通过,所以这里不是隐式类型转换

Cpp7 — 继承和多态_第6张图片

父类对象给子类对象赋值是不行的,即使强制类型转换也不支持,父类是没有办法转回子类的(因为少了一部分东西)。如果是指针或引用的话是可以的,但是是很危险的。

父类指针看到的只是父类的那一部分,就像把它切出来 。

 公有继承下,子类和父类是一个is-a的关系,也就是每一个子类对象都是一个特殊的父类对象

Cpp7 — 继承和多态_第7张图片

父类是转换不成子类的,但是父类的指针、引用可以转换为子类

Cpp7 — 继承和多态_第8张图片

我们现在写了父类和子类,那子类的六个默认成员函数怎么办呢?

派生类中的六个默认成员函数是怎么处理的,它相比普通类不一样的地方是:它的成员有两个部分,一个部分是自己的,一个部分是父类继承下来的

派生类如何初始化?

子类编译默认生成的构造函数会干什么?

1.对于自己的成员,和以前一样(和普通只定义一个类和对象一样,当成一个普通类就可以,即调用自己的构造函数、析构函数等等)

2.对于继承的父类成员:必须调用父类的构造函数初始化

如果父类没有默认构造呢?这时子类无法生成默认构造,因为它处理不了父类的成员,因为其必须得调用父类的默认构造处理,所以我们需要给子类写一个构造

Cpp7 — 继承和多态_第9张图片

应该这么写

 Cpp7 — 继承和多态_第10张图片

不允许子类的构造函数初始化父类。要求是这样的:子类的构造函数只能初始化自己的成员,对于父类的成员,子类只能调用父类的构造函数处理

编译生成默认拷贝构造:

1.对于自己的成员:跟类和对象一样(对于内置类型值拷贝,对于自定义类型调用它的拷贝构造)

2.对于继承的父类成员:必须调用父类的拷贝构造初始化。

Cpp7 — 继承和多态_第11张图片

 Cpp7 — 继承和多态_第12张图片

派生类不写都可以直接调用父类的拷贝构造。(都是自定义类型) 

Cpp7 — 继承和多态_第13张图片

Cpp7 — 继承和多态_第14张图片

 需要我们拿到子类对象中的父类对象。我们把子类对象传给父类对象的引用就构成了切片

也可以强制类型转换等等,不过没什么必要 

Cpp7 — 继承和多态_第15张图片

注意:有深拷贝的时候才需要显式写S

切片很重要

编译器默认生成的operator=、析构都同上。写赋值时记得不能自己给自己赋值

栈溢出基本只有一种情况,就是无限递归了

自己的成员:构造函数、析构函数:内置类型不处理,自定义类型调用他的析构、构造

继承的成员:调用父类析构、构造函数处理

想显式写

析构函数比较特殊

子类的析构函数与父类的析构函数构成隐藏。

Cpp7 — 继承和多态_第16张图片

 这样就可以了

Cpp7 — 继承和多态_第17张图片

 但是我们其实不应该显示的写,因为子类会自动调用父类的析构函数。(为什么多次析构没问题呢,因为没做什么事情,如果析构函数里有对指针的释放,那么就会出问题了,因为对指针释放了两次)

 子类和父类的析构函数名不同,为什么它两构成隐藏?

由于之后多态的需要,析构函数的名字会被同一处理成destructor()

先定义的先初始化,后定义的后初始化,先定义的后析构,后定义的先析构,因为要符合后进先出。父类的先构造,子类再构造,子类先析构,父类后析构。

编译器默认生成的赋值同上

如果我们自己显示写调用父类的析构函数的话,顺序就无法保证了,所以子类的析构函数中不需要显式调用父类的析构函数,因为每个子类析构函数后面,会自动调用父类析构函数,这样才能保证先析构子类,再析构父类

静态变量一定是不被包含在对象中的

子类继承父类的相同名字的变量后,我们实际访问的是子类的该变量,其类似于全局变量和局部变量访问时的就近原则。如果想访问父类的,可以指定作用域

Cpp7 — 继承和多态_第18张图片

Cpp7 — 继承和多态_第19张图片

 Cpp7 — 继承和多态_第20张图片

Cpp7 — 继承和多态_第21张图片

 为什么栈溢出?

Cpp7 — 继承和多态_第22张图片

因为这里递归调用了。一直在调用子类的。(子类和父类的operator=构成了隐藏关系)如果想调父类的, 需要我们指定一下

析构也是:编译器自动生成的析构函数,内置类型不处理,自定义类型调用他的析构

Cpp7 — 继承和多态_第23张图片

 构造和析构可以看成一类,拷贝构造和赋值运算符重载可以看成一类

Cpp7 — 继承和多态_第24张图片

 Cpp7 — 继承和多态_第25张图片

 因为子类的析构函数跟父类析构函数构成隐藏。这里其实是找不到该函数(~Person)。

Cpp7 — 继承和多态_第26张图片

由于多态的需要,析构函数的名字会被统一处理成destructor()

Cpp7 — 继承和多态_第27张图片

子类对象不用显示调用父类 ,因为它会自动调用

取地址重载不用调用父类的,不用写成合成版本

继承与友元

友元关系不能被继承,也就是说基类的友元不能访问子类私有和保护成员

Cpp7 — 继承和多态_第28张图片

某个函数是父类的友元,父类被子类继承,不代表它是子类的友元,如果想访问,需要把该函数弄成子类的友元

继承与静态成员

整个继承体系里,只有一个静态成员

顾名思义

Cpp7 — 继承和多态_第29张图片

 静态成员是存在静态区的,且基类与派生类访问的count是同一份

如果没有明确说明,一般都是说公有继承

Cpp7 — 继承和多态_第30张图片

Cpp7 — 继承和多态_第31张图片

 统计有多少个人,在Person里++一个统计数值

C++11新增了一个final关键字,也叫最终类。

Cpp7 — 继承和多态_第32张图片

Cpp7 — 继承和多态_第33张图片

多继承,Derive继承了Base1和Base2,先继承的在前

Cpp7 — 继承和多态_第34张图片

Cpp7 — 继承和多态_第35张图片

 Cpp7 — 继承和多态_第36张图片

 Cpp7 — 继承和多态_第37张图片

p2大,因为在栈里

Cpp7 — 继承和多态_第38张图片

Cpp7 — 继承和多态_第39张图片

derive其实就是12个字节 

单继承和多继承

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

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

Cpp7 — 继承和多态_第40张图片

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

Cpp7 — 继承和多态_第41张图片

Cpp7 — 继承和多态_第42张图片

Person这个信息在assistant里面就会有两份,两份会导致数据冗余,重复占用空间了,构造了两次,拷贝了两次。

二义性:assistant要访问name时,不知道要访问谁的

Cpp7 — 继承和多态_第43张图片

Cpp7 — 继承和多态_第44张图片

Cpp7 — 继承和多态_第45张图片

如何定义一个不能被继承的类:将父类的构造函数私有,这时子类对象实例化时,就会无法调用构造函数。

Cpp7 — 继承和多态_第46张图片

这样在继承时就会报错 

Cpp7 — 继承和多态_第47张图片

内存中并没有存Base1、Base2,只是编译器在调试窗口方便我们看 

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在assistant的对象中person成员会有两份

Cpp7 — 继承和多态_第48张图片

 研究生助教,既是学生,也是老师。

Cpp7 — 继承和多态_第49张图片

有两份数据。

Cpp7 — 继承和多态_第50张图片

也属于菱形继承。为什么左边的virtual放在B而不是C?如右图,B在C里。第二十四节1:04:00

Cpp7 — 继承和多态_第51张图片

Cpp7 — 继承和多态_第52张图片

 二义性,编译器不知道调谁的_name。

Cpp7 — 继承和多态_第53张图片

这样就可以了,但这样还是有时会造成空间浪费 

如何解决上述问题?

虚继承(菱形虚拟继承)

在腰部位置加上virtual(它和后面讲的多态不同)

Cpp7 — 继承和多态_第54张图片

 Cpp7 — 继承和多态_第55张图片

 就可以这样了,这三种方式访问到的都是同一份

Cpp7 — 继承和多态_第56张图片

 菱形虚拟继承解决了数据冗余和二义性

Cpp7 — 继承和多态_第57张图片

iostream菱形继承了istream和ostream。

Cpp7 — 继承和多态_第58张图片

Cpp7 — 继承和多态_第59张图片

以上就会有数据冗余和二义性的问题。

多继承:

Cpp7 — 继承和多态_第60张图片

Cpp7 — 继承和多态_第61张图片

菱形虚拟继承:

我们可以把中间虚继承了

Cpp7 — 继承和多态_第62张图片

Cpp7 — 继承和多态_第63张图片

这时就可以 

Cpp7 — 继承和多态_第64张图片

我们可以看到,重复的成员被放到了同一地址,同一成员只有一份。只有这一个地址发生改变。 

Cpp7 — 继承和多态_第65张图片

解决了数据冗余和二义性,但是看上去时变大了,并没有减少空间。真正多的是两个指针,多了8字节。那如果a很大呢,例如a是4000字节,多存一份不就是多40000字节,而如果是虚拟继承,只是多一个指针。

Cpp7 — 继承和多态_第66张图片

 Cpp7 — 继承和多态_第67张图片

 这12和20是距离也是偏移量。

偏移量的作用在于:以后要切片的时候,用指针地址加偏移量,就可以

Cpp7 — 继承和多态_第68张图片

Cpp7 — 继承和多态_第69张图片 在虚继承里,B对象有没有a,有,只是怎么存的问题。 但是实际计算B的大小是12。

Cpp7 — 继承和多态_第70张图片

我们发现除了1和2还有一个指针在这里 .公共a是放在最下面的(高地址处),它是怕遇到这种场景

Cpp7 — 继承和多态_第71张图片

当我们不知道这个B*的指针是指向B对象还是D对象。对象里只有一个指向表的指针,这个表叫虚基表,通过偏移量找虚基类。

父类有多个成员需要多个指针吗?不用,能找到第一个,就能找到第二个。它是把对象当成整体的

了解:第二十三节3:00:00-第二十四节00:24:00

如何定义一个不能被继承的类?

C++98:

1.父类构造函数私有(即子类不可见)

2.这样就会导致子类对象实例化时,无法调用构造函数(以此来间接实现该类不能被继承)

Cpp7 — 继承和多态_第72张图片

Cpp7 — 继承和多态_第73张图片倒不是继承时会报错,而是实例化对象时会报错 

 而C++11做出了改进:

新增了关键字final,也叫最终类

Cpp7 — 继承和多态_第74张图片

这样就会直接报错说A不可以被继承,因为已经被声明成final

Cpp7 — 继承和多态_第75张图片

多态继承中的指针偏移:

对象的分布上:先继承的在前面

菱形继承二义性的解决:指定作用域

数据冗余的解决:虚继承(virtual)

菱形虚拟继承解决了二义性和数据冗余,还节省了空间(例如当父类的成员变量很大时)

Cpp7 — 继承和多态_第76张图片

选C第二十三节2:25:30 

Cpp7 — 继承和多态_第77张图片

 Cpp7 — 继承和多态_第78张图片

p2=p3!=p1 

Cpp7 — 继承和多态_第79张图片

 Cpp7 — 继承和多态_第80张图片

继承和组合

公有继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象。例如:人和学生的关系(每一个学生都是人),植物和玫瑰花的关系(玫瑰花都是植物)。

什么是组合呢?是一种has-a的关系例如:头和眼睛的关系、轮胎和车

组合也是复用,相比较公有继承,组合的权限较小(能访问组合的公有,但是不能访问保护和私有)。实际中,哪个符合就用哪个。实际运用中,一些类型的关系既可以认为是is-a,也可以认为是has-a(既适合用继承,又适合用组合),这种情况下,优先使用对象组合,而不是类继承

组合:

Cpp7 — 继承和多态_第81张图片

低耦合,高内聚,所以优先使用组合。

组合是黑箱复用,继承是白箱复用

oj的测试用例就是白盒测试。软件工程提倡联系越少越好,也就是低耦合,互相影响小。

Cpp7 — 继承和多态_第82张图片

第二十四节00:54:00

白箱复用、黑箱复用。

黑盒测试:不知道你的实现,我以功能的角度去进行测试。(耦合度低)

白盒测试:我知道你的实现,针对你的实现去进行测试。(耦合度高)

继承耦合度高(依赖关系强)。我们提倡耦合度低,越低越好

UML图、类图

Cpp7 — 继承和多态_第83张图片

多态

 多态:多种形态,具体点就是去完成某个行为,当不同对象去完成时会产生出不同的状态。

例如买票,学生、军人买票和普通人买票就不一样。学生、军人、普通人就是三种不同类型的对象。学生买到半价票,普通人买票,军人不用排队买票,他们产生出的状态不同

做同一件事,但是结果不同。

只有成员函数才能加virtual,加了virtual之后这个函数就叫做虚函数。只有成员函数才能是虚函数。

(这里的virtual和虚继承那里的不一样,就像delete也有两种作用一样,就像取地址和引用这种)

虚函数(下图buyticket)间的关系:重写(覆盖)

Cpp7 — 继承和多态_第84张图片

 虚函数的重写/覆盖条件:虚函数函数名、参数、返回值类型相同

注:不符合重写,才是隐藏关系(缺省参数给多少/有没有缺省参数并不影响,当然,参数类型不同就不构成重写了,参数名不写也不行,(参数名不同呢?可以吗?))

多态有两个条件:

1.虚函数的重写

2.使用父类的指针或引用去调用虚函数。

这两个条件有一个不满足就构不成多态

Cpp7 — 继承和多态_第85张图片

这时和p的类型已经没有关系了,而是看其指向(引用)的对象

 Cpp7 — 继承和多态_第86张图片

1.不是父类的引用或调用:

Cpp7 — 继承和多态_第87张图片

这样g更不行

Cpp7 — 继承和多态_第88张图片

2.不符合重写virtual

 Cpp7 — 继承和多态_第89张图片

 

3.参数不同

Cpp7 — 继承和多态_第90张图片

 

不构成多态,看类型,构成多态,看它指向的对象

在构成虚函数时,如果去掉父类,则不符合重写了,不是虚函数了

特例:

特例1:如果去掉子类的virtual,则还是虚函数(但去调父类,则不符合重写,就不是虚函数了),因为它认为是先把父类的虚函数继承下来,继承下来之后重写,重写的是它的实现,所以虽然子类中我们没写virtual,但是还是认为是它的实现(子类虚函数不加virtual,依旧构成重写)。实际中最好加上virtual。

特例2:重写的协变。返回值可以不同,但是要求返回值必须是父子关系的指针或者引用。不然会报错。子类返回的是父类的话也不可以,会报错。

Cpp7 — 继承和多态_第91张图片

 这样会报错,这样才对。(即使是父子对象也不行,必须是父子指针或引用,且父是父,子是子)第二十四节2:04:00-2:08:00有一个挺重要的点

Cpp7 — 继承和多态_第92张图片

 

Cpp7 — 继承和多态_第93张图片

参数不同:

Cpp7 — 继承和多态_第94张图片

 Cpp7 — 继承和多态_第95张图片

第二十四节2:17:00

Cpp7 — 继承和多态_第96张图片

选B,首先,test不是多态,test的this指针是A类型的,this指针调用func,

虚函数重写是一个接口继承(接口就是把函数的函数名参数返回值等架子拿下来),普通函数继承是实现继承(主要继承的是你的实现)。

接口继承是重写实现。第二十四节2:24:00

1:56:20不存在要用子类演示的场景,因为子类传不过去

去掉父类的virtual就肯定不符合重写了

带虚函数对象的大小的计算

考察的不是内存对齐,考察的是多态,因为该对象里会多一个指针,这个指针叫虚表指针(vftptr虚函数指针),virtual function,(vftable虚函数表),表:能存储多个数据的一个东西。      其实是用了一个数组把虚函数的地址存到虚函数表里。虚函数会把它的地址放到虚函数表。多态的原理的关键:。虚函数会存进虚表,对象里没有虚表,有的是一个虚表的指针。虚函数重写以后呢?

虚函数指针是一个函数指针数组

多态调用:程序运行时,去指向对象的虚表中找到函数的地址,进行调用

普通函数调用:在编译(编译时就有函数的地址)或链接(在符号表里找)时确定(去找)函数的地址,运行时直接调用

多态的原理:虚表(虚函数表)

多态的条件:1.必须完成虚函数的重写2.必须是父类指针或引用去调用虚函数。才能有多态的效果

只要不符合条件,就是直接调用。

看看构成多态和不构成多态调用的区别:

虚函数是被存在哪里的呢?

虚函数不是存在虚表里的,任何函数都是编译好了变好指令,放在公共代码段的。我们写的代码编译好之后的这个文件,是一个可执行程序,程序运行起来之后变成一个进程,进程分段把我们的代码指令加到代码段里去了。

那么虚表里面放的是什么呢?

虚表里实际上放的只是函数的地址,

C++是怎么考量这种行为的呢?

首先,虚函数是存在公共代码段的。

函数的地址是什么?可能是第一句指令的地址。

虚表里放的只是函数的地址。

父类对象和子类对象不是指向同一个虚表,因为这个虚表被重写了(即被覆盖了),是一个子类的虚函数。虚函数表本质是一个函数指针的数组。

构成多态的调用:第二十五节00:13:00

因为是一个引用,如果是父类对象的引用,找到的就是父类的虚表,如果00:14:00

构成多态的调用,运行时到指向对象虚表中找调用虚函数地址,所以p指向谁,调用谁的虚函数

不构成多态调用,属于普通调用,编译时确定调用函数的地址。

虚函数重写的要求:1.要求父类子类都是虚函数(子类可以不写virtual)2.要求函数名、参数、返回值都相同

如果父类有虚函数,子类没有虚函数重写,那么编译器调用时是编译时决议还是运行时决议?

是运行时决议,编译器只是检查父类有没有虚函数,并且是引用,那么久直接去虚表里找。父类的虚表和子类的虚表在这种情况下,都是同一个虚函数都是父类的虚函数。

有些地方会要求析构函数加virtual。

析构函数是否建议加virtual呢?

在继承中,建议把析构函数定义成虚函数。

加了virtual以后,子类和父类能构成虚函数的重写(子类重写了父类)。普通场景析构函数是不是虚函数都没问题,但是当用一个指针接收new出来的子类对象时,再去delete这个指针。同理来一个new出来的父类。不加virtual时,new的是子类对象,但是调用的会是父类的析构函数,符合多态按照多态去调,不符合就按编译决议。00:32:15

析构函数函数名不同,为什么构成重写呢?

因为析构函数会被处理成destructor,所以加了virtual是完成虚函数重写的。

子类析构函数重写父类析构函数,才能正确调用。指向父类调用父类析构函数。指向子类调用子类析构函数

final:去修饰一个类,这个类就不能被继承。或如果一个虚函数不想被重写,就可以在虚函数后面加一个final。virtual void person() final{}

override:C++11新增关键字。只能加在子类,加在子类去检查子类的虚函数是否完成重写,没有完成重写就会报错

重载、重写(覆盖)、隐藏(重定义)的区别是什么?

相同点:它们都是函数之间的关系。

重载:要求1必须在同一作用域。要求2函数名相同,参数不同(类型、顺序、个数,有两种特殊情况)

重写:要求1两个函数分别在基类和派生类的作用域。要求2三同(函数名、参数、返回值,协变例外)要求3两个函数都必须是虚函数。有时子类虽然不写,但是也算是虚函数。

重定义(隐藏):要求1:两个函数分别在基类和派生类的作用域。要求2函数名相同。要求3两个基类和派生类的同名函数不构成重写就是重定义

多态的原理

person p1;   person p2;

共用一个虚表还是各自用各自的虚表呢?

共用一个虚表。如果各自用各自的,不就浪费了,p1和p2的虚表指针都是相同的。

同一个类型的对象共用一个虚表。

student s1;   student s2;

子类用的是子类的虚表。

重写了,父类是父类的虚表,子类是子类的虚表。

如果没有完成重写,用的是不是同一个虚表呢?没有完成重写,那父类的虚表里是父类的虚函数,子类的虚表里也是父类的虚函数。是不是同一个虚表呢?不是。虽然里面的内容是一样的。

在vs下,不管是否完成重写,子类的虚表跟父类的虚表都不是同一个。(和编译器有关),如果是一个,也是有可能的。

1.派生类只有一个虚表指针,因为派生类是继承的父类,父类里面有一个虚表指针,它就不会再生成虚表指针了。所以单继承的派生类里面也就只有一个虚表指针了。

2.重写为什么也叫作覆盖呢?

覆盖是原理层的叫法。子类的虚表是把父类的虚表拷贝下来,重写的虚函数那个位置会被覆盖成子类的虚函数,重写是语法层的叫法,覆盖是原理层的叫法。

不管我子类是不是重写,如果我子类自己还有虚函数呢?

比如说里面还有一个没有重写的虚函数,这个虚函数放哪呢?

记住,只要是虚函数,虚函数的地址都会放进虚表。

有些编译器(例如vs下)就无法看到子类中自己单独写的虚函数。这并不代表其不进虚表。它是进虚表的。如果非要看,通过内存勉强能看到。也可以通过

虚函数表是一个函数指针数组,我们打印数组里的内容就可以。

注意函数指针取别名时:typedef void(*别名)();

取虚表的对象:

*(int*)&s1

虚表指针,可以通过监视窗口看到。其存在虚表,虚表是一个函数指针数组,用来存放虚函数指针

为什么不放在对象里,而是放在虚表里呢?

一个对象可能会有多个虚函数,多个虚表指针

虚函数指针会放在虚表,重写以后,父类里会有一个虚表

子类中会有父类成员,子类与父类的虚表存的内容不完全一样,因为有的部分被重写了

Cpp7 — 继承和多态_第97张图片

Cpp7 — 继承和多态_第98张图片

Cpp7 — 继承和多态_第99张图片

普通函数不用去找地址了,直接调用。

Cpp7 — 继承和多态_第100张图片

虚函数存在哪里?

不是在虚表里,

Cpp7 — 继承和多态_第101张图片

 任何函数都是编译好了变成指令,放在公共代码段的。

虚表里放得只是函数的地址,虚函数还是放在公共代码段的。

父类对象与子类对象指向的虚表不同

第25节00:24:00

子类没有函数,只有父类有虚函数,是否构成多态?

析构函数建议加virtual,不同析构函数名不同,为什么构成重写?

因为析构函数统一被处理成destructor,所以可以认为她们函数名相同,这样处理的目的就是让他们构成重写

为什么要求构成重写?00:33:00因为可能出现这种错误

建议在继承中,析构函数定义成虚函数

单继承情况下只有一个虚表

override加在子类的虚函数花括号前,检查子类虚函数是否完成重写

抽象类

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化(即不能定义)出对象。派生类继承后规范了派生类必须重写,另外,纯虚函数更体现出了接口继承。

如果不想让一个对象实例化,就把它定义成抽象类

纯虚函数:在虚函数的后面写上=0,则这个函数为纯虚函数。

纯虚函数是不需要实现的,想实现也可以。(可以有函数体,也可以没有函数体)

override是检查重写

抽象类是强制重写

父类无法实例化时,子类也无法实例化,因为子类继承了父类,继承之后子类里也有了纯虚函数,有纯虚函数,如果子类不重写,子类也是抽象类,不合理,可以说是间接强制了子类必须去重写,不然子类会无法实例化出对象。

什么是接口?

两人进行配合,通过一个东西来使得互相写的可以调用。这个东西就叫接口

接口继承和实现继承

虚函数的实现是接口继承。普通继承是实现继承。

Cpp7 — 继承和多态_第102张图片

你可能感兴趣的:(C++,c++)