(C/C++学习)4.C++类中的虚函数表Virtual Table
说明: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
程序运行结果:
通过以上示例,我们把类实例对象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(),这就实现了多态。