本课程将带领大家体会面向对象三大特性中的多态特性,讲述了虚函数、抽象类和接口类等概念,以及多态的实现原理,课程的最后引入RTTI及异常处理,使整个多态篇更加完整,更具实战指导性,本门课程是C++远征课程的高潮和经典,对于面向对象的语言的学习将大有裨益。
1 c++ 多态概述
多态知识点地位
多态衍生的知识点
2 虚函数及实现原理
2.1 什么是多态
说明: 指相同对象收到不同消息或不同对象收到相同消息时产生不同动作。多态可以分为两类,
- 静态多态
- 动态多态
静态多态(早绑定)
说明: 指函数重载
,消息应该发送给谁在编译阶段就确定了。
1 |
|
动态多态(晚绑定)
说明: 动态多态具体到语法中是指,使用父类指针指向子类指针,并可以通过该指针调用子类的方法。
- 产生多态的基础是继承关系,没有继承就没有多态。
- 多态的语法核心是
virtual
关键字,必须使用virtual
才能使多个类间建立多态关系。
1 |
|
2.2 虚析构函数
2.2.1 虚析构函数
说明: 使用 virtual
关键字修饰符修饰析构函数,就构成了虚析构函数。
- 虚函数使用 virtual 关键字定义,但时用 virtual 关键字时,并非全部是虚函数。
- 虚函数特性可以被继承,当子类中定义的函数与父类中虚函数的声明相同时,该函数也是虚函数。
- 虚析构函数是为了避免使用父类指针释放子类对象时造成内存泄漏。
用途: 将父类的析构函数定义为虚析构函数,从而当通过指向子类对象的父类型指针销毁对象时,子类的析构函数也会被自动调用。
注意: 父类的析构函数定义为虚析构函数,则编译器会自动为子类的析构函数添加 virtual
修饰,但仍然建议手动为子类定义虚析构函数,代码的可读性更好。
1 |
|
2.2.2 不能被 virtual 修饰的函数
说明:并不是所有类型的函数都能构用 virtual
修饰。
不能用 virtual 修饰的函数|编译器行为
—|—
全局函数|报错
静态成员函数|报错
内联函数|忽略 virtual 修饰符
构造函数|报错
1 | // 普通函数(全局函数) |
2.3 虚函数与虚析构函数
2.3.1 函数指针
说明: 函数的本质其实就是一段二进制代码,函数指针保存着函数二进制代码存储区域的首地址。
2.3.2 虚函数原理
虚函数表: 关于虚函数表的事实如下
- 类中含有虚析构函数或其它虚函数就能产生虚析函数表。
- 每个类只有一份虚函数表,所有该类的对象公用同一张虚函数表。
- 两张虚函数表中的函数指针可能指向同一个函数(指父类定义了虚函数而子类没有对应的虚函数的情况)。
说明: 在 c++ 中,多态是通过虚函数表
来实现的。具体来说,父类的 A 函数定义为虚函数,那么分两种情况:
子类是否定义了 A 函数|父类指针调用 A 函数
—|—
是|通过查找虚函数表,调用的是子类各自的 A 函数(构成多态)
否|通过查找虚函数表,调用的是父类中定义的 A 函数(不构成多态)
其中第一种情况,事实上就是利用 c++ 函数的覆盖与隐藏
实现了多态。
例子
情形1:子类没定义对应的虚函数
情形2:子类定义了相应的虚函数
2.3.3 虚析构函数原理
说明:虚析构函数可以确保,即使是父类指针引用的子类示例,执行完子类的析构函数(如果有的话)就会执行父类的析构函数,从而避免内存的泄漏。
1 |
|
1 | Animal |
2.3.4 深入了解内存中的对象
说明: 除了前面的虚函数和虚析构函数,我们还想通过这个例子深入了解以下知识点
- 对象的大小
组成部分|大小/字节(64位机器)|存在条件
—|—|—
占位|1|仅当既没有任何数据成员,也没有虚函数表的情况下
虚函数表指针|8|定义了虚函数或虚析构函数,实际上是一个
数据成员|所有数据成员大小之和|存在数据成员
- 对象的地址
- 对象成员的地址
- 虚函数表指针
1 |
|
1 | Shape() |
3 纯虚函数和抽象类
3.1 纯虚函数和抽象类
纯虚函数: 定义为 virtual 函数返回值 函数名() = 0
这种形式的函数叫做纯虚函数。这种函数没有定义任何实现代码,仅能用来被子类类继承并实现。
抽象类: 含有 纯虚函数
的类叫做 抽象类
。 抽象类
不能实例化,只能用来派生其它类。抽象类
的的派生类只有实现了基类的所有纯虚函数才能实例化,否则任然是抽象类
。
1 |
|
3.2 接口类
说明: 仅含有纯虚函数
的类称为接口类
。
- 接口类中仅有纯虚函数,不能有其它函数,也不能有数据成员。
- 可以使用接口类指针指向其子类对象,并调用子类对象中实现的接口类中的纯虚函数。
- 一个类可以继承一个接口类,也可以继承多个接口类。
- 一个类可以继承接口类的同时也继承非接口类。
DEMO1: 即继承接口,也继承类
DEMO2
1 |
|
4 运行时类型识别
说明:RTTI(Run-Time Type Identification)
技术可以通过父类指针识别其所指向对象的真实数据类型。与虚拟概念是类型识别必须建立在虚函数的基础上,否则无需 RTTI 技术。
原理: 利用 typeid
函数 和 dynamic_cast
函数
4.1 dynamic_cast 函数
描述: dynamic_cast
可以将一种类型的指针或引用转换为另外一种。
参数:指针或引用(且要转换的类型中必须包含虚函数)
返回值: 转换成功则返回子类的地址,失败返回 NULL
1 | Flyable *p = new Bird(); // 多态父类(有虚函数)指针指向子类实例 |
4.2 typeid 函数
描述: 可以用来获取任何类型的类型描述。
参数: 参数类型非常广泛,包括
- 对象
- 任意数据类型本身(比如 int、double、自定义的结构体、类、枚举类型等)
- 指针
- 引用
返回值:type_info 对象
1 | class type_info |
注意
- typeid 返回一个 type_info 对象的引用
- 如果想通过基类的指针获得派生类的数据类型,基类必须带有虚函数
- 只能获取对象的实际类型
1 | // typeid 基本用法 |
4.3 RTTI 示例
案例一(代码模块化)
.
├── Bird.cpp
├── Bird.h
├── Flyable.h
├── Plane.cpp
├── Plane.h
├── main.cpp
└── makefile
main.cpp: 核心代码
1 |
|
1 | 4Bird |
案例二(all in one)
1 |
|
1 | Bus -- move |
5 异常处理
说明: 对有可能发生异常的地方做出预见性的安排。
- try-catch:尝试捕获
- throw:抛出异常
基本思想: 主逻辑和异常处理分离。
5.1 try-catch 和 throw
- 在 C++ 中,异常处理通常使用 try…catch…语法结构。
- 一个 try 语句可以对应一个或多个 catch 语句,但不能没有 catch 语句。
- C++ 中使用 throw 抛出异常,通过 catch 捕获异常
try 和 catch 是一对多的关系
1 | try |
可以使用 throw 抛出一段字符串,并通过 try-catch 捕获并关打印出来
1 | ... |
5.2 常见的异常
- 数组下标越界
- 除数为0
- 内存不足(现代计算机内存空间较大,较少遇到这种问题)
5.3 异常和多态的关系
说明: 在比较大的工程中,一般需要要自己定义异常类,可以为各种异常定义一个统一的接口,这时就用到了面向对象的多态。
1 | void fun1() |
5.4 异常综合 DEMO
1 |
|