当前课程知识点:基于Linux的C++ > 第九讲 类与对象 > 9.13 多态(二) > LinuxCPP0913
有一种很特殊的实现
就叫纯虚函数
你就想我们刚才那个例子
三个类:Account、CheckingAccount、SavingsAccount
Account这个账户本身
实际上充当的是一个抽象的账户
也就是说 实际上在它里面
打印一个余额是没有意义的
因为不管怎么样
它要么是结算账户要么是储蓄账户
我们不存在一个不属于储蓄账户
也不属于结算账户的这样一个抽象的账户
在实际的程序运行过程中
这个事情是不存在的
我们也许会有一个这样的指针
也许会有这样一个引用量
但是一定没有真实的Account账户的存在
我们不会构造它的
这是一个非常重要的地方
我们要构造的要么就是结算账户
要么就是储蓄账户
不会构造一个抽象的Account账户
它只是表达我们整个类库设计的时候
最顶层的抽象的一个模式
表示所有的账户类的最顶层的共性
所以为它实现PrintBalance是没有意义的
就像我们的代码中
我实际上是向标准错误流里面
输出一串信息
并不是真正地输出它的账户余额信息
明确了这一点 那我们就说
这样的一个函数实际上我们不该实现它
实现了也没有意义 反正我们也不会调用
那我们怎么办呢
C++就为我们提供了一个
纯虚函数的机制来实现它
像这样的一个纯虚函数
标准的格式就是virtual
后面跟着它的函数原型
然后把它初始化成0
等号还是初始化记号
后面是一个0 其实就是个NULL
就表示这一个函数将没有任何实现
直接就初始化成0
表示这个函数没有代码体的
但不是只声明不定义
只声明不定义 它也叫没有
但是编译器——如果你调用它的时候
就会告诉你它有问题
因为那个函数不存在
所以调用就会出错 编译就通不过
这个呢 一个纯虚的函数
编译器也会告诉你
你不能调用纯虚的函数
但是实际上 这个函数指针是存在的
在这个对象的虚拟表里边
f这个函数的虚拟表指针(虚函数指针)是存在的
只不过那里被填了0
表示这个函数的入口地址是0
函数本身不存在
但是这个指针是存在的
特别注意这一条
所有的带有纯虚函数的类
在我们C++里边都叫纯虚类
更一般地我们其实应该称它为抽象的类
因为我们绝不可能构造这样类的一个对象
语言机制不允许的
因为里面有一个函数没实现
不允许你这么做
这样的话就是一个抽象的一个类
它最主要目的是作为类继承的顶层
一般来讲可能是最上层
也可能如果下一层需要抽象
它还可以是个抽象类
都是按照像这样的格式来定义的
也就是说 它往往都位于类库层次的顶层
它形成的是一个抽象的机制
它不承担实际的工作
这个就叫抽象类
对于类库的程序设计是非常非常有用的
还有一个细节 需要特别强调
就是析构函数 大部分情况下面
我们应该将析构函数定义成虚拟的
也就是虚析构函数
这是在类库继承层次下边
保持整个类的多态性的一个最基本的需要
我们需要用到它 如果是一个非虚的
它实际上就和特定的类是相关的了
当你在指向基类的一个指针上
去销毁它的派生类的那个对象的时候
如果不是一个虚的析构函数
那么实际上是没有办法正确销毁的
所以析构函数往往都是虚的
我们可以看这样的一个例子
定义一个Point的纯虚类
我们前面不讲了吗
我们的那个Point类
它作为一个抽象类的基础
其实是带了x和y
所以它实际上对应的是二维点
而不是真正的抽象的一个点
真正抽象的一个点应该是0维的点
所以属性都不存在
很明确吧 应该是这么写的
那么对于这样一个类来讲
如果我想定义它的一个Print成员函数
显然我不知道该写什么
其实就是我什么都不应该写
好吧 我把Print定义成纯虚函数
virtual void Print() const = 0
这本身是个常函数
因为它不改变数据对象的值
前面是一个virtual 虚函数
要求它保持多态性
因为它没有数据可操作
所以把它定义成纯虚函数 初始化成0
这样的话 Point就表示一个纯虚类
它就是足够抽象的一个顶层逻辑
所有的点都将从我们的class Point派生下去
对于点库 它的一个继承层次来讲
Point就是它们唯一的根
所有的点都从它继承下来
这就是纯虚类在类库设计层次上面
所起到的最重要的一个作用
virtual void Print() const = 0
我们可以在此基础上定义Point2D
虚继承 单根的话 virtual不写 没问题
定义它的构造函数、Get/Set函数
定义它的虚函数Print
我们实现的应该是virtual void Print() const
后面不能再写初始化成0了
再写初始化0 表示它还是纯虚函数
然后Point3D类 Point3D从Point2D继承
添加一个_z成员 然后Print
实现我们的virtual void Print()const这样的一个函数
还要写它的虚函数嘛
3D的数据处理和2D数据肯定也不一样
当然构造函数、Get/Set函数我们也要添上去
接下来就是我们的实现代码
对于2D的Print 我们调用cout输出两维数据
对于3D的这样一个点
我们就用cout输出一个三维的数据x、y、z
写完了 这个就是我们类库的架构的实现
你注意看这三个类 顶层类是Point
然后中间那一层类是Point2D
最底层的那个类是Point3D
这不就形成了类的继承层次了吗
这是基本的类库的架构设计模式
都是按照这个方式写的
在Point类里面 Print是纯虚函数
Point2D和Point3D分别实现了对应的Print函数
接下来是主程序
定义一个pt1 为指向Point类的指针
然后new一个Point2D给它
然后定义一个pt2
同样是一个指向Point的指针
然后new一个Point3D给它
我们会创建一个二维点的对象
也会创建一个三维点的对象
分别把它们的地址传给pt1和pt2
pt1也好 pt2也好 都是指向Point类的指针
都是指向基类的指针
不是指向派生类的指针
但是我们构造的都是派生类的对象
让基类的指针指向派生类
然后我们在基类指针上面调用Print
没有问题
它调用的就都将是派生类的那个虚函数
这是由多态所保证的
要做的就是这个事
它是二维点 我就处理二维点
如果是三维点 我就处理三维点
不管怎么样 它们都是点
这个是非常非常重要的
如果你为它们写了虚析构函数
那实际上很清楚
你在pt1上面删除和pt2上面删除
删除都是它对应的二维点和三维点的那个对象
绝不会删除抽象的那个点的那个对象
特别注意这一条
-1.1 提纲
-1.2 程序设计的基本概念
-1.3 简单C/C++程序介绍
-1.4 程序设计的基本流程
-1.5 基本语法元素
-1.6 程序设计风格
-1.7 编程实践
-第一讲 C/C++基本语法元素--编程实践提交入口
-2.1 提纲
-2.2 结构化程序设计基础
-2.3 布尔数据
-2.4 分支结构
-2.5 break语句
-2.6 循环结构
-2.7 编程实践
-第二讲 程序控制结构--编程实践提交入口
-3.1 提纲
-3.2 函数声明、调用与定义
-3.3 函数调用栈框架
-3.4 编程实践
-第三讲 函数--编程实践提交入口
-4.1 提纲
-4.2 算法概念与特征
-4.3 算法描述
-4.4 算法设计与实现
-4.5 递归算法(一)
-4.6 递归算法(二)
-4.7 容错与计算复杂度
-4.8 编程实践
-第四讲 算法--编程实践提交入口
-5.1 提纲
-5.2 库与接口
-5.3 随机数库(一)
-5.4 随机数库(二)
-5.5 作用域与生存期
-5.6 典型软件开发流程(一)
-5.7 典型软件开发流程(二)
-5.8 编程实践
-第五讲 程序组织与开发方法--编程实践提交入口
-6.1 提纲
-6.2 字符
-6.3 数组(一)
-6.4 数组(二)
-6.5 结构体
-6.6 编程实践
-第六讲 复合数据类型--编程实践提交入口
-7.1 提纲
-7.2 指针基本概念
-7.3 指针与函数
-7.4 指针与复合数据类型(一)
-7.5 指针与复合数据类型(二)
-7.6 字符串
-7.7 动态存储管理(一)
-7.8 动态存储管理(二)
-7.9 引用
-7.10 编程实践
-第七讲 指针与引用--编程实践提交入口
-8.1 提纲
-8.2 数据抽象(一)
-8.3 数据抽象(二)
-8.4 链表(一)
-8.5 链表(二)
-8.6 链表(三)
-8.7 链表(四)
-8.8 函数指针(一)
-8.9 函数指针(二)
-8.10 抽象链表(一)
-8.11 抽象链表(二)
-8.12 编程实践
-第八讲 链表与程序抽象--编程实践提交入口
-9.1 提纲
-9.2 程序抽象与面向对象
-9.3 类类型
-9.4 对象(一)
-9.5 对象(二)
-9.6 类与对象的成员(一)
-9.7 类与对象的成员(二)
-9.8 类与对象的成员(三)
-9.9 继承(一)
-9.10 继承(二)
-9.11 继承(三)
-9.12 多态(一)
-9.13 多态(二)
-9.14 编程实践
-第九讲 类与对象--编程实践提交入口
-10.1 提纲
-10.2 四则运算符重载(一)
-10.3 四则运算符重载(二)
-10.4 关系与下标操作符重载
-10.5 赋值操作符重载(一)
-10.6 赋值操作符重载(二)
-10.7 赋值操作符重载(三)
-10.8 赋值操作符重载(四)
-10.9 赋值操作符重载(五)
-10.10 流操作符重载(一)
-10.11 流操作符重载(二)
-10.12 流操作符重载(三)
-10.13 操作符重载总结
-10.14 编程实践
-第十讲 操作符重载--编程实践提交入口
-11.1 提纲
-11.2 泛型编程概览
-11.3 异常处理机制(一)
-11.4 异常处理机制(二)
-11.5 运行期型式信息(一)
-11.6 运行期型式信息(二)
-11.7 模板与型式参数化
-11.8 题外话:术语翻译
-11.9 泛型编程实践(一)
-11.10 泛型编程实践(二)
-11.11 泛型编程实践(三)
-11.12 泛型编程实践(四)
-11.13 泛型编程实践(五)
-11.14 泛型编程实践(六)
-11.15 泛型编程实践(七)
-11.16 泛型编程实践(八)
-11.17 泛型编程实践(九)
-11.18 泛型编程实践(十)
-11.19 编程实践
-第十一讲 泛型编程--编程实践提交入口
-12.1 提纲
-12.2 程序执行环境(一)
-12.3 程序执行环境(二)
-12.4 程序执行环境(三)
-12.5 程序执行环境(四)
-12.6 输入输出(一)
-12.7 输入输出(二)
-12.8 文件系统
-12.9 设备
-12.10 库(一)
-12.11 库(二)
-12.12 makefile文件(一)
-12.13 makefile文件(二)
-12.14 makefile文件(三)
-12.15 编程实践
-第十二讲 Linux系统编程基础--编程实践提交入口
-13.01 提纲
-13.02 进程基本概念
-13.03 信号
-13.04 进程管理(一)
-13.05 进程管理(二)
-13.06 进程管理(三)
-13.07 进程间通信(一)
-13.08 进程间通信(二)
-13.09 进程间通信(三)
-13.10 进程间通信(四)
-13.11 进程池
-13.12 编程实践
-第十三讲 进程编程--编程实践提交入口
-14.1 提纲
-14.2 线程基本概念
-14.3 线程管理(一)
-14.4 线程管理(二)
-14.5 线程管理(三)
-14.6 线程管理(四)
-14.7 线程同步机制(一)
-14.8 线程同步机制(二)
-14.9 C++11线程库(一)
-14.10 C++11线程库(二)
-14.11 C++11线程库(三)
-14.12 C++11线程库(四)
-14.13 C++11线程库(五)
-14.14 编程实践
-第十四讲 线程编程--编程实践提交入口
-15.1 提纲
-15.2 Internet网络协议
-15.3 套接字(一)
-15.4 套接字(二)
-15.5 编程实践
-第十五讲 网络编程--编程实践提交入口