晚绑定

http://dev.csdn.net/author/bodybo/9284c0eb1a5b4ae4b68e86cd7668c429.html

①编译器在后台为每个包含虚函数的类产生一个静态函数指针数组(虚函数表vtbl),在这个类或者它的基类中定义的每一个虚函数都有一个相应的函数指针。
②每个包含虚函数的类的每一个实例包含一个不可见的数据成员vptr(虚函数指针),这个指针被构造函数自动初始化,指向类的vtbl(虚函数表)
③当客户调用虚函数的时候,编译器产生代码反指向到vptr,索引到vtbl中,然后在指定的位置上找到函数指针,并发出调用。

 

 2、揭密晚绑定的秘密

 编译器到底做了什么实现的虚函数的晚绑定呢?我们来探个究竟。

 编译器对每个包含虚函数的类创建一个表(称为V TA B L E)。在V TA B L E中,编译器放置特定类的虚函数地址。在每个带有虚函数的类中,编译器秘密地置一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个V P T R,并在V TA B L E表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置V TA B L E、初始化V P T R、为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。利用虚函数,这个对象的合适的函数就能被调用,哪怕在编译器还不知道这个对象的特定类型的情况下。(C++编程思想》)

 

那我们来看看编译器是怎么建立VPTR指向的这个虚函数表的。先看下面两个类:
class base
{
public:
    void bfun(){}
    virtual void vfun1(){}
    virtual int vfun2(){}
private:
    int a;
}

class derived : public base
{
public:
    void dfun(){}
    virtual void vfun1(){}
    virtual int vfun3(){}
private:
    int b;
}

两个类VPTR指向的虚函数表(VTABLE)分别如下:
base类
                      ——————
VPTR——> |&base::vfun1 |
                      ——————
                 |&base::vfun2 |
                  ——————
      
derived类
                      ———————
VPTR——> |&derived::vfun1 |
                      ———————
                  |&base::vfun2    |
                  ———————
                  |&derived::vfun3 |
                   ———————
    
每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就为这个类创建一个VTABLE,如上图所示。在这个表中,编译器放置了在这个类中或在它的基类中所有已声明为virtual的函数的地址。如果在这个派生类中没有对在基类中声明为virtual的函数进行重新定义,编译器就使用基类的这个虚函数地址。(在derived的VTABLE中,vfun2的入口就是这种情况。)然后编译器在这个类中放置VPTR。当使用简单继承时,对于每个对象只有一个VPTR。VPTR必须被初始化为指向相应的VTABLE,这在构造函数中发生。
一旦VPTR被初始化为指向相应的VTABLE,对象就"知道"它自己是什么类型。但只有当虚函数被调用时这种自我认知才有用。

VPTR常常位于对象的开头,编译器能很容易地取到VPTR的值,从而确定VTABLE的位置。VPTR总指向VTABLE的开始地址,所有基类和它的子类的虚函数地址(子类自己定义的虚函数除外)在VTABLE中存储的位置总是相同的,如上面base类和derived类的VTABLE中vfun1和vfun2的地址总是按相同的顺序存储。编译器知道vfun1位于VPTR处,vfun2位于VPTR+1处,因此在用基类指针调用虚函数时,编译器首先获取指针指向对象的类型信息(VPTR),然后就去调用虚函数。如一个base类指针pBase指向了一个derived对象,那pBase->vfun2()被编译器翻译为 VPTR+1 的调用,因为虚函数vfun2的地址在VTABLE中位于索引为1的位置上。同理,pBase->vfun3()被编译器翻译为 VPTR+2的调用。这就是所谓的晚绑定。

 

 我们来看一下虚函数调用的汇编代码,以加深理解。

 void test(base* pBase)
 {
  pBase->vfun2();
 }

 int main(int argc, char* argv[])
 {
  derived td;
  

 
  test(&td);
  
  return 0;
 }

 derived td;编译生成的汇编代码如下:
  mov DWORD PTR _td$[esp+24], OFFSET FLAT:??_7derived@@6B@ ; derived::`vftable'
  由编译器的注释可知,此时PTR _td$[esp+24]中存储的就是derived类的VTABLE地址。
  
 test(&td);编译生成的汇编代码如下:
  lea eax, DWORD PTR _td$[esp+24]    
  mov DWORD PTR __$EHRec$[esp+32], 0
  push eax
  call ?test@@YAXPAVbase@@@Z   ; test 
  调用test函数时完成了如下工作:取对象td的地址,将其压栈,然后调用test

 pBase->vfun2();编译生成的汇编代码如下:
   mov ecx, DWORD PTR _pBase$[esp-4]
  mov eax, DWORD PTR [ecx]
  jmp DWORD PTR [eax+4]
   首先从栈中取出pBase指针指向的对象地址赋给ecx,然后取对象开头的指针变量中的地址赋给eax,此时eax的值即为VPTR的值,也就是 VTABLE的地址。最后就是调用虚函数了,由于vfun2位于VTABLE的第二个位置,相当于 VPTR+1,每个函数指针是4个字节长,所以最后的调用被编译器翻译为 jmp DWORD PTR [eax+4]。如果是调用pBase->vfun1(),这句就该被编译为 jmp DWORD PTR [eax]

你可能感兴趣的:(晚绑定)