另一篇关于虚函数的

一, 什么是虚函数

简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异而采用不同的策略,虚函数是C++的多态性的主要体现,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。

下面我们从这段代码中来进行分析:

 


  1. #include<iostream>  
  2. using namespace std;  
  3. class A{   
  4. public:  
  5. A();  
  6. void fun1();  
  7. void fun2();  
  8. };  
  9. A::A()  
  10. {  
  11. }  
  12. void A::fun1()  
  13. {  
  14. cout<<"I am in class A fun1"<<endl;  
  15. }  
  16. void A::fun2()  
  17. {  
  18. cout<<"I am in class A fun2"<<endl;  
  19. }  
  20. class B:public A  
  21. {  
  22. public:  
  23. B();  
  24. void fun1(); //默认的从父类继承来,就是虚函数  
  25. void fun2();  
  26. };  
  27. void B::fun1()  
  28. {  
  29. cout<<"I am in class B fun1"<<endl;  
  30. }  
  31. void B::fun2()  
  32. {  
  33. cout<<"I am in class B fun2"<<endl;  
  34. }  
  35. B::B()  
  36. {  
  37. }  
  38. void main()  
  39. {  
  40. A a;  
  41. B b;  
  42. cout<<"a.fun1() : "<<endl;  
  43. a.fun1();  
  44. cout<<"a.fun2() : "<<endl;  
  45. a.fun2();  
  46. cout<<"b.fun1() : "<<endl;  
  47. b.fun1();  
  48. cout<<"b.fun2() : "<<endl;  
  49. b.fun2();  
  50. }  

运行结果很简单

I am in class A fun1

I am in class A fun2

I am in class B fun1

I am in class B fun2

但这是否真正做到了多态性呢?No,多态还有个关键之处就是一切用指向基类的指针或引用来操作对象。那现在就把main()处的代码改一改。

  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. class A{      
  5. public:  
  6.     A();  
  7.          void fun1();  
  8.     void fun2();  
  9. };  
  10. A::A()  
  11. {  
  12. }  
  13. void A::fun1()  
  14. {  
  15.     cout<<"I am in class A fun1"<<endl;  
  16. }  
  17. void A::fun2()  
  18. {  
  19.     cout<<"I am in class A fun2"<<endl;  
  20. }  
  21. class B:public A  
  22. {  
  23. public:  
  24.     B();  
  25.     void fun1();    
  26.     void fun2();  
  27. };  
  28. void B::fun1()  
  29. {  
  30.     cout<<"I am in class B fun1"<<endl;  
  31. }  
  32. void B::fun2()  
  33. {  
  34.     cout<<"I am in class B fun2"<<endl;  
  35. }  
  36. B::B()  
  37. {  
  38. }  
  39. void main()  
  40. {  
  41.     A a;  
  42.     B b;  
  43.         A *ptr;  
  44.          ptr=&a;  
  45.     cout<<"ptr=&a; prt->fun1():        "<<endl;  
  46.     ptr->fun1();  
  47.     cout<<"ptr=&a; prt->fun2():        "<<endl;  
  48.     ptr->fun2();  
  49.     ptr=&b;             
  50.     cout<<"ptr=&b; prt->fun1():        "<<endl;  
  51.     ptr->fun1();  
  52.     cout<<"ptr=&b; prt->fun2():        "<<endl;  
  53.     ptr->fun2();  
  54. }  

这次的运行结果

I am in class A fun1

I am in class A fun2

I am in class A fun1

I am in class A fun2

问题来了,ptr明明指向的B的对象,为什么调用的却是A的函数呢?要解决这个问题,就要用到了虚函数,我们在修改函数

  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. class A{  
  5.       
  6. public:  
  7.     A();  
  8.     virtual void fun1();  
  9.     void fun2();  
  10. };  
  11. A::A()  
  12. {  
  13. }  
  14. void A::fun1()  
  15. {  
  16.     cout<<"I am in class A fun1"<<endl;  
  17. }  
  18. void A::fun2()  
  19. {  
  20.     cout<<"I am in class A fun2"<<endl;  
  21. }  
  22. class B:public A  
  23. {  
  24.       
  25. public:  
  26.     B();  
  27.     virtual void fun1();  //默认的从父类继承来,就是虚函数  
  28.     void fun2();  
  29. };  
  30. void B::fun1()  
  31. {  
  32.     cout<<"I am in class B fun1"<<endl;  
  33. }  
  34. void B::fun2()  
  35. {  
  36.     cout<<"I am in class B fun2"<<endl;  
  37. }  
  38. B::B()  
  39. {  
  40. }  
  41. void main()  
  42. {  
  43.     A a;  
  44.     B b;  
  45.     A *ptr;  
  46.     ptr=&a;  
  47.     cout<<"ptr=&a; prt->fun1():        "<<endl;  
  48.     ptr->fun1();  
  49.     cout<<"ptr=&a; prt->fun2():        "<<endl;  
  50.     ptr->fun2();  
  51.     ptr=&b;              
  52.     cout<<"ptr=&b; prt->fun1():        "<<endl;  
  53.     ptr->fun1();  
  54.     cout<<"ptr=&b; prt->fun2():        "<<endl;  
  55.     ptr->fun2();  

这时候我们发现运行结果变了

I am in class A fun1

I am in class A fun2

I am in class B fun1

I am in class A fun2

因为fun1是虚函数,B类继承A类的fun1默认也是虚函数,简单总结下,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。

fun2不是虚函数,所以调用的仍旧是A类的fun2函数

二, 虚函数是如何做到的

虚函数是如何做到因对象的不同而调用其相应的函数的呢?现在我们就来剖析虚函数

[c-sharp]  view plain copy print ?
  1. class A{   
  2.   public:  
  3.    virtual void fun(){cout<<1<<endl;}     
  4.      virtual void fun2(){cout<<2<<endl;}     
  5. };     
  6. class B:public A{  
  7.    public:  
  8.    void fun(){cout<<3<<endl;}  
  9.    void fun2(){cout<<4<<endl;}     
  10. };   

由于这两个类中有虚函数存在,所以编译器就会为他们两个分别插入一段你不知道的数据,并为他们分别创建一个表。那段数据叫做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指针。通过上面的分析,现在我们来实作一段代码,来描述这个带有虚函数的类的简单模型

  1. #include<iostream>     
  2. using namespace std;     
  3. //将上面“虚函数示例代码”添加在这里     
  4. int main(){  
  5.    void (*fun)(A*);  
  6.    A *p=new B;  
  7.    long lVptrAddr;  
  8.    memcpy(&lVptrAddr,p,4);  
  9.    memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4);      fun(p);  
  10.    delete p;  
  11.    system("pause");     
  12. }   

用VC或Dev-C++编译运行一下,看看结果是不是输出3,void (*fun)(A*); 这段定义了一个函数指针名字叫做fun,而且有一个A*类型的参数,这个函数指针待会儿用来保存从vtbl里取出的函数地址

A* p=new B; new B是向内存(内存分5个区:全局名字空间,自由存储区,寄存器,代码空间,栈)自由存储区申请一个内存单元的地址然后隐式地保存在一个指针中.然后把这个地址附值给A类型的指针P.

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; 释放由p指向的自由空间;

如果调用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)的长度

 


虚函数表

 

类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址

  注意的是,编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚成员占据虚函数表中的一行。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。

  虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

  编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值