大家好
欢迎回来继续学习
C++语言程序设计
这一节我们来学习虚函数
虚函数是实现动态绑定的函数
什么是动态绑定呢
为什么需要动态绑定呢
大家还记得第七章
那个不成功的例子吧
对了
在第七章那个例子中呢
我们没能实现期望中的
那个通用的显示函数
现在我们来回顾一下
第七章那个不成功的例子
那么在第七章这个例子中呢
我们是定义了基类1 基类2
还有派生类
这个Base2继承Base1
Derived继承Base2
每一个类中都有一个
原型完全相同的display函数
当时呢我们是期望
能够写这么一个通用的函数
这个函数接收基类的指针
作为参数
然后每次运行的时候
这个指针所指向的对象
很可能是不同的
比如说
它曾经指向基类Base1对象
指向Base2对象
指向derived对象
我们把这三个地址
都传给过这个fun函数
然后我们希望这个fun函数
每一次能够根据它
指针所指向的实际对象
去调用每个类
自己的display函数
但是运行结果呢
我们看到没有达到这个效果
所以当时
我给大家的建议是
不要重新定义
继承而来的非虚函数
当时我们还没有学虚函数
也就是这样的函数
达不到这个效果
你就不要这样写
写了以后呢
我们读程序的时候
预期的效果
和程序真正的效果
它不一样
会造成这种程序的可读性有问题
大家有没有深入去想一想
为什么不成功呢
不成功的原因就是
在编译阶段
编译器根据指针
无法去判断在运行时
它会指向一个什么类型的对象
所以呢
它只能说指针是什么类型的
它就调用那个类
定义的display函数
那这种情况下
我们特别希望告诉编译器
对了
在编译阶段
你没法正确地决定
那怎么办呢
你推迟这个决定
在编译的时候先别确定
这个display函数调用表达式
跟函数体
到底哪个函数体跟它对应
先别对应
把它留着
留到运行时再确定
那么运行时
当然就能够知道
指针在某个时刻指向的实际对象
是什么了
怎么告诉编译器这件事呢
就用一个virtual关键字
就这么简单
现在呢
我们把第七章
那个不成功的例题呢
再改编一下
现在我们在第七章
这个例题的基础之上呢
只做一点点修改
在这个display函数
前面加一个virtual关键字来修饰
那这个virtual的意思
就是告诉编译器
凡是你遇到对这样原型的
这个函数的调用
你都不要在编译的时候
马上做决定
决定它
该去调用哪个函数的函数体
先把这个置后
这是一个指示编译器
不要在编译阶段做静态绑定
要为运行阶段做动态绑定
做好准备
是这么一个意思
好 那我们再来看
现在呢
我们就不能把这个
display函数的实现
写在类体里面
作为内联函数了
因为
既然不要它在编译阶段处理
要求它在运行阶段
再去决定
对display的调用
该执行哪个函数体
而内联函数呢
是在编译阶段处理的
就要把它嵌入到程序代码中去
所以这两者显然矛盾
所以这样的虚函数
加了virtual的函数
都要在类外去实现函数体
不能写成内联的了
再看同样Base2也有display函数
同样的原型也写成虚函数
Derived也是
同原型的函数写成虚函数
都是在类外
有各自不同的实现
那现在我们再进行同样地测试
这一回呢
我们希望的效果就达到了
每次分别送给它
不同的基类派生类对象的地址
送到这个形参里面去
这个指针呢
在运行的时候
不同的时刻
它可能就指向了
不同的类型的对象
虽然都是这个基类指针
来调用display
但是运行时
能够正确地找到
每个对象自己的display去运行
为什么呢
因为我们说了它是虚函数
所以编译器在编译的时候
它不做决定
不确定该调用哪个函数体
而是到运行的时候
再确定该调用哪个函数体
现在大家看到
用了一个virtual关键字
一切问题就都迎刃而解了
用virtual关键字说明的函数呢
就是虚函数
虚函数是实现运行时多态的基础
那么C++中
你用virtual指定的函数呢
就是要实现动态绑定的函数
但是虚函数
必须是非静态的成员函数
也就是说
虚函数应该是属于对象的
不是属于整个类的
它是需要在运行的时候
用指针去定位到
它指向的对象是谁
然后决定调用哪个函数体
所以呢虚函数
当然得是属于对象的
而不是属于类的函数
那么虚函数经过派生以后呢
就可以实现这种运行中的多态
这个我们在刚才例子中呢
也已经看到过了
我们知道类的成员函数呢
有这样几种
最大多数是一般的
实现类的功能的成员函数
还有呢
两种特殊的函数
一类是构造函数
一个是析构函数
那么一般的成员函数呢
可以是虚函数
构造函数是不能是虚函数
析构函数可以是虚函数
首先呢我们来看一下
怎么样声明一般的虚成员函数
这个大家在刚才例子中
已经都看到了
特别简单
就加一个virtual关键字
在最前面就可以了
其他的没有什么差别
但是呢要注意
虚函数的声明
它只能出现在
类定义中的函数原型声明中
不能在成员函数实现的时候
去加这个virtual关键字
那么有了这个
virtual关键字的声明以后呢
在派生类中
就可以对基类中的成员函数
进行覆盖了
在第七章那个例子的最后
我给大家一个建议说
千万不要重写
继承而来的非虚函数
对吧 为什么呢
因为它如果不是虚函数
在第七章中我们看到了
它就达不到我们想达到的目的
那你何必去重写一个
一样原型的函数呢
在阅读的时候
还会引起一种误会
现在我们有了这个virtual关键字
用virtual关键字去把函数声明为虚函数以后
在派生类中
就可以对它进行覆盖
写一个相同原型的函数
就叫做覆盖
那么经过这样的覆盖呢
我们就可以实现运行时的多态性
还有呢
虚函数一般不要声明为内联函数
因为对虚函数的调用
需要进行动态绑定
也就是在运行时才绑定
而对内联函数的处理呢
它是在编译时进行的
是静态的
现在呢
我们对virtual关键字
做一个简单的小结
在基类中只要我们用virtual
去声明了一个虚函数
那么派生类继承了这个函数以后
它自动就是虚函数了
那么这个时候派生类
可以写一个相同原型的函数
来覆盖这个虚函数
这个相同原型的函数
写virtual关键字
或者不写virtual
它都是虚函数
既然在派生类中
你有些函数写不写virtual
它都是虚函数
那么编译器是通过什么来判定的
一个函数到底是不是虚函数呢
当然你自己写了virtual了
它肯定是虚函数
对于没有写virtual的
那编译器就会去判断
这个函数
它的函数名 参数表
还有它的返回值类型
以及
它的按照兼容规则规定的
这种指针引用的返回类型
是不是跟基类是一致的
也就是说
它的函数原型
跟基类的虚函数的函数原型
是不是一致
如果是一致的
就判定它是个虚函数
就要对它进行动态绑定
派生类中的虚函数呢
还会隐藏
基类中的同名函数的所有
其他的重载形式
那么这一点呢
大家也需要注意
如果你需要调用基类
同名函数的其他的重载形式的话
那你还要用类名
虽然说在派生类中
我们可以对继承而来的虚函数
进行覆盖
并且不写virtual关键字
但是一般呢
还是习惯于在派生类中
也用这个virtual关键字
这样程序的可读性会更好一些