简介

本课程将带领大家体会面向对象三大特性中的多态特性,讲述了虚函数、抽象类和接口类等概念,以及多态的实现原理,课程的最后引入RTTI及异常处理,使整个多态篇更加完整,更具实战指导性,本门课程是C++远征课程的高潮和经典,对于面向对象的语言的学习将大有裨益。

第1章 课程介绍

1-1 C++多态概述(03:48)

隐藏与覆盖的区别
早绑定与晚绑定

第2章 虚函数及实现原理

##2-1 [C++]虚函数(08:32)
多态:相同对象收到不同的命令或者不同对象收到相同的命令做出不同的动作。

动态多态和静态多态的区别(注意这些多态都出现在类里面,因为没有了继承就没有多态):
静态多态(早绑定):静态多态主要通过函数和运算符重载来实现,例如函数void go(int a){}和void go(){}这两个函数的名字一样,但是在编译的时候就可以根据go()里面有无输入参数区分出到底执行哪个方法

动态多态(晚绑定):例如父类father有个方法名字叫go(){“父类的go”};
子类son有个方法也叫go(){“son的go”};
子类daughter有个方法也叫go(){“daughter的go”};
father p1=new son;
father
p2=new daughter;
然后当我们执行
p1->go();
p2->go();
输出的结果都只是父类的go;如果想实现输出”son的go”和”daughter的go”,就需要在父类的go()方法得前面加上关键字virtual变成。这样再输入p1->go();p2->go();就可以输出”son的go”和”daughter的go”

多态:指相同对象受到不同消息或不同对象收到相同消息时产生不同的动作。
静态多态(早绑定):在运行前,编译阶段就已确定要调用哪个函数,很早就把函数编译进去。
动态多态(晚绑定):不同对象,下达相同指令,产生不同动作。前提:以封装与继承为基础。至少要两个类,父类与子类,用三个类时,动态多态表现地会更明显。
实现多态的成员函数:用virtual修饰函数,使之成为虚函数。

2-4 [C++]虚析构函数(06:54)

虚析构函数:virtual->析构函数

virtual在修饰函数时的限制:
1.普通函数不能是虚函数。
2.静态成员函数(static)不能是虚函数。
3.内联函数(inline)不能是虚函数
4.构造函数不能是虚函数。

多态–>用父类指针指向子类对象–>delete 时只调用父类析构函数,需要用虚析构。

虚函数实现动态多态(晚绑定);
虚析构函数使得释放父类对象时,同时释放子类对象;
虚继承使得多重继承和多继承只继承一份父类的数据,不会产生冗余。

2-6 练习题

虚函数特性可以被继承,当子类中定义的函数与父类中虚函数的声明相同时,该函数也是虚函数。
虚函数使用virtual关键字定义,但使用virtual关键字时,并非全部是虚函数
虚析构函数是为了避免使用父类指针释放子类对象时造成内存泄露。

2-7 [C++]虚函数与虚析构函数…(12:17)

重载是指两个同名函数,但是参数的类型和数量不同
隐藏是指父类和子类中,具有同名函数。
覆盖是指父类和子类中,具有同名的虚函数。

(1)虚函数表指针(指向了虚函数表的首地址)->虚函数表(地址的偏移找到对应的虚函数的地址)->虚函数。
(2)虚函数表指针、虚函数表、虚函数都是占用内存空间的。

多态的原理

在子类中访问父类的计算面积的函数,也能通过虚函数表指针找到子类自己的虚函数,也是指向父类的计算面积的函数的入口的。

若子类定义了自己的计算面积的函数,通过相同的偏移量找到的是子类的计算面积的虚函数。

在子类中定义了同名的虚函数,就会在子类的虚函数列表中将父类中定义的虚函数的函数地址覆盖掉。

虚析构函数的原理

通过子类的虚函数指针找到虚函数表列表,然后找到虚析构函数,执行子类的虚析构函数,再自动执行父类的虚析构函数。

2-8 [C++]虚函数表示例一…(10:13)

1.对象大小:指类实例化对象时,数据成员占有内存空间的大小,不包括成员函数。没有任何数据成员的类理论上讲不占任何内存。

2.对象地址:指通过一个类实例化一个对象时,对象占有一定的内存单元,这个对象占据的第一个单元的地址即是对象的地址

3.对象成员地址:指当用一个类实例化一个对象时,这个对象可能有多个数据成员,由于数据成员的数据类型不同,那么占据的空间大小也就不同,这个类的每个成员都会占一定的空间,每个数据成员的地址也就是对象成员的地址不同

4.虚函数表指针:指在有虚函数时,实例化一个对象时,这个对象的第一块内存中存放的是一个指针,是虚函数表的地址,大小为4。

