12.3C++虚函数、虚函数的作用和使用方法
- 格式:doc
- 大小:80.05 KB
- 文档页数:5
c++虚函数作用及底层原理C++是一种面向对象的编程语言,并支持多态性的实现。
其中,虚函数是C++中实现多态性的主要机制之一。
虚函数是一种特殊的成员函数,可以在派生类中重写,并通过基类指针或引用的间接方式调用派生类的实现。
虚函数的作用:1. 实现动态绑定:实现多态性的关键是在运行时确定函数的具体实现。
虚函数通过将函数调用的确定延迟到运行时,而不是在编译时确定,从而实现动态绑定。
2. 多态性:允许派生类重写基类的函数,并使用基类指针或引用调用派生类的实现。
这种多态性的实现可以增强代码的灵活性和可重用性。
3. 实现抽象类:虚函数也可以用于实现抽象类,即只声明函数接口而没有实现。
这样,派生类必须实现虚函数才能被实例化。
虚函数的底层原理:虚函数的实现需要两个关键组件:虚函数表(vtable)和虚函数指针(vptr)。
1. 虚函数表:每个包含虚函数的类都有一个虚函数表,其中包含了类中所有虚函数的地址。
虚函数表是一个静态数据结构,只有一个虚函数表,且在编译时生成。
2. 虚函数指针:每个包含虚函数的类的对象都有一个虚函数指针,指向其所属类的虚函数表。
虚函数指针是一个指向虚函数表的指针,指针的值在程序运行时动态确定。
当调用虚函数时,程序首先通过对象的虚函数指针找到其所属类的虚函数表,然后查找相应的虚函数地址,最终调用正确的虚函数实现。
这样,虚函数的实现在运行时动态确定,实现了动态绑定和多态性。
总之,虚函数是C++中实现多态性的主要机制之一,通过虚函数表和虚函数指针的使用,实现了动态绑定和多态性的实现。
虚函数的应用可以增强代码的灵活性和可重用性。
虚函数纯虚函数
1虚函数
虚函数是C++中一种特殊的成员函数,它的行为依赖于调用对象的类型,而不依赖与调用的对象的具体实例。
它通常用在基类和派生类之间实现多态性,这在允许基类指针或者引用指向派生类时非常有用。
虚函数是通过使用关键字virtual来定义的,以指出它具有多态性,虚函数可以有自己的实现或可以被派生类覆写。
2纯虚函数
纯虚函数是一种特殊的虚函数,它通常用在基类中,它仅仅是个标识,指示编译器,子类中这个函数必须被实现,否则就会报错,它也可以在声明的时候提供一个简单的函数实现,但是这个实现不被调用。
使用纯虚函数的工作原理是,父类声明函数而子类实现函数。
派生类可以覆写或重定义父类的纯虚函数而实现函数的功能,可以创造出有更多调用方式的函数。
结论:虚函数和纯虚函数都是C++中使用多态性实现动态绑定的一种重要方法,它们常用于基类和派生类之间实现多态性和函数重载,它们可以让我们更加便捷优秀地实现继承类之间的调用,提高程序的质量。
c 虚函数C++中的虚函数是一种关键的特性。
虚函数是指在基类中声明的函数,它可以被派生类重写。
虚函数是多态的基础,它使得我们可以通过基类指针或引用来调用派生类的函数。
接下来,我将为您介绍C++虚函数的一些重要概念和用法。
1.基本语法:在C++中,我们可以通过在函数声明前加上virtual关键字来创建虚函数。
虚函数可以是纯虚函数,它表示基类没有实现函数,而是将其留给派生类去实现。
2.多态性:作为多态性的基础,虚函数使得我们可以通过基类指针或引用来调用派生类的函数。
这种多态性被称为运行时多态,也就是我们在程序运行期间选择使用的函数。
3.覆盖和重载:虚函数的覆盖是指派生类重写基类中的虚函数。
一旦派生类覆盖了基类中的虚函数,我们就无法通过基类指针或引用调用该函数的基类实现。
而重载是指在同一范围内使用相同的函数名,但是参数列表不同的函数。
C++允许派生类中覆盖基类中同名函数的虚函数,只要它们的参数和返回类型完全相同。
这是因为虚函数的重载不会产生歧义,且派生类中的函数会覆盖基类中的函数。
4.虚函数表:每个包含虚函数的类都有一个虚函数表。
虚函数表是存储指向虚函数地址的指针数组,它存储在类的只读数据段中。
每个虚函数在虚函数表中占据一段连续的内存空间。
类的对象在内存中分配时,会包含指向其虚函数表的指针。
这个指针被称为虚函数表指针(vptr)。
5.虚函数的性能影响:虚函数的使用需要额外的内存和时间开销。
每个具有虚函数的类都需要一个虚函数表,这会增加每个对象的内存需求。
此外,动态函数调用需要更长的时间,因为调用的函数地址需要从虚函数表中查找。
因此,如果代码对性能有严格的要求,则应尽可能避免使用虚函数。
总结:在C++中,虚函数是一种非常强大的语言特性。
它使得多态性成为可能,能够将多个派生类对象视为共同的基类对象处理。
虚函数的覆盖和重载让我们可以在派生类中自由地定义函数,而虚函数表负责管理派生类对象的动态调用。
然而,虚函数的性能开销可能会成为程序的瓶颈,所以需要仔细地使用。
C++虚函数及虚函数表解析虚函数的定义: 虚函数必须是类的⾮静态成员函数(且⾮构造函数),其访问权限是public(可以定义为private or proteceted,但是对于多态来说,没有意义。
),在基类的类定义中定义虚函数的⼀般形式: virtual 函数返回值类型虚函数名(形参表) { 函数体 } 虚函数的作⽤是实现动态联编,也就是在程序的运⾏阶段动态地选择合适的成员函数,在定义了虚函数后, 可以在基类的派⽣类中对虚函数重新定义(形式也是:virtual 函数返回值类型虚函数名(形参表){ 函数体 }),在派⽣类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。
以实现统⼀的接⼝,不同定义过程。
如果在派⽣类中没有对虚函数重新定义,则它继承其基类的虚函数。
当程序发现虚函数名前的关键字virtual后,会⾃动将其作为动态联编处理,即在程序运⾏时动态地选择合适的成员函数。
实现动态联编需要三个条件: 1、必须把需要动态联编的⾏为定义为类的公共属性的虚函数。
2、类之间存在⼦类型关系,⼀般表现为⼀个类从另⼀个类公有派⽣⽽来。
3、必须先使⽤基类指针指向⼦类型的对象,然后直接或者间接使⽤基类指针调⽤虚函数。
定义虚函数的限制: (1)⾮类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。
实际上,优秀的程序员常常把基类的析构函数定义为虚函数。
因为,将基类的析构函数定义为虚函数后,当利⽤delete删除⼀个指向派⽣类定义的对象指针时,系统会调⽤相应的类的析构函数。
⽽不将析构函数定义为虚函数时,只调⽤基类的析构函数。
(2)只需要在声明函数的类体中使⽤关键字“virtual”将函数声明为虚函数,⽽定义函数时不需要使⽤关键字“virtual”。
(3)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、参数类型都相同的⾮虚函数。
C++中虚函数的作⽤和虚函数的⼯作原理1 C++中虚函数的作⽤和多态虚函数:实现类的多态性关键字:虚函数;虚函数的作⽤;多态性;多态公有继承;动态联编C++中的虚函数的作⽤主要是实现了多态的机制。
基类定义虚函数,⼦类可以重写该函数;在派⽣类中对基类定义的虚函数进⾏重写时,需要在派⽣类中声明该⽅法为虚⽅法。
当⼦类重新定义了⽗类的虚函数后,当⽗类的指针指向⼦类对象的地址时,[即B b; A a = &b;] ⽗类指针根据赋给它的不同⼦类指针,动态的调⽤⼦类的该函数,⽽不是⽗类的函数(如果不使⽤virtual⽅法,请看后⾯★*),且这样的函数调⽤发⽣在运⾏阶段,⽽不是发⽣在编译阶段,称为动态联编。
⽽函数的重载可以认为是多态,只不过是静态的。
注意,⾮虚函数静态联编,效率要⽐虚函数⾼,但是不具备动态联编能⼒。
★如果使⽤了virtual关键字,程序将根据引⽤或指针指向的对象类型来选择⽅法,否则使⽤引⽤类型或指针类型来选择⽅法。
下⾯的例⼦解释动态联编性:class A{private:int i;public:A();A(int num) :i(num) {};virtual void fun1();virtual void fun2();};class B : public A{private:int j;public:B(int num) :j(num){};virtual void fun2();// 重写了基类的⽅法};// 为⽅便解释思想,省略很多代码A a(1);B b(2);A *a1_ptr = &a;A *a2_ptr = &b;// 当派⽣类“重写”了基类的虚⽅法,调⽤该⽅法时// 程序根据指针或引⽤指向的 “对象的类型”来选择使⽤哪个⽅法a1_ptr->fun2();// call A::fun2();a2_ptr->fun2();// call B::fun1();// 否则// 程序根据“指针或引⽤的类型”来选择使⽤哪个⽅法a1_ptr->fun1();// call A::fun1();a2_ptr->fun1();// call A::fun1();2. 虚函数的底层实现机制实现原理:虚函数表+虚表指针关键字:虚函数底层实现机制;虚函数表;虚表指针编译器处理虚函数的⽅法是:为每个类对象添加⼀个隐藏成员,隐藏成员中保存了⼀个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使⽤⼀个虚函数表,每个类对象⽤⼀个虚表指针。
C++中虚函数的作用(多态的原理)2009-04-17 22:02■ 如果你期望衍生类别重新定义一个成员函数,那么你应该在基础类别中把此函数设为virtual。
■ 以单一指令唤起不同函数,这种性质称为Polymorphism,意思是"the ability toassume many forms",也就是多态。
■ 虚拟函数是C++ 语言的Polymorphism 性质以及动态绑定的关键。
■ 既然抽象类别中的虚拟函数不打算被调用,我们就不应该定义它,应该把它设为纯虚拟函数(在函数声明之后加上"=0" 即可)。
■ 我们可以说,拥有纯虚拟函数者为抽象类别(abstract Class),以别于所谓的具象类别(concrete class)。
■ 抽象类别不能产生出对象实体,但是我们可以拥有指向抽象类别之指针,以便于操作抽象类别的各个衍生类别。
■ 虚拟函数衍生下去仍为虚拟函数,而且可以省略virtual 关键词。
虚函数联系到多态,多态联系到继承.所以本文中都是在继承层次上做文章.没了继承,什么都没得谈.下面是对C++的虚函数这玩意儿的理解.一.什么是虚函数(如果不知道虚函数为何物,但有急切的想知道,那你就应该从这里开始)简单地说,那些被virtual关键字修饰的成员函数,就是虚函数.虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异而采用不同的策略.下面来看一段简单的代码: class A{public:void print(){ cout<<"This is A"<<endl;}};class B: public A{public:void print(){ cout<<"This is B"<<endl;}};int main() //为了在以后便于区分,我这段main()代码叫做main1{A a;B b;a.print();b.print();}通过class A和class B的print()这个接口,可以看出这两个class 因个体的差异而采用了不同的策略,输出的结果也是我们预料中的,分别是This is A和This is B.但这是否真正做到了多态性呢? No,多态还有个关键之处就是一切用指向基类的指针或引用来操作对象.那现在就把main()处的代码改一改.int main() //main2{A a;B b;A* p1=&a;A* p2=&b;p1->print();p2->print();}运行一下看看结果,哟呵,蓦然回首,结果却是两个This is A.问题来了,p2明明指向的是class B的对象但却是调用的class A的print()函数,这不是我们所期望的结果,那么解决这个问题就需要用到虚函数.class A{public:virtual void print(){ cout<<"This is A"<<endl;} //现在成了虚函数了};class B: public A{public:void print(){ cout<<"This is B"<<endl;} //这里需要在前面加上关键字virtual吗?};毫无疑问,class A的成员函数print()已经成了虚函数,那么class B 的print()成了虚函数了吗?回答是Yes,我们只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数.所以,class B的print()也成了虚函数.那么对于在派生类的相应函数前是否需要用virtual关键字修饰,那就是你自己的问题了.现在重新运行main2的代码,这样输出的结果就是This is A和This is B了.现在来消化一下,我作个简单的总结,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数.二.虚函数是如何做到的(如果你没有看过《Inside The C++ Object Model》这本书,但又急切想知道,那你就应该从这里开始.)虚函数是如何做到因对象的不同而调用其相应的函数的呢?现在我们就来剖析虚函数.我们先定义两个类:class A{ //虚函数示例代码public:virtual void fun(){cout<<1<<endl;}virtual void fun2(){cout<<2<<endl;}};class B: public A{public:void fun(){cout<<3<<endl;}void fun2(){cout<<4<<endl;}};由于这两个类中有虚函数存在,所以编译器就会为他们两个分别插入一段你不知道的数据,并为他们分别创建一个表.那段数据叫做vptr指针,指向那个表.那个表叫做vtbl,每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,我们可以把vtbl形象地看成一个数组,这个数组的每个元素存放的就是虚函数的地址,请看图通过上图,可以看到这两个vtbl分别为class A和class B服务.现在有了这个模型之后,我们来分析下面的代码:A *p=new A;p->fun();毫无疑问,调用了A::fun(),但是A::fun()是如何被调用的呢?它像普通函数那样直接跳转到函数的代码处吗? No,其实是这样的,首先是取出vptr的值,这个值就是vtbl的地址,再根据这个值来到vtbl这里,由于调用的函数A::fun()是第一个虚函数,所以取出vtbl第一个slot 里的值,这个值就是A::fun()的地址了,最后调用这个函数.现在我们可以看出来了,只要vptr不同,指向的vtbl就不同,而不同的vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务.而对于class A和class B来说,他们的vptr指针存放在何处呢?其实这个指针就放在他们各自的实例对象里.由于class A和class B都没有数据成员,所以他们的实例对象里就只有一个vptr指针.通过上面的分析,现在我们来实作一段代码,来描述这个带有虚函数的类的简单模型.#include<iostream>using namespace std;//将上面"虚函数示例代码"添加在这里int main(){void (*fun)(A*) ;A *p=newB ;long lVptrAddr ;memcpy(&lVptrAddr, p, 4) ;memcpy(&fun, reinterpret_cast<long*>(lVptrAddr), 4) ;fun(p) ;delete p ;system("pause");}用VC或Dev-C++编译运行一下,看看结果是不是输出3,如果不是,那么太阳明天肯定是从西边出来.现在一步一步开始分析: void (*fun)(A*);这段定义了一个函数指针名字叫做fun,而且有一个A*类型的参数,这个函数指针待会儿用来保存从vtbl里取出的函数地址;A* p=new B;这个我不太了解,算了,不解释这个了;long lVptrAddr;这个long类型的变量待会儿用来保存vptr的值;memcpy(&lVptrAddr,p,4);前面说了,他们的实例对象里只有vptr 指针,所以我们就放心大胆地把p所指的4bytes内存里的东西复制到lVptrAddr中,所以复制出来的4bytes内容就是vptr的值,即vtbl的地址;现在有了vtbl的地址了,那么我们现在就取出vtbl第一个slot里的内容;memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4);取出vtbl 第一个slot里的内容,并存放在函数指针fun里.需要注意的是lVptrAddr里面是vtbl的地址,但lVptrAddr不是指针,所以我们要把它先转变成指针类型;fun(p);这里就调用了刚才取出的函数地址里的函数,也就是调用了B::fun()这个函数,也许你发现了为什么会有参数p,其实类成员函数调用时,会有个this指针,这个p就是那个this指针,只是在一般的调用中编译器自动帮你处理了而已,而在这里则需要自己处理;delete p;和system("pause");这个我不太了解,算了,不解释这个了.如果调用B::fun2()怎么办?那就取出vtbl的第二个slot里的值就行了;memcpy(&fun,reinterpret_cast<long*>(lVptrAddr+4),4);为什么是加4呢?因为一个指针的长度是4bytes,所以加4,或者memcpy(&fun,reinterpret_cast<long*>(lVptrAddr)+1,4);这更符合数组的用法,因为lVptrAddr被转成了long*型别,所以+1就是往后移sizeof(long)的长度.三.以一段代码开始#include<iostream>using namespace std;class A{ //虚函数示例代码2public:virtual void fun(){ cout<<"A::fun"<<endl;}virtual void fun2(){cout<<"A::fun2"<<endl;} };class B: public A{public:void fun(){ cout<<"B::fun"<<endl;}void fun2(){ cout<<"B::fun2"<<endl;}}; //end虚函数示例代码2int main(){void (A::*fun)(); //定义一个函数指针A *p=new B;fun=&A::fun;(p->*fun)();fun = &A::fun2;(p->*fun)();delete p;system("pause");}你能估算出输出结果吗?如果你估算出的结果是A::fun和A::fun2,呵呵,恭喜恭喜,你中圈套了.其实真正的结果是B::fun和B::fun2,如果你想不通就接着往下看.给个提示,&A::fun和&A::fun2是真正获得了虚函数的地址吗?首先我们回到第二部分,通过段实作代码,得到一个"通用"的获得虚函数地址的方法.#include<iostream>using namespace std;//将上面"虚函数示例代码2"添加在这里void CallVirtualFun(void* pThis,int index=0){void (*funptr)(void*);long lVptrAddr;memcpy(&lVptrAddr,pThis,4);memcpy(&funptr,reinterpret_cast<long*>(lVptrAddr)+index,4);funptr(pThis); //调用}int main(){A* p=new B;CallVirtualFun(p); //调用虚函数p->fun()CallVirtualFun(p,1);//调用虚函数p->fun2()system("pause");}现在我们拥有一个"通用"的CallVirtualFun方法.这个通用方法和第三部分开始处的代码有何联系呢?联系很大.由于A::fun()和A::fun2()是虚函数,所以&A::fun和&A::fun2获得的不是函数的地址,而是一段间接获得虚函数地址的一段代码的地址,我们形象地把这段代码看作那段CallVirtualFun.编译器在编译时,会提供类似于CallVirtualFun这样的代码,当你调用虚函数时,其实就是先调用的那段类似CallVirtualFun的代码,通过这段代码,获得虚函数地址后,最后调用虚函数,这样就真正保证了多态性.同时大家都说虚函数的效率低,其原因就是,在调用虚函数之前,还调用了获得虚函数地址的代码.最后的说明:本文的代码可以用VC6和Dev-C++4.9.8.0通过编译,且运行无问题.其他的编译器小弟不敢保证.其中,里面的类比方法只能看成模型,因为不同的编译器的低层实现是不同的.例如this指针,Dev-C++的gcc就是通过压栈,当作参数传递,而VC的编译器则通过取出地址保存在ecx中.所以这些类比方法不能当作具体实现。
C++虚函数学习笔记(仅供参考)1、虚函数的作用:允许在派生类中重新定义与基类同名的函数,并且可以通过基类的指针或引用来访问基类和派生类中的同名函数。
2、虚函数的工作原理编译器处理虚函数的方法:对每个对象添加一个隐藏成员。
隐藏成员中保存了一个指向函数地址数组的指针,这种数组称为虚函数表(vtbl)。
虚函数表中存储了为类对象进行声明的虚函数的地址。
例如:基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。
派生类对象将包含一个指向独立地址表的指针如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果没有重新定义虚函数该vtbl将保存函数原始版本的地址,如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中。
注:无论类中包含了多少虚函数,都只需在对象中添加一个地址成员,只是表的大小不同。
C++规定:当一个成员函数声明为基类后,其派生类中的同名函数也自动成为虚函数。
虚函数使用的注意事项:一、只能用virtual声明类的成员函数,使它成为虚函数,而不能将类外的普通函数声明为虚函数,因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。
显然,它只能用于类的继承层次结构中。
二、一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同参数的(包括个数和类型)和函数返回值类型的同名函数。
静态关联与动态关联:关联:确定调用具体对象的过程称为关联。
静态关联:函数重载和通过对象名调用的虚函数,在编译时可确定其调用的虚函数属于哪一个类,其过程称为静态关联;动态关联:在运行阶段将类和对象绑定在一起的过程称为动态关联;如:在虚函数中的pt->display(),无法确定确定应调用哪一类的虚函数,在这样的情况下,编译系统把它放到运行阶段处理,在运行阶段确定其关联关系。
在运行阶段,基类指针变量先指向某一个类的对象,然后通过此指针变量调用该对象中的函数,此时调用哪一个对象的函数就确定了。
c++虚函数作用及底层原理
c++虚函数作用及底层原理
什么是虚函数?
虚函数是指函数表现出一种多态特性的函数,也就是说在程序中可以根据所操作对象的不同而调用不同的函数。
在函数实现方面,他们就是有一个虚表(Vtable)指向的函数的指针,并且只有用virtual 关键字来声明的函数才会存储在Vtable 中。
虚函数的主要作用:
1、实现多态:当定义的类含有需要多态的函数,那么就要在定义的函数前面加上virtual 关键字,使函数具有虚函数的能力,这样可以实现多态性。
2、实现动态绑定:使用虚函数,可以实现动态绑定,也就是说,当程序在运行时,可以根据所操作对象的不同而调用不同的函数。
虚函数的底层原理:
在c++中的实现原理是采用一个指针数组,也称为虚表(Vtable)实现的。
虚表是一个数组,里面存放的是函数的地址。
当调用虚函数时,程序会首先检查对象是否有虚表指针,如果有系统会拿到虚表指针,然后根据指针所指的虚表的索引号去虚表中找到实际的函数再去调用。
因此,虚函数的实现原理需要以下几步:
1t根据继承关系初始化虚表:程序会在构造函数中初始化虚
表,虚表中存放的是所有虚函数的地址;
2t程序在运行过程中,会检查对象是否有虚表指针;
3t根据指针所指的虚表的索引号去虚表中找到实际的函数再去调用;
4t程序运行结束,会销毁所有的对象。
虚函数:为了重载和多态的需要,在基类中是由定义的,即便定义是空,所以子类中可以重写也可以不写基类中的函数!纯虚函数在基类中是没有定义的,必须在子类中加以实现,很像java中的接口函数!不能直接实例化,需要派生类来实现函数定义纯虚函数及其作用:纯虚函数是指在基类中声明但是没有定义的虚函数:virtual type func(param list) = 0;把虚函数声明为纯虚函数可以强制在派生类中重新定义虚函数,否则编译器会报错。
抽象类及其特征:如果一个类至少有一个纯虚函数,则称为抽象类。
抽象类只能用来作为其他类的基类,不能定义抽象类的对象,因为在抽象类中有一个或者多个函数没有定义。
但是能够使用抽象类来声明指针或者引用。
子类指针不能指向父类对象(即将父类对象赋值给子类指针)父类指针可以指向子类对象(即将子类对象赋值给父类指针)C++虚函数与虚函数表:多态性可分为两类:静态多态和动态多态。
函数重载和运算符重载实现的多态属于静态多态,动态多态性是通过虚函数实现的。
每个含有虚函数的类有一张虚函数表(vtbl),表中每一项是一个虚函数的地址,也就是说,虚函数表的每一项是一个虚函数的指针。
没有虚函数的C++类,是不会有虚函数表的。
两张图:一般继承(无虚函数覆盖)下面,再让我们来看看继承时的虚函数表是什么样的。
假设有如下所示的一个继承关系:请注意,在这个继承关系中,子类没有重载任何父类的函数。
那么,在派生类的实例中,其虚函数表如下所示:对于实例:Derive d; 的虚函数表如下:我们可以看到下面几点:1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证。
一般继承(有虚函数覆盖)覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。
下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
程序设计二(面向对象)_实训13_虚函数虚函数是面向对象程序设计中的一个重要概念,它在继承和多态的实现中起着关键的作用。
本文将介绍虚函数的概念、作用、使用方法以及一些注意事项。
一、什么是虚函数在面向对象的程序设计中,虚函数是一种特殊的成员函数,它可以在基类中声明为虚函数,并在派生类中进行重写。
通过使用虚函数,我们可以实现多态性,即同一个函数在不同的对象上表现出不同的行为。
二、虚函数的作用虚函数的主要作用是实现动态绑定。
在程序运行时,根据对象的实际类型来确定调用哪个函数。
这使得我们可以通过基类指针或引用来调用派生类的函数,实现了对派生类对象的统一操作。
三、虚函数的使用方法1. 在基类中声明虚函数:在基类中将需要在派生类中重写的函数声明为虚函数。
例如,我们有一个基类Animal,其中有一个虚函数makeSound(),表示动物发出声音。
2. 在派生类中重写虚函数:派生类继承基类后,可以重写基类中的虚函数,并根据实际需求修改函数的实现。
例如,我们有一个派生类Dog,它重写了基类中的makeSound()函数,表示狗的叫声。
3. 使用基类指针或引用调用虚函数:在程序中,我们可以使用基类的指针或引用来调用派生类中重写的虚函数,实现对派生类对象的统一操作。
例如,我们可以使用Animal*指针指向Dog对象,并通过该指针调用makeSound()函数,即使实际对象是Dog类型,也能正确调用Dog类中的makeSound()函数。
四、虚函数的注意事项1. 虚函数必须是非静态成员函数。
虚函数与对象的状态有关,因此必须是非静态成员函数。
2. 虚函数可以被派生类重写,但不能被派生类隐藏。
派生类重写虚函数时,函数的签名必须与基类中的虚函数相同,即函数名、参数列表和返回类型都要相同。
否则,派生类中的函数将隐藏基类中的虚函数,而不是重写。
3. 构造函数和析构函数不能是虚函数。
构造函数和析构函数在对象的创建和销毁过程中起着重要作用,因此不能声明为虚函数。
我们知道,在同一类中是不能定义两个名字相同、参数个数和类型都相同的函数的,否则就是“重复定义”。
但是在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。
例如在例12.1程序中,在Circle类中定义了area函数,在Circle类的派生类Cylinder中也定义了一个area函数。
这两个函数不仅名字相同,而且参数个数相同(均为0),但功能不同,函数体是不同的。
前者的作用是求圆面积,后者的作用是求圆柱体的表面积。
这是合法的,因为它们不在同一个类中。
编译系统按照同名覆盖的原则决定调用的对象。
在例12.1程序中用cy1.area( ) 调用的是派生类Cylinder中的成员函数area。
如果想调用cy1 中的直接基类Circle的area函数,应当表示为cy1.Circle::area()。
用这种方法来区分两个同名的函数。
但是这样做很不方便。
人们提出这样的设想,能否用同一个调用形式,既能调用派生类又能调用基类的同名函数。
在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。
例如,用同一个语句“pt->display( );”可以调用不同派生层次中的display函数,只需在调用前给指针变量pt 赋以不同的值(使之指向不同的类对象)即可。
打个比方,你要去某一地方办事,如果乘坐公交车,必须事先确定目的地,然后乘坐能够到达目的地的公交车线路。
如果改为乘出租车,就简单多了,不必查行车路线,因为出租车什么地方都能去,只要在上车后临时告诉司机要到哪里即可。
如果想访问多个目的地,只要在到达一个目的地后再告诉司机下一个目的地即可,显然,“打的”要比乘公交车方便。
无论到什么地方去都可以乘同—辆出租车。
这就是通过同一种形式能达到不同目的的例子。
C++中的虚函数就是用来解决这个问题的。
虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
请分析例12.2。
这个例子开始时没有使用虚函数,然后再讨论使用虚函数的情况。
[例12.2] 基类与派生类中有同名函数。
在下面的程序中Student是基类,Graduate是派生类,它们都有display这个同名的函数。
#include <iostream>#include <string>using namespace std;//声明基类Studentclass Student{public:Student(int, string,float); //声明构造函数void display( );//声明输出函数protected: //受保护成员,派生类可以访问int num;string name;float score;};//Student类成员函数的实现Student::Student(int n, string nam,float s)//定义构造函数{num=n;name=nam;score=s;}void Student::display( )//定义输出函数{cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\n\n"; }//声明公用派生类Graduateclass Graduate:public Student{public:Graduate(int, string, float, float);//声明构造函数void display( );//声明输出函数private:float pay;};// Graduate类成员函数的实现void Graduate::display( )//定义输出函数{cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\npay="<<pay<<endl; }Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){} //主函数int main(){Student stud1(1001,"Li",87.5);//定义Student类对象stud1Graduate grad1(2001,"Wang",98.5,563.5);//定义Graduate类对象grad1Student *pt=&stud1;//定义指向基类对象的指针变量ptpt->display( );pt=&grad1;pt->display( );return 0;}运行结果如下:num:1001(stud1的数据)name:Liscore:87.5num:2001 (grad1中基类部分的数据)name:wangscore:98.5假如想输出grad1的全部数据成员,当然也可以采用这样的方法:通过对象名调用display函数,如grad1.display(),或者定义一个指向Graduate类对象的指针变量ptr,然后使ptr指向gradl,再用ptr->display()调用。
这当然是可以的,但是如果该基类有多个派生类,每个派生类又产生新的派生类,形成了同一基类的类族。
每个派生类都有同名函数display,在程序中要调用同一类族中不同类的同名函数,就要定义多个指向各派生类的指针变量。
这两种办法都不方便,它要求在调用不同派生类的同名函数时采用不同的调用方式,正如同前面所说的那样,到不同的目的地要乘坐指定的不同的公交车,一一对应,不能搞错。
如果能够用同一种方式去调用同一类族中不同类的所有的同名函数,那就好了。
用虚函数就能顺利地解决这个问题。
下面对程序作一点修改,在Student类中声明display函数时,在最左面加一个关键字virtual,即virtual void display( );这样就把Student类的display函数声明为虚函数。
程序其他部分都不改动。
再编译和运行程序,请注意分析运行结果:num:1001(stud1的数据)name:Liscore:87.5num:2001 (grad1中基类部分的数据)name:wangscore:98.5pay=1200 (这一项以前是没有的)看!这就是虚函数的奇妙作用。
现在用同一个指针变量(指向基类对象的指针变量),不但输出了学生stud1的全部数据,而且还输出了研究生grad1的全部数据,说明已调用了grad1的display函数。
用同一种调用形式“pt->display()”,而且pt是同一个基类指针,可以调用同一类族中不同类的虚函数。
这就是多态性,对同一消息,不同对象有不同的响应方式。
说明:本来基类指针是用来指向基类对象的,如果用它指向派生类对象,则进行指针类型转换,将派生类对象的指针先转换为基类的指针,所以基类指针指向的是派生类对象中的基类部分。
在程序修改前,是无法通过基类指针去调用派生类对象中的成员函数的。
虚函数突破了这一限制,在派生类的基类部分中,派生类的虚函数取代了基类原来的虚函数,因此在使基类指针指向派生类对象后,调用虚函数时就调用了派生类的虚函数。
要注意的是,只有用virtual声明了虚函数后才具有以上作用。
如果不声明为虚函数,企图通过基类指针调用派生类的非虚函数是不行的。
虚函数的以上功能是很有实用意义的。
在面向对象的程序设计中,经常会用到类的继承,目的是保留基类的特性,以减少新类开发的时间。
但是,从基类继承来的某些成员函数不完全适应派生类的需要,例如在例12.2中,基类的display函数只输出基类的数据,而派生类的display函数需要输出派生类的数据。
过去我们曾经使派生类的输出函数与基类的输出函数不同名(如display和display1),但如果派生的层次多,就要起许多不同的函数名,很不方便。
如果采用同名函数,又会发生同名覆盖。
利用虚函数就很好地解决了这个问题。
可以看到:当把基类的某个成员函数声明为虚函数后,允许在其派生类中对该函数重新定义,赋予它新的功能,并且可以通过指向基类的指针指向同一类族中不同类的对象,从而调用其中的同名函数。
由虚函数实现的动态多态性就是:同一类族中不同类的对象,对同一函数调用作出不同的响应。
虚函数的使用方法是:.在基类用virtual声明成员函数为虚函数。
这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。
在类外定义虚函数时,不必再加virtual。
.在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。
C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。
因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。
如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。
.定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
.通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。
通过虚函数与指向基类对象的指针变量的配合使用,就能方便地调用同一类族中不同类的同名函数,只要先用基类指针指向即可。
如果指针不断地指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数。
这就如同前面说的,不断地告诉出租车司机要去的目的地,然后司机把你送到你要去的地方。
需要说明;有时在基类中定义的非虚函数会在派生类中被重新定义(如例12.1中的area函数),如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。
以前介绍的函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题,前者是横向重载,后者可以理解为纵向重载。
但与重载不同的是:同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)。