首页 > 技术文章 > (C/C++学习)4.C++类中的虚函数表Virtual Table

tuihou 2018-09-27 17:02 原文

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

 

下面介绍一下与这张虚函数表有关的几个问题:

1.普通成员函数不占存储空间,而所有虚函数入口地址存储在一张虚函数表中,由一个指针指向该虚函数表;

2.指向该虚函数表的指针位于类实例对象内存的最前面,占四个字节;

3.若子类覆写了父类的虚函数,则父类的虚函数被覆盖,即虚函数表中只存在子类的虚函数地址;否则,父类和子类的虚函数都存在于虚函数表中(当然,没有覆写父类的虚函数是毫无意义的),这就是多态形成的原因。

 

通过上面的介绍,我们对虚函数表有了大致的了解,下面通过一个实例来加深一下认识:

  1 #include <iostream>
  2 using namespace std;
  3 
  4 class base
  5 {
  6 public:
  7     virtual void f(){cout<<"base::f()"<<endl;}
  8     virtual void g(){cout<<"base::g()"<<endl;}
  9     virtual void h(){cout<<"base::h()"<<endl;}
 10 private:
 11     int a;
 12 };
 13 
 14 //定义一个函数指针,并别名为pfunc,用时不需再加*,
 15 typedef void (*pfunc)(void);
 16 
 17 int main()
 18 {
 19     base b;
 20 
 21     //C++编译器使虚函数表的指针存在于对象实例中的最前面(四个字节)
 22     cout<<"sizeof(base) = "<<sizeof(base)<<'\t'<<"sizeof(b) = "<<sizeof(b)<<endl<<'\n';
 23 
 24     //分别打印对象b的起始地址和虚函数表中首个函数指针指向的地址
 25     //对象实例最前面的四个字节为指向虚函数表的指针,取内容后才为虚函数表
 26     cout<<"&b = "<<&b<<"\t\t"<<"&VTable = "<<(int **)*(int *)(&b)<<endl<<"\n\n";
 27 
 28     pfunc pf;
 29     //定义一个函数指针
 30     void(*p)(void);
 31     //还可以这样定义一个函数指针
 32 
 33     //虚函数表里面存放的是指向各个虚函数的指针,取内容后才是各个相应的虚函数
 34     pf = (pfunc)*((int **)*(int *)(&b)+0);
 35     pf();
 36     pf = (pfunc)*((int **)*(int *)(&b)+1);
 37     pf();
 38     pf = (void(*)())*((int **)*(int *)(&b)+2);
 39     pf();
 40 
 41     cout<<"\n\n";
 42 
 43     p = (pfunc)*((int **)*(int *)(&b)+0);
 44     p();
 45     p = (void(*)())*((int **)*(int *)(&b)+1);
 46     p();
 47     p = (void(*)())*((int **)*(int *)(&b)+2);
 48     p();
 49 
 50     return 0;
 51 }
 52 

程序运行结果:

2获

通过以上示例,我们把类实例对象b取址,然后将&b强转成int*型,然后对其取内容,取得虚函数表的地址,然后再对其取内容,就得到了第一个虚函数的地址了,然后再将其通过(int**)强转成步长为4的指针,通过加1来得到虚函数表中不同的虚函数的地址,最终强转成为函数指针,再通过该函数指针访问相应的虚函数.

5.下面我们将通过几个例子来解释一下虚函数表的存在形式,在这部分,主要弄清楚虚函数表是怎么一回事,至于程序运行结果,读者自行实验。

a.在父子类中,若子类没有对父类的虚函数进行覆写(当然,前面提到过,没有覆写父类的虚函数是毫无意义的。之所以要讲述没有覆写的情况,主要目的是为了给一个对比,在比较之下,我们可以更加清楚地知道其内部的具体实现),如下代码,

  1 #include<iostream>
  2 using namespace std;
  3 class base
  4 {
  5 public:
  6     virtual void func(){};
  7     virtual void foo(){};
  8 };
  9 class derive:public base
 10 {
public: 11 virtual void func1(){}; 12 virtual void foo1(){}; 13 }; 14 int main() 15 { 16 derive d; 17 return 0; 18 } 19

则其虚函数表如下所示:

无标题

注意:

1.上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。

2.虚函数是按照其声明顺序放于表中的。

3.父类的虚函数在子类的虚函数前面。

 

b.在父子类中,若子类对父类的虚函数进行了覆写(为了对比,假设只覆写父类一个虚函数),如下代码,

  1 #include<iostream>
  2 using namespace std;
  3 class base
  4 {
  5 public:
  6     virtual void func(){};
  7     virtual void foo(){};
  8     virtual  ~base(){}
  9 };
 10 class derive:public base
 11 {
 12 public:
 13     virtual void func(){cout<<"___"<<endl;};
 14     virtual void foo1(){};
 15     virtual  ~derive(){}
 16 };
 17 int main()
 18 {
 19     base *p = new derive;
 20     p->func();
 21     delete p;
 22     return 0;
 23 }
 24 

则其虚函数表如下所示:

无标题

由此,可得覆写的子类func()放在了虚函数表中原来父类func()的位置,没有覆写的虚函数依旧原样存放。这样,在上述代码中,由于p所指的func()的位置已经被derive::func()的函数地址所取代,因此在发生实际调用的时候,调用的是子类的func(),这就实现了多态。

推荐阅读