Chinaunix首页 | 论坛 | 博客
  • 博客访问: 178443
  • 博文数量: 43
  • 博客积分: 827
  • 博客等级: 准尉
  • 技术积分: 487
  • 用 户 组: 普通用户
  • 注册时间: 2012-01-26 19:19
文章分类

全部博文(43)

文章存档

2015年(1)

2014年(1)

2013年(5)

2012年(36)

我的朋友

分类: C/C++

2012-04-23 11:41:56

本文接前面"Be Careful with Pointer to Member Function",对成员函数指针做了一个更深入的探讨,本文的内容来源于个人对VS生成的一些汇编码以及在Jan Gray, "C++: Under the Hood"文中一些内容的理解上而推出的一些结论,其中可能存在一些不正确的结论,如果读者对相关内容有更深入的理解,很感激能告诉我。

对于成员函数期望的this指针的类型

对于非多态函数,他期望的this指针是当前定义该函数的类型的指针
在继承情况的时候,如果基类定义了这个函数,但是在子类没有定义这个函数,则这个函数期望的是基类的指针
如果子类重写定义了这个函数,那么这个函数期望的将是子类的指针。
class A
{
public:
    int a;
};

class B
{
public:
    void funcB(){}
    int b;
};

class C : public A, public B
{
public:
    void funcB(){}
    int c;
};

这里C::funcB期望的是C*, B::funcB期望的是B*,如果C没有定义funcB,则C::funcB期望的是B*。

对于多态函数,他期望的this指针是最先引入该多态函数的类型的指针
比如
class Base
{
public:
        virtual void func(){}
};

class Dummy
{
public:
        virtual void dummy(){}
};
class Derive : public Dummy, public Base
{
    public:
            virtual void func(){}
}; 

这里Derive重写了Base中的func(),但是func期望的this仍然是Base*,所以在使用Derive*调用func的时候,实际上会将Derive*转化为Base*来调用(也就是在调用之前有一个offset施加)。

但是对于这种情况
class Base
{
public:
        virtual void func(){}
};

class Dummy
{
public:
        virtual void func(){}
};
class Derive : public Dummy, public Base
{
    public:
            virtual void func(){}
};

Derive重写了Base和Dummy中的func,因为在继承声明中,对于func最先导入的是Dummy类,所以在Derive中的func期望的是Dummy*(这里也就是Derive*,但是,如果Dummy前面还有一个基类Base0,并且Base0也有虚函数(如果没有虚函数,编译器会做优化,把Base0调到所有具有虚函数基类的后面去),这个时候Dummy*就不是Derive*了,注意这一点),而对于Base下来的func实际上仍然期望的是Base*,但是在Derive中对于Base的虚表中最后要调用的是Derive::func,而Derive::func期望的是Dummy*,这个时候Base*就需要转化为Dummy*,而这个转化过程在一个thunk中完成,thunk中将Base*转化为Dummy*然后跳转到Derive::func中,这被称为adjust-and-jump技术。
其实也就是说,Derive中对于Dummy的func和Base的func仍然期望的是原始类型的this,但是在Derive中的func期望的是在继承中最先声明的基类的this,而对于后面的基类,则引入thunk来进行修正。因此最后在Derive::Dummy虚表中直接记录的是Derive::func,在Derive::Base虚表中记录的是一个thunk。
如下面的图所示

thunk什么时候才会被引入?当Derive类中的一个虚函数override了多个父类的虚函数,这个时候,在这些父类总对于不是最左边的父类的虚表中对应虚函数项保存的是一个thunk。仅当这个时候会引入thunk。

注意:对于虚继承部分,这里不讨论,那个有些复杂。

在上面,弄清了,一个成员函数他期望的this指针应该是什么类型,那么后面来讨论对于成员函数指针的问题。

typedef ret_type(ClassType::* MemFunc)(args);
在这个typedef中,MemFunc为一个成员函数指针类型。
MemFunc memFunc;
对于这样一个语句,对于编译器而言,他定义了对于MemFunc类型的成员函数,他所期望的this总是为ClassType*。

1:
那么对于任意一个对象或者对象指针obj,pObj.
(obj.*memFunc)();
(pObj->*memFunc)();
他们都会将&obj和pObj隐式转为ClassType*再进行后面的调用过程。如果&obj和pObj不能隐式转为ClassType*,那么编译器就会出错。
这个过程只完成了一步。

2:
对于具体的成员函数指针的赋值过程
memFunc = &ClassType::Func;
编译器在处理这个过程的时候,如前面所说Func自己有一个自己期望的this类型,假设记为Target*类型,而memFunc在1中得到的总是为ClassType*,所以这个时候需要保存一些额外的信息。
1) 如果Target*和ClassType*没有偏移施加,那么memFunc就直接保存&ClassType::Func的地址值,这个时候MemFunc的大小为4bytes。这通常是单继承的情况。
2) 如果Target*和ClassType*仅仅相差一个Offset,那么memFunc不仅要保存&ClassType::Func的地址值,还需要保存这个偏移值,这个时候MemFunc的大小为8bytes。4bytes保存funcPtr,4bytes保存offset。这通常是多继承的情况
3) 如果Target*和ClassType*具有虚拟继承的情况,那么这个时候,不仅仅是Offset和&ClassType::Func的地址,还需要一个在vbtable上的偏移。这个时候MemFunc大小为12bytes。4bytes保存funcPtr,4bytes保存offset,4bytes保存vbtable上的偏移。这通常是在虚继承的情况
所以成员函数指针不能同等于一般的函数指针。