5.如果实例化对象没有数据成员,就会有一个内存单元来标记这个对象的存在,如果对象中有数据成员,就不用内存单元来标记这个对象的存在,这个对象的大小就是数据成员所占内存的大小。

2-10 练习题

虚函数表:
1、C++中的多态是通过虚函数表实现的。
2、每个类只有一份虚函数表,该类的对象共用一张虚函数表。
3、两张虚函数表中的函数指针可能指向同一个函数。

第3章 纯虚函数和抽象类

3-1 [C++]纯虚函数抽象类…(06:12)

虚函数:virtual double calcArea(){———;}
纯虚函数:virtual double calcPerimeter() = 0;
包含纯虚函数的类,就是抽象类,抽象类无法实例化对象
抽象类子类只有把抽象类当中的所有纯虚函数都做实现,才可以实例化

纯虚函数,通过它可以指明一个虚拟函数只是提供了一个可被子类型改写的接口。

3-4 单元练习

virtual eat(){}; 这样是给函数一个空定义,而不是定义为虚函数。

A 只有函数声明没有函数定义的虚函数是纯虚函数。
B 含有纯虚函数的类叫做抽象类。所以就必须含有=0标志了。
C 不可以使用含有纯虚函数的类实例化对象。
D 抽象类的子类也可以是抽象类。

3-5 [C++]接口类(08:17)

在类当中,无数据成员,仅含有的成员函数又都是纯虚函数的类叫做接口类。
没有构造函数,没有析构函数。

接口类更多的时候表达的是具有某种能力或是功能,比如说。。某种类型的光谱仪(A、B、C、D)都具扫描光谱的功能,但是他们所具有的扫描光谱的功能又有差别(例如:有的精度高,有的速度快。。。),这四种光谱仪都可以继承该接口类,然后再各自实现自己扫描光谱的功能。

第4章 运行时类型识别

4-1 [C++]RTTI(07:54)

RTTI:运行时类型识别
typeid(obj).name()打印出obj这个指针指向的实际的对象类型
对类型进行比对:if{typeid(
obj)==typeid(Bird)}
dynamic_cast< >转换类型,< >中是要转化成为的类型,例如:Bird bird = dynamic_cast<Bird >(obj);(obj)转化为Bird * 类型

  • dynamic_cast用法的注意事项:只能应用于指针和引用之间的转换,即< >中只能是某一类型的指针或者是某一类型的引用;要转换的类型中,必须包含虚函数;转换成功返回子类的地址,失败返回NULL
  • typied的注意事项:type_id 返回一个type_info对象的引用;如果想通过基类的指针获得派生类的数据类型,基类必须带有虚函数;只能获取对象的实际类型

4-2 [C++]RTTI代码示例(11:33)

使用RTTI技术时,需要导入这个头文件

第5章 异常处理

5-1 [C++]异常处理(13:54)

异常:程序运行期出现的错误。
异常处理:对有可能发生异常的地方做出预见性的安排。
异常处理关键字try…catch…和throw就是将主逻辑放在try块里,异常处理逻辑放在catch里面。
基本思想:主逻辑与异常处理分离。好处看上去整齐,非常容易理解。
异常传播方向和调用关系相反。
try 和catch可以是一对一也可以是一对多。catch(…)里面可以加…,捕获值需要引用。
常见的异常:数组下标越界,除数为0,内存不足。
多态和异常处理的联系:我们可以通过定义一个异常类,把异常类定义为一个接口类其中定义一些打印的方法,或者异常处理的方法,然后我们通过细分的子类来继承接口类当我们抛出这些子类的对象时都可以用父类来捕获。throw new加子类名捕获时用catch(父类名+&e)我们也可以通过子类对象去调用子类相应的虚函数。

C++中常见的异常,数组越界:提示用户就好了。除数为零,也比较常见。系统处理就会有错误的结果。内存不足:已经少见了。(循环。吃掉内存)

5-2 [C++]异常处理代码示例…(09:33)

catch(…)是最后的处理


catch(err 父类)比较常用

throw 时可以定义。throw string(“blablabla”);
throw 时可以动态申请内存。throw new IndexException

5-4 巩固练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <string>
#include <stdlib.h>
using namespace std;

/**
* 定义函数division
* 参数整型dividend、整型divisor
*/
int division(int dividend, int divisor)
{
if(0 == divisor)
{
// 抛出异常,字符串“除数不能为0”
throw string("除数不能为0");
}
else
{
return dividend / divisor;
}
}

int main(void)
{
int d1 = 0;
int d2 = 0;
int r = 0;
cin >> d1;
cin >> d2;
// 使用try...catch...捕获异常
try {
division(d1, d2);
}
catch (string &e) {
cout << e << endl;
}

return 0;
}