本文分析虚函数的小秘密,通过几个case说明为了支持虚函数,应该有什么样的约定,生成什么样的代码。
C++中虚函数用于实现多态:即方法调用和对象的动态类型绑定。
详细地说对A*类型指针p指向A的公有派生类B的对象,A中有虚函数foo,B中给定foo的还有一份实现,p->foo应该和B中的新实现绑定,而不是和A中的实现绑定。
一般而言。会在对象布局中插入一个虚函数表指针。在表中列出了全部的虚函数。
以下以这样的模型为基础讨论。
先看基类A,假定有数据成员dataA,虚函数表指针vptrA。虚函数fooA()。
从fooA本身的实现来看,在thiscall的约定下,觉得ecx作为输入參数,当中的值是this指针,this指针的类型当然是A*了。
要调用虚函数则要满足例如以下条件:
1. 可以找到fooA的实现地址。
2. ecx中含有this,this的类型是A*(即this确实指向了A的对象,而不会是A的派生类的对象)。
在此,引入一个不变式:
假设有A* ptr;那么ptr指向的内存数据的理解应该全然由A这个类型决定。不管ptr指向的确实是是一个A的对象,或者A的派生类的对象。
既然如此,派生类对象应该存在某段区域,这段区域能够看到一个A的对象。ptr指向派生类时。应该指向派生类对象的这段区域。
[不变式使得向上向下转型时有指针重设,使得在ptr->foo的代码中能够断言ptr指向的对象是什么,使得能够用不变的几步操作来实现对foo的调用]
case 1
假设我们有A* ptr = new A();虚函数的调用实现应该是,从ptr指向的数据拿到虚表指针,依据偏移,进而拿到A::fooA的详细地址。将ptr直接放到ecx中。
所以满足fooA的调用条件能够满足。
假设有B继承自A,B中引入虚函数fooB,数据dataB。没有override了fooA。一个可能的内存布局是:
vptrB dataB vptrA dataA。
case 2
假设有A* ptr = new B();依据前面的不变式ptr会指向vptrA的位置。假设在vptr中fooA的位置放上fooA实现的地址。这个时候调用方法没问题,和前面讨论的一样。
case 3
假设有B* ptr = new B();依据类型信息B,通过偏移量拿到vptrA。进而能拿到fooA的地址。此外,拿到vptrA的同一时候,也拿到了A-sub-object的地址。将这个sub object的地址放到ecx中,于是虚函数调用条件满足。
再考虑B中override了fooA的情况,最好还是设这个override的函数名为fooA_override_by_B:在构造B的时候。将fooA_override_by_B填入了vptrA中相应位置。
case 4
对于A* ptr = new B();传入的ecx指向的是A这个sub-object,实际调用到的函数是fooA_override_by_B。而依据虚函数调用条件,fooA_override_by_B会觉得传入的是B*。
所以fooA_override_by_B分为两部分,当中一部分fooA_override_by_B_impl是详细实现,会觉得传入的this是B*的。还有一部分fooA_override_by_B_adjust会将传入的A*调整为B*,然后跳转到fooA_override_by_B_impl。
vptrA中放的应该是fooA_override_by_B_adjust。
case 5
对于B* ptr = new B();在外部将ecx指向了A-sub-object,在fooA_override_by_B_adjust中又将指向A-sub-object的对象调整为指向B的对象。
所以。在发生override时,一方面提供相应的实现函数,这个函数接受正在override的类的指针。还有一方面在被override的类的虚函数表中。放上调整函数,将父类指针调整为子类指针。这样做是可行的,由于被override的类和正在override的类的信息是编译时确定的。
讨论到这里,我们在有T* ptr时,调用虚函数的条件是这样满足的:
1. 依据T这个类型。及其继承关系。考察被调用的虚函数,找到相应的虚表指针。然后依据虚函数在虚表中的位置。确定虚函数的地址。
当中T的类型,继承关系,被调用的虚函数,某个类引入的虚函数在虚表中的位置,ptr的值,这5个量是已知的。未知的是虚函数的位置。
2. 相同被调用的虚函数所属的类在T中的偏移也是个已知量,加上ptr就得到相应的sub-object的地址。
再看个多继承的样例,假设A,B作为基类。都有虚函数fooX。C继承自A,B。override了fooX。
这个时候C中有三个虚函数表,在A的虚函数表中fooX应该是fooX_A_override_by_C_adjust。在B的虚函数表中foo应该是fooX_B_override_by_C_adjust。
和前面的不同,我们用fooX_A表示fooX是A中的,fooX_B是B中的,两者名字同样。我们用这个后缀以作差别。两个adjust函数能够分别将A和B的指针调整为C的指针。然后分别都跳转到fooX_A_override_by_C_impl,fooX_B_override_by_C_impl实际上。这两个impl函数是同一个。当然。第三个虚函数表是C自己能够引入的虚函数,在此不影响讨论。
虚函数有什么秘密?
1.[指针约定、对象布局]形如T* ptr;应当觉得指向的内存開始sizeof(T)个字节确实是一个T的对象。
这就要求向上或向下转型时。有指针重设。在对象布局中子类中存在某段区域。这段区域正好是基类对象。
2.[引入虚函数表]对象中有指针指向虚函数表。使得在指向不同的虚函数表的时候,虚函数调用有不同的表现。
3.[虚函数表中的项对this的需求]在call虚函数表的某一项时,ecx中保存的是虚函数表相应的对象的this指针。
4.[override后的this改动]在子类override时,对于每一处须要改动的虚函数表(一般仅仅有一个),因为3满足,所以能够插入将这个对象转为子类对象的转换代码。然后跳转到override后的实现。
5.[可能override多个虚函数表中的项]有多个须要改动的虚函数表时说明通过该对象同一时候override多个实现。
注:虚函数表共享在里没有被提及,可是并不影响分析。
全部的这些结果,依据经验推算而来,不代表实现中一定是这样。