虚函数
来源:互联网 发布:windows安装snmp 编辑:程序博客网 时间:2024/06/05 02:46
目录
- 定义
- 作用
- 示例
- 虚函数的实例
- 条件
- 其他信息
- c++的虚函数
- 一, 什么是虚函数
- 二, 虚函数的实现
- 三, 代码示例
- CallVirtualFun方法
- 最后的说明
- 展开
编辑本段定义
定义:在某基类中声明为 virtual 并在一个或多个派生类中被重新定 义的成员函数[1] 语法:virtual 函数返回类型 函数名(参数表) { 函数体 } 用途:实现多态性,通过指向派生类的基类指针,访问派生类中同名覆盖成员函数 虚函数必须是基类的非静态成员函数,其访问权限可以是protected或public,在基类的类定义中定义虚函数的一般形式: class 基类名{ ....... virtual 返回值类型 将要在派生类中重载的函数名(参数列表); };编辑本段作用
虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。 当程序发现虚函数名前的关键字virtual后,会自动将其作为动态联编处理,即在程序运行时动态地选择合适的成员函数。虚函数是C++多态的一种表现。 例如:子类继承了父类的一个函数(方法),而我们把父类的指针指向子类,则必须把父类的该函数(方法)设为virtual(虚函数)。 ([2010.10.28] 注:下行语义容易使人产生理解上的偏差,实际效果应为: 如存在:Base -> Derive1 -> Derive2 及它们所拥有的虚函数func() 则在访问派生类Derive1的实例时,使用其基类Base及本身类型Derive1,或被静态转换的后续派生类Derive2的指针或引用,均可访问到Derive1所实现的func()。) 动态联编规定,只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式: 1、指向基类的指针变量名->虚函数名(实参表) 2、基类对象的引用名. 虚函数名(实参表) 使用虚函数,我们可以灵活的进行动态绑定,当然是以一定的开销为代价。如果父类的函数(方法)根本没有必要或者无法实现,完全要依赖子类去实现的话,可以把此函数(方法)设为virtual 函数名=0 我们把这样的函数(方法)称为纯虚函数。 如果一个类包含了纯虚函数,称此类为抽象类。编辑本段示例
虚函数的实例
#include<iostream> using namespace std; class Cshape { public: void SetColor(int color) { m_nColor=color;} virtualvoid Display( void) { cout<<"Cshape"<<endl; } private: int m_nColor; }; class Crectangle: public Cshape { public: virtualvoid Display( void) { cout<<"Crectangle"<<endl; } }; class Ctriangle: public Cshape { virtual void Display( void) { cout<<"Ctriangle"<<endl; } }; class Cellipse :public Cshape { public: virtualvoid Display(void) { cout<<"Cellipse"<<endl;} }; voidmain() { Cshape obShape; Cellipse obEllipse; Ctriangle obTriangle; Crectangle obRectangle; Cshape * pShape[4]= { &obShape,&obEllipse,&obTriangle,& obRectangle }; for( int I= 0; I< 4; I++) pShape[I]->Display( ); } 本程序运行结果: Cshape Cellipse Ctriangle Crectangle条件
所以,从以上程序分析,实现动态联编需要三个条件: 1、 必须把动态联编的行为定义为类的虚函数。 2、 类之间存在子类型关系,一般表现为一个类从另一个类公有派生而来。 3、 必须先使用基类指针指向子类型的对象,然后直接或者间接使用基类指针调用虚函数。编辑本段其他信息
定义虚函数的限制:(1)非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。实际上,优秀的程序员常常把基类的析构函数定义为虚函数。因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。 (2)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。 (3)当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数。 (4)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种同名函数。 虚函数联系到多态,多态联系到继承。所以本文中都是在继承层次上做文章。没了继承,什么都没得谈。编辑本段c++的虚函数
下面是对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形象地看成一个数组,这个数组的每个元素存放的就是虚函数的地址,请看图三, 代码示例
#include<iostream> using namespace std; class A{ //虚函数示例代码2 public: 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//虚函数示例代码2 int 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方法
现在我们拥有一个“通用”的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中。所以这些类比方法不能当作具体实现。- 构造函数 虚函数
- 虚函数 inline函数
- 纯虚函数、虚函数、虚析构函数
- 虚函数,虚析构函数,虚函数表
- 普通函数,虚函数,纯虚函数
- 虚函数,纯虚函数,需析构函数
- 构造函数、析构函数、虚函数
- 构造函数 虚函数 虚析构函数
- 虚函数/构造函数/析构函数
- 构造函数&析构函数&虚函数
- 虚函数
- 虚函数
- 虚函数
- 虚函数
- 虚函数
- 虚函数
- 虚函数
- 虚函数
- 堆和堆栈区别
- Linux查看网络流量
- [转] mysql show processlist命令 详解
- Java程序员从笨鸟到菜鸟之(九十二)深入java虚拟机(一)——java虚拟机底层结构详解
- 删除html
- 虚函数
- 黑马程序员——自定义类模拟实现LineNumberReader类
- Nexus配置实践
- thread.h
- 字符串函数
- 小宝,小宝,我爱你
- Thread.cpp
- 看老外程序员如何向妻子解释OOD (转载)
- 黑马程序员_面向对象的特性