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进行持续集成和持续部署呢? 结合找到的 ...
随机推荐
- PHP学习笔记1-常量,函数
常量:使用const(php5)声明,只能被赋值一次,php5以下版本使用define: <?php const THE_VALUE = 100;//PHP5中才有const echo THE_ ...
- js window.onload事件
1.最简单的调用方式 直接写到html的body标签里面,如: ? 1 2 3 4 <html> <body onload="func()"& ...
- Java--再次理解多态
Java中多态性(polymorphism)的实现 什么是多态 1. 面向对象的三大特性:封装.继承.多态.从一定角度来看,封装和继承几乎都是为多态而准备的.这是我们最后一个概念,也是最重要的知识点. ...
- python模块介绍- multi-mechanize 性能测试工具
python模块介绍- multi-mechanize 性能测试工具 2013-09-13 磁针石 #承接软件自动化实施与培训等gtalk:ouyangchongwu#gmail.comqq 3739 ...
- windows下以指定用户访问SMB服务器进行读写
需求:最近要开发某系统前端界面,但是该系统是部署在linux服务器上,前端是用php开发,实时调试运行需要linux下系统环境支持, 每次修改都需要手动传到服务器上,尤其是debug阶段,每修改一点就 ...
- 在C++中使用C#编写的类2
在那篇<在C#中使用C++编写的类>中我介绍了如何在C#中使用C++编写的类.可是由于C#在用户界面设计.数据库存储和XML文件读取等方面的优势,有时候也会出现要在C++中使用C#编写的类 ...
- 1688: [Usaco2005 Open]Disease Manangement 疾病管理( 枚举 )
我一开始写了个状压dp..然后没有滚动就MLE了... 其实这道题直接暴力就行了... 2^15枚举每个状态, 然后检查每头牛是否能被选中, 这样是O( 2^15*1000 ), 也是和dp一样的时间 ...
- tomcat启动后ids页面无法访问
修改servers-->tomcat6.0-->server.xml <Context docBase="/tds7030-web" path="&qu ...
- linux指令(目录类操作指令)
pwd 显示当前所在的工作目录 cd 目标目录 例如cd /boot/grub 从当前目录切换到某个目录 cd 切换到根目录 cd.. 切换到当前目录的上层目录 ls 显示当前目录下的内容 ...
- CVPapers论文整理工具-开源
一.工具介绍及运行实例 相信计算机视觉领域的同道中人都知道这个Computer Vision Resource网站, http://www.cvpapers.com/ 网页部分截图如下: 可以看到有 ...