有一定面向对象知识的朋友对继承与多态一定很熟悉,C++想实现继承的话就要使用虚函数,那么什么是虚函数,其原理是什么,下面尽量给大家分析一下C++中其运行机制:
首先,基础,什么是虚函数,什么是多态?
答:被virtual关键字修饰的成员函数,就是虚函数。虚函数用来实现多态性(Polymorphism),将接口与实现进行分离;简单来说就是实现共同的方法,但因个体差异而采用不同的策略。
举例来说,我有一个动物的基类,这个类里面包含“跑”(virtual void run())的一个成员方法,这个基类有两个派生类——猫和袋鼠,我想让猫和袋鼠都可以使用跑的方法,但是很明显他们两个跑的方式是不一样的。这样,由于个体的不同而实现同一方法的不同效果就是多态。
情况1:
class A{ public: void Show(){ cout<<”I am A”<<endl;} }; class B:public A{ public: void Show (){ cout<<”I am B”<<endl;} }; int main(){ A a; B b; a. Show (); b. Show (); }
这里为了演示方便就声明了A,B两个类,B继承了A。
这样打印出来的语句肯定是,i am A,I am B。不过这并不是多态,B在声明Show()方法的时候完全是自己的
Show(),他们调用的完全两个对象的方法,所以并不是多态(其实这是一种隐藏)。如何才是多态?一切用指向基类的指针或引用来操作对象是多态的基本特征,也就是说得有指针而且还是指向基类的指针。
那改一下吧~
情况2:
int main(){ A a; B b; A *p1=&a; A *p2=&b; p1-> Show (); p2-> Show (); }
那现在结果呢?I am A,I am A。还是不对,虽然p2已经指向b,但是还是调用的A的Show()。
所以,我们不妨按照虚函数的定义,试一下,把Show前面加一个virtual。
情况3:
class A{ public: virtual void Show(){ cout<<”I am A”<<endl;} }; class B:public A{ public: virtual void Show (){ cout<<”I am B”<<endl;} };
现在重新运行main的代码,这样输出的结果就是I am A,I am B了。
简单总结,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。
必须是基类指针指向派生类对象,派生类指针不能指基类。所以派生类只能操作父类有的属性及函数。对于那些父类没有的属性,必须将父类指针强制转化为子类指针后才可使用。
那么为什么会根据不同的类对象,调用不同版本的函数呢?
—————————————————————————————类的指针与对象分析———————————————————————————————————
写到这里,我想先放一放,非常建议大家和我一起研究一下类的指针和类的对象,这对之后的理解有很大的帮助。
A a; //类的对象
A *p1=&a;或者A*p1=newA(); //类的指针
A*p1=NULL; //类的指针,是可以先行定义的
B b; //类的对象
(这里要说明一下,用new创建的指针(必须赋予指针)需要结束之后调用delete,才能执行析构函数。而B b不需要手动释放,可以自动调用构造函数与析构函数)
类的指针(用 ->操作符):他是一个内存地址值,他指向内存中存放的类对象(包括一些成员变量所赋的值),如果用new声明的话那么他使用的是内存堆,是个永久变量,除非你释放它(否则也是在栈中). 并且没有调用构造函数。
类的对象(用 . 操作符):他是利用类的构造函数在内存中分配一块内存(包括一些成员变量所赋的值,在运行时就分配了对应大小的内存),成员函数的地址是全局已知的,所以其内存无需保存在对象实例中(关于类占用内存的大小,请参考另一篇博客XXXX)。类的对象使用的是内存栈,是个局部的临时变量。
理解: 当类是有虚函数的基类,假如Show是它的一个虚函数,则调用Show时:
类的对象:调用的是它自己的Show;
类的指针:调用的是分配给它空间时那种类的Show(父类的指针可以指向子类的对象);
(我们使用基类的引用或指针调用函数的时候(p2-> Show ();)并不清楚该函数真正调用的对象是什么类型,如果是虚函数的话,就只能在运行时才决定(如果不是虚函数,我们认为编译时就可以定下来了)~不过这里大家肯定还会有疑问?既然它调用的是指针所指向的对象,那不加virtual的函数为什么就没有效果呢?继续往下看)
————————————————————————————————————————————————————————————————————————
好了,下面我们继续学习虚函数,对上面的例子再进行分析!
在上面的情况2下(没有虚函数的情况),我们给A类型指针传递了不同类的地址,按常理来说我们希望这个指针能够区分是基类还是派生类,然而结果却是都当做派生类来处理。所以我们自然想改变这种情况,虚函数也就诞生了~并且实现了多态的效果
虚函数这样的特点到底如何实现?答案就是虚函数表! Virtual Table简称为V-table(虚表)
在虚函数存在的类中,编译器就会为他们创建一个vptr指针,这个指针指向的就是虚表(V-Table)。
这里我们着重看一下这张虚函数表。在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
下面举代码例子:
class A{ public: virtual void show(){ cout<<”Show A”<<endl;} virtual void print(){cout<<”print A”<<endl;} }; typedef void (*Fun)(void); //指向返回void类型并且无参数的函数的指针,类比typedef void (*) (void) Fun; A a; Fun pFun = NULL; cout << "虚函数表地址:" << (int*)(&a) << endl; //取a的地址之后强转成int*,这样获取的就是虚函数表的地址(其实就是vptr) cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&a) << endl; //对vptr解引用(等价于*vptr),强转成int*就是第一个虚函数的地址 // Invoke the first virtual function pFun = (Fun)*((int*)*(int*)(&a)); pFun();
结果打印:
虚函数表地址:0012FED4
虚函数表 — 第一个函数地址:0044F148
A::Show
下面我们再声明两个类:
class B:public A{ public: virtual void show(){ cout<<”Show B”<<endl;} virtual void printB(){cout<<”print B”<<endl;} //注意这里没有覆盖A的方法 }; class C: public A { public: virtual void showC(){ cout<<”Show C”<<endl;} virtual printC(){cout<<”print C”<<endl;} };
这里C没有覆盖A的方法,而B覆盖了A的方法
我们看一下不同情况下的虚函数的表是什么样的
对于无覆盖的虚函数
A a;
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
对于有覆盖的虚函数表 ( 没错,其实这就是我们常说的覆盖,而博客一开始的情况一就是隐藏的例子 )
我们可以看到下面几点:
1)覆盖的show()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数位置不变。
这样,我们就可以看到对于下面这样的程序,
A *a = new B(); a->show();由 a 所指的内存中的虚函数表的 show() 的位置已经被 B:: show () 函数地址所取代,于是在实际调用发生时,是 B:: show () 被调用了。多态就这样实现了 ~
这里附一张多继承的效果图,B继承与A1,A2,A3、
补充:
是基类虚表中有的,在派生类中都有,并根据派生类自己的虚函数进行了扩展
虽然上面例子的a可以访问show(),但是却不能访问printB,即使a虚表里面拥有(父类不可以访问子类自己的虚函数)
如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,我们还是有办法访问到。
最后再说明几点:
1. 静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。
2. 内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义,但是在编译的时候系统仍然将它看做是非内联的。
3. 构造函数不能是虚函数,因为构造的时候,对象还是一片未定型的空间,只有构造完成后,对象才是具体类的实例。
4. 析构函数可以是虚函数,而且通常声名为虚函数,继承关系下,派生类的实例中会为基类的成员变量申请相应的内存。析构的时候,我们需要析构基类以及派生类的所有占用空间,不用虚函数的话,就会只调用基类的析构函数
说了这么多,应该对虚函数与多态有一个全新的了解了吧~
参考了 wswifth 的博客(感谢!)