虚函数的一般实现模型:每一个class有一个virtual table,内含该class中的virtual function的地址,然后每个object有一个vptr,指向virtual table。
识别class是否支持多态,唯一恰当的方法是看它是否有virtual function,只要class拥有virtual function,它就需要额外的执行期信息(以下两点)。
考虑ptr->z(),ptr是基类指针,z是虚函数,为了找到并调用z()的适当实体,我们需要两项信息:
1.ptr所指对象的真实类型(ptr的动态类型,在运行时决定)
2.z()实体位置(放在virtual table中)
如何实现上述信息?
在每个多态的class object身上添加两个members:
1.一个字符串或数字,表示class的类型
2.一个指针,指向某表格,表格中带有程序的虚函数的执行期地址
在c++中,虚函数可以在编译时获知,它们地址是固定不变的,放在virtual table中。为了找到virtual table,每个class object被安插一个由编译器内部产生的指针,指向该表格。为了找到函数地址,每一个虚函数被指派一个表格索引值。以上的工作由编译器完成,程序运行时要完成的是在特定的virtual table slot(记录着虚函数地址)中激活虚函数。
(编译时把每个类的虚函数地址放到该类对应的virtual table中,运行时根据指针或引用的动态类型在对应的virtual table中找到目标函数并调用)
一个class只有一个virtual table,每个table内含其对应的class object中所有active virtual function的地址。这些active virtual function包括:
1.这个类所定义的函数实体,它会改写一个可能存在的base class virtual function函数实体(覆盖)
2.继承自base class的函数实体,这是在派生类不改写virtual function时才会出现的情况
当一个类继承自另一个类时,会发生以下三种情况:
1.它可以继承base class 所声明的virtual function的函数实体,即该函数实体的地址会被拷贝到派生类的virtual table相对应的slot中
2.它可以使用自己的函数实体,该函数实体覆盖了父类的虚函数(覆盖)
3.它可以加入一个新的虚函数,这时候virtual table的尺寸会增加一个slot,而新的函数实体地址会被放进该slot之中
----------------------------------------------------------------------------------------------------------------------------------
虚函数表详解(转载)
一般继承(无虚函数覆盖)
对于实例Derive d的虚函数表如下:
我们可以看到:
1)虚函数按照其声明顺序放入表中
2)父类的虚函数在子类的虚函数前面
一般继承(有虚函数覆盖)
对于派生类的实例,其虚函数表如下:
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
b的动态类型是派生类指针,在派生类的虚函数表中查找Derive函数,而在该表中Derive函数为派生类函数实体地址,所以调用的是派生类函数。由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的),即子类有多个虚表,分别对应不同的父类,自己自定义的虚函数放在第一个虚表中
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。
class a { public: virtual void f1(){} }; class b { public: virtual void f2(){} }; class c { public: virtual void f3(){} }; class d: public a, public b, public c { public: virtual void f4(){} }; void main() { cout << sizeof(a) << " " << sizeof(b) << " " << sizeof(c) <<" "<<sizeof(d)<< endl; system("pause"); }
vs2013输出为4,4,4,12
安全性
一、通过父类型的指针访问子类自己的虚函数
任何妄图使用父类指针想调用子类中的独有的的成员函数的行为都会被编译器视为非法,因为这些成员不在父类指针所指内存空间里,编译器是根据指针的静态类型来确定调用对象的地址空间的,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。
二、访问non-public的虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们(类的用户不能访问类的private或protected成员)同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。