C++学习之路—多态性与虚函数(一)利用虚函数实现动态多态性
(根据《C++程序设计》(谭浩强)整理,整理者:华科小涛,@http://www.cnblogs.com/hust-ghtao转载请注明)
多态性是面向对象程序设计的一个重要特征。顾名思义,多态性就是一个事物具有多种形态。在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为。也就是说,每个对象可以用自己的方式去响应共同的消息,所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。在C++中,多态性表现形式之一是:具有不同功能的函数可以用同一个函数名,这样就可以实现用一个函数名调用不同内容的函数。
从系统实现的角度来看,多态性分为两类:静态多态性和动态多态性。
静态多态性是通过函数重载实现。由函数重载和运算符重载(运算符重载实质上也是函数重载)形成的多态性属于静态多态性,要求程序在编译时就知道函数调用的全部信息,因此,在程序编译时系统就能决定要调用的是哪个函数。静态多态性的函数调用速度快、效率高,但是缺乏灵活性,在程序运行之前就已经决定了执行的函数和方法。
动态多态性是通过虚函数实现的。特点是:不在编译时确定调用的哪个函数,而是在程序运行过程中动态地确定操作所针对的对象。
这里先介绍动态多态性,静态多态性以后再介绍。
1 虚函数的作用
在同一个类中是不能定义两个名字相同、参数个数和类型都相同的函数的,否则就是“重复定义”。但在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数,这种情况是合法的,因为它们不在同一个类中,编译系统按照同名覆盖的原则决定调用的对象。
那么,能否用同一个调用形式来调用派生类和基类的同名函数。在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针来调用,要做的只是在调用前临时给指针变量赋予不同的值(使之指向不同的类对象)。C++中的虚函数就是用来解决动态多态问题的。所谓虚函数,就是在基类声明函数时虚拟的,并不是实际存在的函数,然后在派生类中才正式定义此函数。在程序运行期间,用指针指向某一派生类对象,这样就能调用指针指向的派生类对象中的函数,而不会调用其他派生类中的函数。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
例子如下:
1: class Student
2: {
3: public:
4: ...
5: void display () ; //输出数据成员 num name score
6: protected:
7: int num ;
8: string name ;
9: float score ;
10: };
11:
12: class Graduate : public Student
13: {
14: public:
15: ...
16: void display () ; //输出成员函数 num name score wage
17: private:
18: float wage ;
19: };
20:
21: int main()
22: {
23: Student stud1 ( 1001 ,"Li" , 87.5 ) ; //定义基类对象
24: Graduate grad1 ( 2001 , "Wang" , 98.5 , 1200 ) ; //定义派生类对象
25: Student* pt = &stud1 ; //定义基类指针变量,指向stud1
26: pt->display() ;
27: pt = &grad1 ; //基类指针变量指向派生类对象grad1
28: pt->display() ;
29:
30: return 0 ;
31: }
运行结果如下:
num:1001
name:Li
score:87.5
num:2001
name:Wang
score:98.5
我们本希望输出grad1的全部数据成员,但结果却不是这样,这是因为:本来,基类指针是用来指向基类对象的,如果用它指向派生类对象,则自动进行指针类型转换,将派生类对象的指针先转换为基类的指针,这样基类指针指向的是派生类中基类部分,所以只输出从基类继承过来的数据成员。Ofcourse,想要输出grad1的全部数据成员,可以通过对象名或指向派生类对象的指针变量来调用display() 。但是,如果该基类有多个派生类,每个派生类又产生新的派生类,每个派生类都有同名函数display,若在程序中需要调用不同类的同名函数,则上述方法就很不方便。
用虚函数就能解决这个问题。只需对原程序作一点修改,在Student类中声明display函数时,在最前面加一个关键字virtual:
1: virtual void display() ;
这样就把Student类的display函数声明为虚函数.其余部分不变,这次的运行结果是:
num:1001
name:Li
score:87.5
num:2001
name:Wang
score:98.5
wage:1200
现在用同一个指针变量,不但输出了stud1的全部数据,还输出了grad1的全部数据,说明已经成功调用了grad1的display函数。在基类中的display被声明成虚函数,在声明派生类时被重载,这时派生类的同名函数display就取代了基类中的虚函数。因此在使用基类指针指向派生类对象后,调用display函数时就调用了派生类的display函数。
虚函数的以上功能很有实际意义。在面向对象的程序设计中,经常会用到类的继承,目的是保留基类的特性,以减少新类的开发时间。但是,从基类继承而来的某些成员函数不完全适应派生类的需要,当把基类的某个成员函数声明为虚函数后,允许在派生类中对该函数重新定义,赋予它新的功能,并且可以通过指向基类的指针指向同一类族中不同类的对象,从而调用其中的同名函数。注意:当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数,但为使程序更加清晰,习惯上在每层声明该函数时都加virtual。
2 在什么情况下应当声明虚函数
在使用虚函数时,有两点需要注意:
(1)只能用virtual声明类的成员函数,把它作为虚函数,而不能将类外的普通函数声明为虚函数。
(2)一个成员函数被声明为虚函数后,在同一类族中的类就不能在定义一个非virtual的但与该虚函数具有相同的参数和函数返回值类型的同名函数。
是否应该把一个成员函数声明为虚函数,主要考虑以下几点:
(1)首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后功能是否会改变,如果希望更改其功能,一般应该将它声明为虚函数。
(2)如果成员函数在类被继承后功能无须更改,或派生类用用不到该函数,则不要把它声明为虚函数。
(3)应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问,则应当声明为虚函数。
(4)有时,在定义虚函数是,并不定义其函数体,即函数体为空。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。
3 虚析构函数
析构函数的作用是在对象撤销之前做必要的“清理现场”的工作。当派生类类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。但是,如果用一个基类指针指向一个用new建立的派生类的临时对象,在程序中用delete运算符撤销该对象时,会发生这样一种情况:系统只会执行基类的析构函数,而不会执行派生类的析构函数。
例如:
1: class Point //定义基类Point类
2: {
3: public:
4: Point(){} //Point类的构造函数
5: ~Point()
6: { //Point类的析构函数
7: cout << " executing Point destructor " << endl ;
8: }
9: };
10:
11: class Circle : public Point //定义派生类Circle类
12: {
13: public:
14: Circle(){} //Circle类的构造函数
15: ~Circle() //派生类的析构函数
16: {
17: cout << " executing Circle destructor " << endl ;
18: }
19: };
20:
21: int main ()
22: {
23: Point* p = new Circle ; //用new开辟动态存储空间
24: delete p ; //用delete释放动态存储空间
25: return 0 ;
26: }
在本程序中,p是指向基类的指针变量,指向用new开辟的动态存储空间,希望用delete释放p所指向的空间。但运行结果为:
excuting Point destrcutor
表示只执行了基类Point的析构函数,而没有执行派生类的构造函数。如果希望执行派生类Circle的析构函数,可以将基类的析构函数声明为虚函数,如下:
1: virtual ~Point()
2: {
3: cout << " executing Point destructor " << endl ;
4: }
则执行结果为:
excuting Circle destrcutor
excuting Point destrcutor
先调用了派生类的析构函数,再调用基类的析构函数。即当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,系统都会采用动态关联,调用相应的析构函数,对该对象进行清理工作。
如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数都自动成为虚函数,即使派生类的析构函数与基类的析构函数的名字不相同。最好把基类的析构函数声明为虚函数。这样将使所有派生类的析构函数自动成为虚函数。这样,如果在程序中显示的调用了delete运算符准备删除一个对象,则系统会调用相应类的析构函数。
C++学习之路—多态性与虚函数(一)利用虚函数实现动态多态性的更多相关文章
- 【编程题目】请修改 append 函数,利用这个函数实现两个非降序链表的并集
42.请修改 append 函数,利用这个函数实现(链表):两个非降序链表的并集,1->2->3 和 2->3->5 并为 1->2->3->5另外只能输出结 ...
- Python小白学习之路(十八)—【内置函数三】
一.对象操作 help() 功能:返回目标对象的帮助信息 举例: print(help(input)) #执行结果 Help on built-in function input in module ...
- python学习之路---day20--面向对象--多继承和super() 函数
一:python多继承 python多继承中,当一个类继承了多个父类时候,这个类拥有多个父类的所欲非私有的属性 l例子: class A: pass class B(A): pass class C( ...
- Spark学习之路 (十九)SparkSQL的自定义函数UDF
在Spark中,也支持Hive中的自定义函数.自定义函数大致可以分为三种: UDF(User-Defined-Function),即最基本的自定义函数,类似to_char,to_date等 UDAF( ...
- Python小白学习之路(十六)—【内置函数一】
将68个内置函数按照其功能分为了10类,分别是: 数学运算(7个) abs() divmod() max() min() pow() round() sum() 类型转换(24个) bo ...
- Hive学习之路 (九)Hive的内置函数
数学函数 Return Type Name (Signature) Description DOUBLE round(DOUBLE a) Returns the rounded BIGINT valu ...
- Spark学习之路 (十九)SparkSQL的自定义函数UDF[转]
在Spark中,也支持Hive中的自定义函数.自定义函数大致可以分为三种: UDF(User-Defined-Function),即最基本的自定义函数,类似to_char,to_date等 UDAF( ...
- IOS开发---菜鸟学习之路--(二十三)-直接利用键值对的方式来处理数据的感想
首先声明,本文纯粹只是做为本人个人新手的理解.文中的想法我知道肯定有很多地方是错的. 但是这就是我作为一个新人的使用方法,对于大牛非常欢迎指导,对于喷子请绕道而行. 由于这是早上跟我学长讨论数据处理时 ...
- Salesforce学习之路-developer篇(一)利用VS Code结合Git开发Salesforce
Part 1: 从Git中克隆代码到本地 git clone https://github.com/git/git Part 2: 在VS Code中安装Salesforce和Git插件 在VS Co ...
- Salesforce学习之路-developer篇(二)利用Jenkins和Bitbucket实现Salesforce的CI/CD功能
上文提到,基于CRM的二次开发是必不可少的,但是在实际项目中CI/CD是不可忽略的一个重要部分,与传统的Java,Python项目不同,如果对Salesforce进行持续集成和持续部署呢? 结合找到的 ...
随机推荐
- E - Phone List(字典序,string类型使用)
Description Given a list of phone numbers, determine if it is consistent in the sense that no number ...
- 数矩形(N - 暴力求解、打表)
数矩形 Description 给你一个高为n ,宽为m列的网格,计算出这个网格中有多少个矩形,下图为高为2,宽为4的网格. Input 第一行输入一个t, 表示有t组数据,然后 ...
- Week1(9月12日):很激动的第一次课
Part I:课程介绍 =========================== 1. 学时 8*16=128 2. 时间 周二1234,周五1234 3. 地点 E307 4. 考试方式 笔试+上机 ...
- Qt 操作 pdf 文件
写了好久的东西,不小心按了下返回键就没了.CSDN居然没自动保存,坑爹啊 原本还有很多信息的,现在直入正题吧. QT没有内置PDF操作的功能(其实有一个,QPrinter,不过只能写不能读,基本是半残 ...
- 基于 JVMTI 实现 Java 线程的监控(转)
随着多核 CPU 的日益普及,越来越多的 Java 应用程序使用多线程并行计算来充分发挥整个系统的性能.多线程的使用也给应用程序开发人员带来了巨大的挑战,不正确地使用多线程可能造成线程死锁或资源竞争, ...
- hdu1248
Problem Description 不死族的巫妖王发工资拉,死亡骑士拿到一张N元的钞票(记住,只有一张钞票),为了防止自己在战斗中频繁的死掉,他决定给自己买一些道具,于是他来到了地精商店前. 死亡 ...
- 变相的取消Datagridview控件的选中状态
思路:把每一列的文字颜色设为黑色,选中时候的背景为白色,颜色为黑色.每一列都这样设置,那么变相的达到了取消选中效果. 图:
- 从 Qt 的 delete 说开来
原地址:http://blog.csdn.net/dbzhang800/article/details/6300025 在C++中学习过程中,我们都知道: delete 和 new 必须 配对使用(一 ...
- HDU 4160 Dolls (最小路径覆盖=顶点数-最大匹配数)
Dolls Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total Submiss ...
- grunt getHTML
var Base = require( "../common/base" ) , HandlerBase = require( "../common/handlerBas ...