我是重度拖延症患者,本来这篇总结应该是早就应该写下来的。
一、虚函数表
首先我们看下面的代码:
复制代码
1 class ClassA
2 {
3 public:
4 int m_data1;
5 int m_data2;
6 void vfunc1(){cout << "i am A" << endl;}
7 };
8 class ClassB : public ClassA
9 {
10 public:
11 int m_data3;
12 void funcB(){}
13 void vfunc1(){cout << "i am B" << endl;}
14 };
15 class ClassC : public ClassB
16 {
17 public:
18 int m_data1;
19 int m_data4;
20 void funcC(){}
21 void vfunc1(){cout << "i am C" << endl;}
22 };
23 int main()
24 {
25 ClassA a;
26 ClassB b;
27 ClassC c;
28 cout << sizeof(int) << endl;
29 cout << sizeof(ClassA) << endl;
30 cout << sizeof(ClassB) << endl;
31 cout << sizeof(ClassC) << endl;
32
33 cout << &(a) << endl;
34 cout << &(a.m_data1) <
35 cout << &(a.m_data2) <
36 cout << endl;
37
38 cout << &(b) << endl;
39 cout << &(b.ClassA::m_data1) << endl;
40 cout << &(b.ClassA::m_data2) << endl;
41 cout << &(b.m_data3) <
42 cout << endl;
43
44 cout << &(c) << endl;
45 cout << &(c.ClassA::m_data1) << endl;
46 cout << &(c.m_data2) << endl;
47 cout << &(c.m_data3) << endl;
48 cout << &(c.m_data1) <
49 cout << &(c.m_data4) <
50
51 return 0;
52 }
复制代码
我如果把上面的程序中ClassA的函数vfunc1声明成虚函数,即将第6行改为:
1 virtual void vfunc1(){cout << "i am A" << endl;}
程序运行的两个结果分别为:
由上面的结果可以明显的看出,声明为虚函数的类比原来的类在大小上多了4个字节。没有虚函数的类的起始地址和第一个成员变量的地址保持一致,有虚函数的类的起始地址在第一个成员变量地址的前四个字节。这中间多出来的这四个字节就是隐藏起来的VPTR。VPTR是一个指向一个VTABLE的指针,换句话说,这多出来的四个字节里面存的是VTABLE的地址。
而VTABLE里面就记录了这个类里面虚函数的地址。
再看下面的代码:
1 ClassA *pa;
2 ClassB *pb;
3 ClassC *pc;
4 pa = &c;
5 pa->vfunc1();
我们都知道如果是虚函数,上面的代码结果肯定为
i am C
如果没用虚函数,结果为
i am A
这是怎么做到的?
首先,我们要知道,子类继承父类,子类拥有所有父类的成员变量跟成员函数,就是说:
1 c.vfunc1();
2 c.ClassA::vfunc1()
我们可以上面的方式显示地去访问被子类覆盖掉的函数和变量。可以理解为,虽然名字一样,其实子类里面有两个独立的vfunc1()函数,只不过子类调用的默认为ClassC::vfunc1()函数。
当我们用父类的指针去指向一个子类的指针时,会有一个向上转换(我暂时这么叫)的过程。用pa指向对象c时,pa是一个ClassA类型的指针,pa只能访问ClassA类里面有的成员变量和成员函数地址,多余的,A类没有而C类有的成员变量和函数地址都被“upcasting”掉了。
没有VTABLE时,只能找到ClassA类的vfunc1()函数的地址,找不到ClassC类的vfunc1()函数的地址。有虚函数表的存在时,对象c的虚函数表里面会记录ClassC::vfunc1()的地址,这样用pa指向对象c时,虚函数表不会被“upcasting”掉,于是,按照虚函数表里面的地址,就能够成功访问ClassC::vfunc1()。