知识点十四、虚函数表和虚函数针
虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数。典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式。vptr指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到vtbl。当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的vptr指向的vtbl,然后在vtbl中寻找合适的函数指针。
虚拟函数的地址翻译取决于对象的内存地址,而不取决于数据类型(编译器对函数调用的合法性检查取决于数据类型)。如果类定义了虚函数,该类及其派生类就要生成一张虚拟函数表,即vtable。而在类的对象地址空间中存储一个该虚表的入口,占4个字节,这个入口地址是在构造对象时由编译器写入的。所以,由于对象的内存空间包含了虚表入口,编译器能够由这个入口找到恰当的虚函数,这个函数的地址不再由数据类型决定了。故对于一个父类的对象指针,调用虚拟函数,如果给他赋父类对象的指针,那么他就调用父类中的函数,如果给他赋子类对象的指针,他就调用子类中的函数(取决于对象的内存地址)。
每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。在每个带有虚函数的类中,编译器秘密地置入一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E。 当构造该派生类对象时,其成员VPTR被初始化指向该派生类的VTABLE。所以可以认为VTABLE是该类的所有对象共有的,在定义该类时被初始化;而VPTR则是每个类对象都有独立一份的,且在该类对象被构造时被初始化。
通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个VPTR,并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置VTABLE、初始化VPTR、为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。
1)简单继承:
总结一下VPTR 和 VTABLE 和类对象的关系:
每一个具有虚函数的类都有一个虚函数表VTABLE,里面按在类中声明的虚函数的顺序存放着虚函数的地址,这个虚函数表VTABLE是这个类的所有对象所共有的,也就是说无论用户声明了多少个类对象,但是这个VTABLE虚函数表只有一个。
在每个具有虚函数的类的对象里面都有一个VPTR虚函数指针,这个指针指向VTABLE的首地址,每个类的对象都有这么一种指针。
2)虚继承:
这个是比较不好理解的,对于虚继承,若派生类有自己的虚函数,则它本身需要有一个虚指针,指向自己的虚表。另外,派生类虚继承父类时,首先要通过加入一个虚指针来指向父类,因此有可能会有两个虚指针。
(第一,在虚继承中,引入一个指针指向虚继承的基类;第二由于在基类中有虚函数,因此需要指针指向其虚函数表;第三,由于派生类自己本身也有自己的虚函数,因为采取的是虚继承,因此它自己的虚函数不会放到基类的虚函数表的后面,而是另外分配一个只存放自己的虚函数的虚函数表,于是又引入一个指针)
1、单继承(无虚函数覆盖)
假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有覆盖任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
假设我们有这样的一个类:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
对于实例:Derive d; 的虚函数表如下:
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
2、单继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数覆盖了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f ()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
我们从表中可以看到下面几点,
1)覆盖的f ()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f ()的位置已经被Derive::f ()函数地址所取代,于是在实际调用发生时,是Derive::f ()被调用了。这就实现了多态。
3、多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
由于每个基类都需要一个指针来指向其虚函数表,因此d的sizeof等于d的数据成员加3*4=12。
4、多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f ()函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f ()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f ()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
当类中没有任何成员变量,也没有虚函数的时候,该类的大小是1.
附:虚函数的入口地址和普通函数有什么不同?
答:每个虚函数在虚函数表vtable中占了一个表项,保存了它的入口地址。当一个包含虚函数的对象被创建的时候,它在头部附加一个指针,指向虚函数表vtable中的相应位置。调用虚函数的时候,不管用什么制作调用,它先根据vtable找到虚函数的入口地址,再执行。从而实现了“动态联编”。而不像普通函数那样简单地跳转到一个固定地址。
附上链接
http://blog.chinaunix.net/uid-25132162-id-1564955.html
http://www.cnblogs.com/BeyondTechnology/archive/2010/09/21/1832369.html
http://www.cnblogs.com/BeyondAnyTime/archive/2012/06/05/2537451.html