基本的类布局

       为了解释下面的内容,让我们看看这个简单例子:

 

   

 class A
    {
      int a1;
    public:
      virtual int A_virt1();
      virtual int A_virt2();
      static void A_static1();
      void A_simple1();
    };
 
    class B
    {
      int b1;
      int b2;
    public:
      virtual int B_virt1();
      virtual int B_virt2();
    };
 
    class C: public A, public B
    {
      int c1;
    public:
      virtual int A_virt2();
      virtual int B_virt2();
};

 

       多数情形下,MSVC的类按如下格局分布:

Ÿ   指向虚函数表的指针(_vtable__vftable_),不过它只在类包括虚函数,以及不能从基类复用合适的函数表时才会被添加。

Ÿ   基类。

Ÿ   函数成员。

 

虚函数表由虚函数的地址组成,表中函数地址的顺序和它们第一次出现的顺序(即在类定义的顺序)一致。若有重载的函数,则替换掉基类函数的地址。

因此,上面三个类的布局看起来象这样:

 

    

class A size(8):
        +---
     0  | {vfptr}
     4  | a1
        +---
 
    A's vftable:
     0  | &A::A_virt1
     4  | &A::A_virt2
 
    class B size(12):
        +---
     0  | {vfptr}
     4  | b1
     8  | b2
        +---
 
    B's vftable:
     0  | &B::B_virt1
     4  | &B::B_virt2
 
    class C size(24):
        +---
        | +--- (base class A)
     0  | | {vfptr}
     4  | | a1
        | +---
        | +--- (base class B)
     8  | | {vfptr}
    12  | | b1
    16  | | b2
        | +---
    20  | c1
        +---
 
    C's vftable for A:
     0  | &A::A_virt1
     4  | &C::A_virt2
 
    C's vftable for B:
     0  | &B::B_virt1
     4  | &C::B_virt2

 

上面的图表是由VC8编译器使用一个未公开的参数生成。为了看到这样的类布局,使用编译参数 –d1 reportSingleClassLayout,可以输出单个类的布局。-d1 reportAllClassLayout可以输出全部类的布局(包括内部的CRT类)。这些内容都被输出到stdout(标准输出)。

       正如你看到的,C有两个虚函数表vftables,因为它从两个都有虚函数的类继承。C::A_virt2的地址替换了A::A_virt2在类C虚函数表的地址,类似的,C::B_virt2替换了B::B_virt2

调用惯例和类方法

       MSVC中所有的类方法都默认使用_thiscall_调用惯例。类实例的地址(_this_指针)作为隐含参数传到ecx寄存器。在函数体中,编译器通常立刻用其它寄存器(如esied),或栈中变量来指代。以后对类成员的引用都通过这个寄存器或栈变量。然而,在实现COM类时,则使用_stdcall_调用习惯。下文是对各种类型的类方法的一个概述。

 

1)      静态方法

调用静态方法不需要类的实例,所以它们和普通函数一样的工作原理。没有_this_指针传入。因此也就不可能可靠的分辨静态方法和简单的普通函数。例如:

 

A::A_static1();

call    A::A_static1

 

2)      简单方法

简单方法需要一个类实例,_this_指针隐式的作为第一个参数传入,通常使用_thiscall_调用惯例,例如通过_ecx_寄存器。当基类对象没有分配在派生类对象的开始处,在调用函数前,_this_指针需要被调整到指向基类子对象的实际开始位置。例如:

 

    

;pC->A_simple1(1);
    ;esi = pC
    push    1
    mov ecx, esi
    call    A::A_simple1
 
    ;pC->B_simple1(2,3);
    ;esi = pC
    lea edi, [esi+8] ;adjust this
    push    3
    push    2
    mov ecx, edi
    call    B::B_simple1

 

正如你看到的,在调用B的方法前,_this_指针被调整到指向B的子对象。

3)      虚方法(虚函数)

为了调用虚函数,编译器首先需要从_vftable_取得函数地址,然后就像调用简单方法一样(例如,传入_this_指针作为隐含参数)。例如:

 

    

;pC->A_virt2()
    ;esi = pC
    mov eax, [esi]  ;fetch virtual table pointer
    mov ecx, esi
    call [eax+4]  ;call second virtual method
   
    ;pC->B_virt1()
    ;edi = pC
    lea edi, [esi+8] ;adjust this pointer
    mov eax, [edi]   ;fetch virtual table pointer
    mov ecx, edi
call [eax]       ;call first virtual method

 

4)      构造函数和析构函数

构造函数和析构函数类似于简单方法,它们取得隐式的_this_指针(例如,在_thiscall_调用惯例下通过ecx寄存器)。虽然形式上构造函数没有返回值,但它在eax中返回_this_指针。

打赏