当前课程知识点:基于Linux的C++ > 第九讲 类与对象 > 9.12 多态(一) > LinuxCPP0912
最后一节是多态 什么叫多态呢
多态其实就是一种物体的几种不同的形态
所以叫多态 听上去很高级
在我们面向对象程序设计中
多态性是非常重要的一个概念
想写好程序 不理解多态性肯定是不行的
但实际上 这个概念我们一直在用
而我们压根就没有注意到
首先 你从小学开始
你用到的那个操作就是多态的
你比如说加法 你一开始学1+1=2
后来你学1.0+2.0=3.0
也就是先是整数加法
后来你学了小数加法
后来你又学了整数和小数加法
要小数点对齐
再后来你又学了代数的加法
再后来呢 你又会学抽象代数的加法
所有的这些操作 你注意看
这个加法操作本身实际上是多态的
就一个加法符号
我们其实把它理解成多态的一个加法操作
它在整数加法上 在小数加法上
在代数加法上
它的做法可能都是不一样的
这个其实就叫多态
到我们计算机里边 那更是完全不一样了
因为整数加法指令是整数加法指令
小数加法指令是小数加法指令
那是两个不同指令
同样的一个加法操作
是会翻译成两条不同指令的
压根就不是一回事啊
所以那个加法一定是个多态的
所以你看我们一直就在用它
而我们编程真地就离不开它
也就是多态的目的就是为了让不同对象
在接受到同样消息的时候
能够进行不同的响应
我要做一个加法操作
传我整数 传我浮点数 我会做不同指令
一个类的成员函数在不同的类下边
应该做不同响应
我传给基类 这个函数应该做这件事情
我传给派生类 它应该做另外一件事情
这个就叫多态 同一个函数做不同的事
就看它是处于基类还是派生类的
多态性所对应的这个现象
就是对应于同样的成员函数的名称
它在不同的基类和派生类里面
执行的是不同的代码 不同的函数体
所以实现的时候我们使用virtual这个关键字
来声明这个成员函数
就让这个成员函数成为多态的
注意 多态性只影响函数
数据成员不存在多态这个概念
只有成员函数 因为它对应是操作
所以virtual只作用在我们的成员函数上面
我们在成员函数上面写了一个virtual
就表示这个成员函数将成为一个虚函数
维持它的多态性
这个维持 是指从这个类把这个函数
定义成虚函数之后就开始了
它的派生类里边
不管这个同名的函数有没有写virtual
它也是virtual的
也就是说 一日为virtual 永远为virtual
注意这个概念
声明的格式就是函数的原型前面加virtual
这个virtual只需要写在声明里
在函数的实现时候不需要写
我们看这个例子
我们定义了三个对象 一个叫账户
我们这个例子做什么用的呢
实际上处理的是一个银行账户
同学们知道
这个银行账户实际上是分成两大类
一类是储蓄账户
我们平时用的存钱、取钱
像活期存款、定期存款
那个就叫储蓄账户
另外是给予你做结算的目的的
主要是企业在用的那种结算的账户
和我们储蓄账户是不一样的
这是两类不同的账户
我们编程实现它的时候
就必须能够体现这一点
所以我们必须为储蓄账户和结算账户
定义不同的类 它们显然都是相关的
因为它们都是银行的账户
所以我们需要定义一个账户的基类
这个例子里边 我们就定义了这样三个类
一个是基类 抽象的账户 class Account
这里边定义了三个成员函数
只有一个私有的数据成员_balance
表示这个账户的余额
这个就是我们的账户这个类的定义
实现它 内联GetBalance
return_balance就行了
因为这个构造函数也被我们内联了
直接定义在这个类定义里边了
所以它已经被内联了
我们就把GetBalance也定义成内联的
接下来需要实现的就只有一个Print函数了
接下来是两个账户 CheckingAccount
我们的结算账户 它从Account账户公有继承
有自己的构造函数 有PrintBalance
新写的一个函数
还有一个类是储蓄账户 SavingsAccount
也是从Account账户公有继承下来
同样有一个构造函数 有一个PrintBalance函数
你注意看 不管是CheckingAccount
还是SavingsAccount
里边都有两个PrintBalance函数
一个是从基类继承下来的
一个是自己实现的
现在我们要实现这三个PrintBalance函数
对于一个标准的账户来讲
因为它是一个抽象的账户
所以实际上我们在Account这个账户上
我PrintBalance 想打印余额
那实际上是不可以打印的
所以我们cerr 直接向标准错误流里面
输出“余额不可用”信息就完了
当我们在结算账户和储蓄账户打印的时候
那我们就直接就打印它的余额 就完了
代码倒很简单
就都是cout和cerr这样的一个输出
关键我们来看我们程序的使用
我们的main函数里边
定义了一个指向结算账户的一个指针checking
然后new一个CheckingAccount
构造一个结算账户出来
当然是动态构造的
然后把它的地址赋值给checking
然后我又动态构造了一个储蓄账户出来
把它的地址赋值给Savings
Savings是指向SavingsAccount类的一个指针
现在我们定义一个
指向Account类的一个指针 account
然后我把它初始化成checking
我用一个结算账户
去初始化我们的一个抽象账户
我们在account上面调用PrintBalance
想打印我们的余额 savings赋值给account
然后在account上面
再次调用PrintBalance打印余额
然后删除checking 然后删除Savings
代码倒很简单
两个指针分别指向结算账户和储蓄账户
然后再定义指向抽象账户的一个指针
分别用结算账户和储蓄账户指针赋值过去
然后在抽象账户上面打印余额
这个程序的输出结果将会是什么呢
回忆我们前面讲的
同学们现在一定就知道
它打印出来的东西都是cerr
因为我们只能调用Account上面的
那个PrintBalance那个函数
指向基类的一个指针
不管接受的是基类的对象的地址
还是派生类对象的地址
都只能访问它的基类部分
所以访问的都是基类的对象
调用的都是基类的成员函数
你操纵的结果就是cerr
向标准错误流里边输出我们的错误信息
不能真正打印余额
因为你去银行处理这个东西的时候
你说我想看看我银行卡里边有多少钱
就是余额是多少 让她 打印一下
你管的账户叫储蓄账户还是结算账户呢
所以真实来讲 它就是一个账户
我就应该能够打印出来它的余额出来才行
这是非常重要的一个地方
也就是说 我们要的就是
如果account是一个指向基类的指针
那么当我们用它指向派生类的时候
我们应该调用派生类的那个成员函数
把它的余额打印出来
而不是调用基类的那个函数
当在非虚函数情况下
这个工作是做不了的
所以我们必须使用虚函数
我们看我们虚函数的版本
我们简单地在Account这个类的定义处
将PrintBalance这个函数写成virtual
本身这个函数是个const类型的
它并不需要改变_balance的值
所以它本身是个const
最前边要加上virtual表示它是个虚函数
如果在Account这个类上边
将PrintBalance定义成了一个虚函数
那么一切就不同了
我们在CheckingAccount和SavingsAccount下边
继续将PrintBalance这个函数定义成虚函数
因为我们要在这两个账户下边重新地实现它
我们要重新写一遍
所以就要重新把它定义成虚函数
因为我们刚才讲了
一日为虚 终日为虚嘛
既然已经在Account账户上边
将PrintBalance定义成虚函数了
所以在CheckingAccount和SavingsAccount上边
重写PrintBalance的时候
前边不写virtual 它也自动是virtual
不过这可能对你会造成一种困扰
你可能看派生类的时候
就不知道这个函数到底是虚的还是不是虚的
所以我建议同学们 只要它应该是虚的
哪怕是在派生类继续是虚的
所以前面都要加上virtual
反正加上不为错
然后我们按照刚才那个代码去调用
定义一个指向CheckingAccount账户的指针
定义一个指向Savings账户的指针
然后构造这两个对象 分别初始化给它们
然后我将checking赋值给Account
然后我在Account上面调用PrintBalance
然后把Savings再赋值为Account
然后在Account上面再调用一次PrintBalance
最后你来看我们这个程序的运行结果
你会发现当第一次打印PrintBalance的时候
它将调用CheckingAccount类的
那个PrintBalance函数
当它第二次调用PrintBalance的时候
它将打印SavingsAccount账户里面的内容
它将调用SavingsAccount那个里面的
成员函数PrintBalance
也就是说 我这样一个指向基类的指针account
将能够根据它实际指向的那个对象的类型
来调用对应的恰当的那个虚函数
这是一个基类指针
如果它指向基类 它就调用基类的PrintBalance
如果指向派生类
它就调用派生类的PrintBalance
不管那个派生类是CheckingAccount
还是SavingsAccount
总之那个派生类是哪一个
它就调用哪一个类的对应虚函数
很明确吧 这个就叫自适应
它能够自动地适应 它所指向的目标类
它自然就体现出了多态
这个就是虚函数最重要的一个地方
在讲虚函数的时候
有一点是需要强调的
如果基类中有一个虚函数
你的派生类里边不想动它
那你怎么办呢 你可以不写
因为派生类自动地能够继承
基类的全部数据对象和成员函数
所以基类的那个虚函数
自动地在你的派生类中就存在了
你不写它也有 只不过它的行为
和基类的那个函数一模一样
你写了 就意味着新写的这个函数
将会替换基类的对应的那个虚函数
怎么实现的呢 在C++代码里边
它会为每一个这样的类的一个对象
维持着一个虚拟表
我们称为虚拟表的指针
用一个虚拟表的指针指向这样一个虚表
每一个虚表里边就会记录
你这个类所实现所有的虚函数的入口地址
当它是一个基类的时候
它就把基类虚函数的入口地址写进去
如果它是一个派生类的时候
它就把派生类的虚拟函数的基地址写进去
当你构造的是一个派生类对象的时候
它写的实际上是派生类的
那个虚函数的入口地址
所以即使你是使用一个指向基类的指针
指向这个派生类的对象
当它调用那个虚函数的时候
它一查那个虚拟表
查到的依然是派生类的
那个虚函数的入口地址
而不是基类的那个虚函数的入口地址
所以我们的指向基类的那个指针
才能够调用派生类的虚函数
如果是非虚的函数 这个事情是做不了的
它是哪一个类
它就调用哪一个类的对应函数
想调用其它类函数 你必须解析它
调用派生类的函数 它看不见 调用不了
所以它是做不到的
而只有虚函数才能够做到这一点
非常非常重要的机制
-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 编程实践
-第十五讲 网络编程--编程实践提交入口