那么整个成员函数指针的调用过程为:
1:将obj或者pObj转为ClassType*作为当前的this。
2:通过memFunc中保存的信息对this施加偏移,得到memFunc本身所代表的成员函数所期望的类型的this。
3:跳转到memFunc保存的funcPtr。

例子:
class Dummy
{
public:
    int d;
    virtual void funcD(){}
};

class A
{
public:
    virtual void vfunc(){}
    int a;
};

class B 
{
public:
    virtual void vfunc(){}
    int b;
};

class E : public Dummy, public A , public B
{
public:
    virtual void vfunc(){}
    int f;
};



int main()
{
    E e;
    typedef void(B::* BMemFunc)();

    BMemFunc bMemFunc = &B::vfunc;
    (e.*bMemFunc)();

    return 0;
}

分析:
typedef void(B::* BMemFunc)(); 指定BMemFunc为期望B*
BMemFunc bMemFunc = &B::vfunc; B::vfunc期望为B*
所以bMemFunc不会保存偏移值
(e.*bMemFunc)(); 这里e会先得到B*,然后直接跳转到B::vfunc。
E::B::vftabel中保存的是一个thunk,所以在thunk中会将B*修正为A*,然后进入到E::vfunc.

汇编码:
BMemFunc bMemFunc = &B::vfunc;        
00411476  mov         dword ptr [bMemFunc],offset B::`vcall'{0}' (411249h)       // No offset
(e.*bMemFunc)();
0041147D  lea         eax,[e] 
00411480  test        eax,eax 
00411482  je          main+42h (411492h) 
00411484  lea         ecx,[e] 
00411487  add         ecx,10h                         //Cast E* to B*
0041148A  mov         dword ptr [ebp-0F4h],ecx 
00411490  jmp         main+4Ch (41149Ch) 
00411492  mov         dword ptr [ebp-0F4h],0 
0041149C  mov         esi,esp 
0041149E  mov         ecx,dword ptr [ebp-0F4h] 
004114A4  call        dword ptr [bMemFunc]            
        00411249  jmp         B::`vcall'{0}' (411590h) 
                B::`vcall'{0}':
                00411590  mov         eax,dword ptr [ecx] 
                00411592  jmp         dword ptr [eax] 
                        0041123F  jmp         [thunk]:E::vfunc`adjustor{8}' (411880h) 
                                [thunk]:E::vfunc`adjustor{8}':
                                00411880  sub         ecx,8      // Cast B* to A*
                                00411883  jmp         E::vfunc (411127h)  // Jump to E::vfunc


将上面的例子改为:
int main()
{
    E e;
    typedef void(E::* EMemFunc)();

    EMemFunc eMemFunc = &B::vfunc;
    (e.*eMemFunc)();

    return 0;
}
分析:
typedef void(E::* EMemFunc)();表明EMemFunc总是期望为E*
EMemFunc eMemFunc = &B::vfunc;这个时候B::vfunc期望的是B*
所以这个时候需要一个偏移
汇编码:
   EMemFunc eMemFunc = &B::vfunc;
00411476  mov         dword ptr [ebp-100h],offset B::`vcall'{0}' (411249h) // Record the funcPtr
00411480  mov         dword ptr [ebp-0FCh],10h         // Record the offset
0041148A  mov         eax,dword ptr [ebp-100h] 
00411490  mov         dword ptr [eMemFunc],eax         // Record the funcPtr 
00411493  mov         ecx,dword ptr [ebp-0FCh] 
00411499  mov         dword ptr [ebp-2Ch],ecx          // Record the offset
    (e.*eMemFunc)();
0041149C  mov         eax,dword ptr [ebp-2Ch]          // Get the recorded offset 
0041149F  lea         ecx,e[eax]                       // Do the offset in E*, and ecx stores B* 
004114A3  mov         esi,esp 
004114A5  call        dword ptr [eMemFunc]             // The same as above 


如果例子改为     
int main()
{
    E e;
    typedef void(E::* EMemFunc)();

    EMemFunc eMemFunc = &E::vfunc;
    (e.*eMemFunc)();

    return 0;
}                             
这个时候E::vfunc期望的是A*,所以变化的只有offset

汇编码:
    EMemFunc eMemFunc = &E::vfunc;
00411476  mov         dword ptr [ebp-100h],offset B::`vcall'{0}' (41124Eh) 
00411480  mov         dword ptr [ebp-0FCh],8   // Record the offset
0041148A  mov         eax,dword ptr [ebp-100h] 
00411490  mov         dword ptr [eMemFunc],eax 
00411493  mov         ecx,dword ptr [ebp-0FCh] 
00411499  mov         dword ptr [ebp-2Ch],ecx 
    (e.*eMemFunc)();
0041149C  mov         eax,dword ptr [ebp-2Ch] 
0041149F  lea         ecx,e[eax] 
004114A3  mov         esi,esp 
004114A5  call        dword ptr [eMemFunc]                       

本文依赖的平台为VS2008

阅读(1003) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~