说明: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(),这就实现了多态。