在C++中,多态表示 “以一个公有基类的指针或引用,寻址出一个派生类对象” 。
假如有调用 ptr->get_c() ,其中ptr是基类指针,get_c()是一个虚函数。要在执行期能正确调用get_c()的实例,我们需要知道:
1.ptr所指对象的真正类型,以便我们选择正确的get_c()实例。
2.get_c()实例的位置,以便我们能够调用他。
在实现上,编译时期会构建出来一张虚表,表格中有程序的虚函数的执行期地址。
为了找到这个表格,每一个类对象被安插一个由编译器产生的指针,指向虚表。
为了找到函数地址,每一个虚函数被指派一个表格索引值。
如果有代码例子:
class A { protected: int a; public: A(const int a) :a(a){} virtual ~A(); virtual int pure_v() = 0; //纯虚函数 virtual int get_b(){ return 0; } virtual int get_c(){ return 0; } }; class B :public A { protected: int b; public: B(int a = 0, int b = 0):A(a),b(b){} ~B(); int pure_v(); int get_b(){ return b; } }; class C :public B { protected: int c; public: C(int a = 0, int b = 0, int c = 0) :B(a, b), c(c){} ~C(); int pure_v(); int get_c(){ return c; } };
一个类只会有一张虚表。每个表内含其对应类对象中三类虚函数,这三类包括:
1.这一类所定义的虚函数实例或改写自基类的虚函数实例。如例子中Calss B中的get_b()。
2.继承自基类的已被改写过的函数实例。如例子中Calss C的虚表中会有get_b(),在C中不改写,而在B中已被改写过。
3.一个纯虚函数的实例。如Class B中的pure_v()。
接下来我们看类A,B,C的虚表布局。
对于A a; 布局如下:(博主是用visio画的,有点丑- -)
_vptr是编译时期产生的虚表指针。
slot 0通常是指出每个类所关联的type_info object(用以支持RTTI)。
A的虚析构被指派为solt 1。纯虚函数被指派为solt 2,但A中的纯虚函数没有定义,如果意外调用了此函数,通常的操作是结束这个程序。
get_b()被指派slot ,get_c()被指派slot 4。
对于B:A b; 布局如下:
B的虚表中在solt 1指出析构函数。A中的纯虚函数有了定义,所以在slot 2指出pure_()。
自己的get_b()函数实例地址放在solt 3。继承自A的get_c()函数实例地址放在solt 4。
对于C:B c; 布局如下:
solt 1放置析构函数地址,solt 2放置pure_v()函数地址,solt 3放置继承自B的get_b()的函数地址,solt 4放置自己的get_c()函数地址。
现在再来看调用ptr->get_c();
*在每次调用get_c()时,我们并不知道ptr所指对象的真正类型。但是我知道通过ptr可以存取到该对象的虚表。
*虽然我不知道哪一个get_c()函数实例会被调用,但是我知道每一个get_c()的函数实例地址都被放在solt 4中。
通过这些信息,编译器可以把调用改写为:
(ptr->_vptr[4])(ptr);
从而在执行期调用正确的函数实例。