当前课程知识点:C++语言程序设计进阶 > 第八章 多态性 > 虚函数 > 虚函数
问题:还记得第7章的例子吗?
#include <iostream> using namespace std; class Base1 { //基类Base1定义 public: void display() const { cout << "Base1::display()" << endl; } }; class Base2: public Base1 { //公有派生类Base2定义 public: void display() const { cout << "Base2::display()" << endl; } }; class Derived: public Base2 { //公有派生类Derived定义 public: void display() const { cout << "Derived::display()" << endl; } }; void fun(Base1 *ptr) { //参数为指向基类对象的指针 ptr->display(); //"对象指针->成员名" } int main() { //主函数 Base1 base1; //声明Base1类对象 Base2 base2; //声明Base2类对象 Derived derived; //声明Derived类对象 fun(&base1); //用Base1对象的指针调用fun函数 fun(&base2); //用Base2对象的指针调用fun函数 fun(&derived); //用Derived对象的指针调用fun函数 return 0; }
现在我们来改进一下第7章的程序
#include <iostream> using namespace std; class Base1 { public: virtual void display() const; //虚函数 }; void Base1::display() const { cout << "Base1::display()" << endl; } class Base2::public Base1 { public: virtual void display() const; }; void Base2::display() const { cout << "Base2::display()" << endl; } class Derived: public Base2 { public: virtual void display() const; }; void Derived::display() const { cout << "Derived::display()" << endl; } void fun(Base1 *ptr) { ptr->display(); } int main() { Base1 base1; Base2 base2; Derived derived; fun(&base1); fun(&base2); fun(&derived); return 0; }
用virtual关键字说明的函数
虚函数是实现运行时多态性基础
C++中的虚函数是动态绑定的函数
虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现运行过程中的多态。
一般成员函数可以是虚函数
构造函数不能是虚函数
析构函数可以是虚函数
虚函数的声明
virtual 函数类型 函数名(形参表);
虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
在派生类中可以对基类中的成员函数进行覆盖。
虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的。
派生类可以不显式地用virtual声明虚函数,这时系统就会用以下规则来判断派生类的一个函数成员是不是虚函数:
该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型;
该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、引用型的返回值;
如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数。
派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式。
一般习惯于在派生类的函数中也使用virtual关键字,以增加程序的可读性。
大家好
欢迎回来继续学习
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关键字
这样程序的可读性会更好一些
-导学
--导学
-继承的基本概念和语法
-第七章 继承与派生--继承的基本概念和语法习题
-继承方式
-第七章 继承与派生--继承方式
-基类与派生类类型转换
-第七章 继承与派生--基类与派生类类型转换
-派生类的构造和析构
--派生类的构造函数
--派生类的析构函数
--第七章 继承与派生--派生类的构造和析构
-派生类成员的标识与访问
--虚基类
-第七章 继承与派生--派生类成员的标识与访问
-小结
--小结
-综合实例
--第七章综合实例
-实验七
--实验七
-导学
--导学
-第八章 多态性--导学
-运算符重载
--运算符重载的规则
-第八章 多态性--运算符重载
-虚函数
--虚函数
--虚析构函数
--虚表与动态绑定
-第八章 多态性--虚函数
-抽象类
--抽象类
--第八章 多态性--抽象类
-override与final
-第八章 多态性--override与final
-小结
--第八章小结
-综合实例
--第八章综合实例
-实验八
--实验八
- 第八章讲义
-导学
--导学
-模板
--函数模板
--类模板
-第九章 模板与群体数据--模板
-线性群体
--线性群体的概念
-第九章 模板与群体数据--线性群体
-数组
--数组类模板
-链表
--链表类模板
-第九章 模板与群体数据--链表
-栈
--栈类模板
--栈类模板课后习题
--例9-9 栈的应用课后习题
-队列
--队列类模板
-第九章 模板与群体数据--队列
-排序
--排序概述
--插入排序
--选择排序
--交换排序
-第九章 模板与群体数据--排序
-查找
--查找
--查找课后习题
-小结
--小结
-综合实例
--综合实例
-实验九
--实验九
- 第九章讲义
-导学
--导学
-泛型程序设计及STL的结构
--STL简介
-第十章 泛型程序设计与C++标准模板库--泛型程序设计及STL的结构
-迭代器
--迭代器
-第十章 泛型程序设计与C++标准模板库--迭代器
-容器的基本功能与分类
-第十章 泛型程序设计与C++标准模板库--容器的基本功能与分类
-顺序容器
--顺序容器的特征
--第十章 泛型程序设计与C++标准模板库--顺序容器
-关联容器
--集合
--映射
-第十章 泛型程序设计与C++标准模板库--关联容器
-函数对象
--函数对象
--函数适配器
-算法
--算法
-小结
--第十章小结
-综合实例
--综合实例
-实验十
--实验十
- 第十章讲义
-导学
--导学
-I/O流的概念及流类库结构
-第十一章 流类库与输入/输出--I/O流的概念及流类库结构
-输出流
--输出流概述
--向文本文件输出
--向二进制文件输出
--向字符串输出
-第十一章 流类库与输入/输出--输出流
-输入流
--输入流概述
--输入流应用举例
--从字符串输入
-第十一章 流类库与输入/输出--输入流
-输入/输出流
--输入/输出流
-第十一章 流类库与输入/输出--输入/输出流
-小结
--小结
-综合实例
--综合实例
-实验十一
--实验十一
- 第十一章讲义
-导学
--第12章导学
-异常处理的思想与程序实现
-第十二章 异常处理--异常处理的思想与程序实现
-异常处理中的构造与析构
-第十二章 异常处理--异常处理中的构造与析构
-标准程序库异常处理
-第十二章 异常处理--标准程序库异常处理
-小结
--第12章小结
-综合实例
--综合实例
-实验十二
--实验十二
- 第十二章讲义