首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > C++ >

:C++类中的函数指针数组异常

2012-04-10 
求救:C++类中的函数指针数组错误想在类中建立一个函数指针数组(公有的),用它来索引一系列成员函数(私有的)

求救:C++类中的函数指针数组错误
想在类中建立一个函数指针数组(公有的),用它来索引一系列成员函数(私有的)
比如:
class tcmd
{
public:
tcmd()
{
cmd[0] = cmd_A;
}
void (*cmd[10])();
private:
void cmd_A() {}
};

但有问题: “error C2440: '=' : cannot convert from 'void (__thiscall Stock::*)(void)' to 'void (__cdecl *)(void)'
  There is no context in which this conversion is possible”

不知犯了什么错误,求修正

如果想要实现该怎么弄 请教各位大神

[解决办法]

4.1 Microsoft的实现
4.1.1 内部表示
Microsoft VC的实现采用的是Microsoft一贯使用的Thunk技术(不知道这个名字怎么来的,不过有趣的是把它反过来拼写就变成了大牛Knuth的名字,呵呵)。

对于Mircosoft来说,成员函数指针实际上分两种,一种需要调节this指针,一种不需要调节this指针。

先分清楚那些情况下成员函数指针需要调整this指针,那些情况下不需要。回忆上一节讨论的c++对象内存布局的说明,我们可以得出结论如下:

如果一个类对象obj含有一些子对象subobj,这些子对象的首地址&subobj和对象自己的首地址&obj不等的话,就有可能需要调整this指针。因为我们有可能把subobj的函数当成obj自己的函数来使用。

根据这个原则,可以知道下列情况不需要调整this指针:

继承树最顶层的类。

单继承,若所有类都不含有虚拟函数,那么该继承树上所有类都不需要调整this指针。

单继承,若最顶层的类含有虚函数,那么该继承树上所有类都不需要调整this指针。

下列情况可能进行this指针调整:

多继承

单继承,最顶的base class不含virtual function,但继承类含虚函数。那么这些继承类可能需要进行this指针调整。

Microsoft把这两种情况分得很清楚。所以成员函数的内部表示大致分下面两种:

struct pmf_type1{

void* vcall_addr;

};



struct pmf_type2{

void* vcall_addr;

int delta; //调整this指针用

};

这两种表示导致成员函数指针的大小可能不一样,pmf_type1大小为4,pmf_type2大小为8。有兴趣的话可以写一段代码测试一下。

4.1.2 Vcall_addr实现
上面两个结构中出现了vcall_addr, 它就是Microsoft 的Thunk技术核心所在。简单的说,vcall_addr是一个指针,这个指针隐藏了它所指的函数是虚拟函数还是普通函数的区别。事实上,若它所指的成员函数是一个普通成员函数,那么这个地址也就是这个成员函数的函数地址。若是虚拟成员函数,那么这个指针指向一小段代码,这段代码会根据this指针和虚函数索引值寻找出真正的函数地址,然后跳转(注意是跳转jmp,而不是函数调用call)到真实的函数地址处执行。

看一个例子。

//源代码

class C

{

public:

int nv_fun1(int) {return 0;}

virtual int v_fun(int) {return 0;}

virtual int v_fun_2(int) {return 0;}

};

void foo(C *c)

{

int (C::*pmf)(int);

pmf = &C::nv_fun1;

(c->*pmf)(0x12345678);

pmf = &C::v_fun;

(c->*pmf)(0x87654321);

pmf = &C::v_fun_2;

(c->*pmf)(0x87654321);

}

; foo的汇编代码,release版本,部分地方进行了优化

:00401000 56 push esi

:00401001 8B742408 mov esi, dword ptr [esp+08]

; pmf = &C::nv_fun1;

; (c->*pmf)(0x12345678);

:00401005 6878563412 push 12345678

:0040100A 8BCE mov ecx, esi ;this

:0040100C E81F000000 call 00401030

; pmf = &C::v_fun;

; (c->*pmf)(0x87654321);

:00401011 6821436587 push 87654321

:00401016 8BCE mov ecx, esi ;this

:00401018 E803070000 call 00401720

; pmf = &C::v_fun_2;

; (c->*pmf)(0x87654321);

:0040101D 6821436587 push 87654321

:00401022 8BCE mov ecx, esi ;this

:00401024 E807070000 call 00401730

:00401029 5E pop esi

:0040102A C3 ret

:00401030 33C0 ; 函数实现 xor eax, eax

:00401032 C20400 ret 0004

:00401720 8B01 ; vcall mov eax, dword ptr [ecx]

:00401722 FF20 jmp dword ptr [eax]

:00401730 8B01 ; vcall mov eax, dword ptr [ecx]

:00401732 FF6004 jmp [eax+04]

从上面的汇编代码可以看出vcall_addr的用法。00401030, 00401720, 00401730都是vcall_addr的值,其实也就是pmf的值。在调用的地方,我们不能分别出是不是虚函数,所看到的都是一个函数地址。但是在vcall_addr被当成函数地址调用后,进入vcall_addr,就有区别了。00401720, 00401730是两个虚函数的vcall,他们都是先根据this指针,计算出函数地址,然后jmp到真正的函数地址。00401030是C::nv_fun1的真实地址。



Microsoft的这种实现需要对一个类的每个用到了的虚函数,都分别产生这样的一段代码。这就像一个template函数:

template <int index>

void vcall(void* this)

{

jmp this->vptr[index]; //pseudo asm code

}

每种不同的index都要产生一个实例。

Microsoft就是采用这样的方式实现了虚成员函数指针的调用。

4.1.3 This指针调整
不过还有一个this调整的问题,我们还没有解决。上面的例子为了简化,我们故意避开了this指针调整。不过有了上面的基础,我们再讨论this指针调整就容易了。

首先我们需要构造一个需要进行this指针调整的情况。回忆这节开头,我们讨论了哪些情况下需要进行this指针调整。我们用一个单继承的例子来进行说明。这次我们避开virtual/non-virtual function的问题暂不考虑。

class B {

public:

B():m_b(0x13572468){}

int b_fun(int) {

std::cout<<'B'<<std::endl;

return 0;

}

private:

int m_b;

};

class D : public B {

public:

D():m_d(0x24681357){}

virtual int foo(int) {

std::cout<<'D'<<std::endl;

return 0;

}

private:

int m_d;

};
// 注意这个例子中virtual的使用


void test_this_adjust(D *pd, int (D::*pmf)(int))

{

(pd->*pmf)(0x12345678);

}


:00401000 mov eax, dword ptr [esp+04] ; this入参

:00401004 mov ecx, dword ptr [esp+0C] ; delta入参

:00401008 push 12345678 ;参数入栈

:0040100D add ecx, eax ; this = ecx= this+delta

:0040100F call [esp+0C] ; vcall_addr入参

:00401013 ret

void test_main(D *pd)

{

test_this_adjust(pd, &D::foo);

test_this_adjust(pd, &B::b_fun);

}


; test_this_adjust(pd, &D::foo);

:00401020 xor ecx, ecx

:00401022 push esi

:00401023 mov esi, dword ptr [esp+08] ; pd, this指针

:00401027 mov eax, 004016A0 ; D::foo vcall地址

:0040102C push ecx ; push delat = 0, ecx=0

:0040102D push eax ; push vcall_addr

:0040102E push esi ; push this

:0040102F call 00401000 ; call test_this_adjust

; test_this_adjust(pd, &B::b_fun);

:00401034 mov ecx, 00000004 ;和上面的调用不同了

:00401039 mov eax, 00401050 ; B::b_fun地址

:0040103E push ecx ; push delta = 4, exc=4

:0040103F push eax ; push vcall_addr, B::b_fun地址

:00401040 push esi ; push this

:00401041 call 00401000 ; call test_this_adjust

:00401046 add esp, 00000018

:00401049 pop esi

:0040104A ret

注意这里和上面一个例子的区别:

在调用test_this_adjust(pd, &D::foo)的时候,实际上传入了3个参数,调用相当于

test_this_adjust(pd, vcall_address_of_foo, delta(=0));

调用test_this_adjust(pd, &B::b_fun)的时候,也是3个参数

test_this_adjust(pd, vcall_address_of_b_fun, delta(=4));

两个调用有个明显的不同,就是delta的值。这个delta,为我们后来调整this指针提供了帮助。

再看看test_this_adjust函数的汇编代码,和上一个例子的不同,也就是多了一句代码:

:0040100D add ecx, eax ; this = ecx= this+delta

这就是对this指针作必要的调整。

4.1.4 结论
Microsoft根据情况选用下面的结构表示成员函数指针,使用Thunk技术(vcall_addr)实现虚拟函数/非虚拟函数的自适应,在必要的时候进行this指针调整(使用delta)。



struct pmf_type1{

void* vcall_addr;

};



struct pmf_type2{

void* vcall_addr;

int delta; //调整this指针用

};

4.2 GCC的实现
GCC对于成员函数指针的实现和Microsoft的方式有很大的不同。

4.2.1 内部表示
GCC对于成员函数指针统一使用类似下面的结构进行表示:

struct

{

void* __pfn; //函数地址,或者是虚拟函数的index

long __delta; // offset, 用来进行this指针调整

};

4.2.2 实现机制
先来看看GCC是如何区分普通成员函数和虚拟成员函数的。

不管是普通成员函数,还是虚拟成员函数,信息都记录在__pfn里面。这里有个小小的技巧。我们知道一般来说因为对齐的关系,函数地址都至少是4字节对齐的。这就意味这一个函数的地址,最低位两个bit总是0。(就算没有这个对齐限制,编译器也可以这样实现。) GCC充分利用了这两个bit。如果是普通的函数,__pfn记录该函数的真实地址,最低位两个bit就是全0,如果是虚拟成员函数,最后两个bit不是0,剩下的30bit就是虚拟成员函数在函数表中的索引值。

使用的时候,GCC先取出最低位两个bit看看是不是0,若是0就拿这个地址直接进行函数调用。若不是0,就取出前面30位包含的虚拟函数索引,通过计算得到真正的函数地址,再进行函数调用。



GCC和Microsoft对这个问题最大的不同就是GCC总是动态计算出函数地址,而且每次调用都要判断是否为虚拟函数,开销自然要比Microsoft的实现要大一些。这也差不多可以算成一种时间换空间的做法。

在this指针调整方面,GCC和Mircrosoft的做法是一样的。不过GCC在任何情况下都会带上__delta这个变量,如果不需要调整,__delta=0。

这样GCC的实现比起Microsoft来说要稍简单一些。在所有场合其实现方式都是一样的。而且这样的实现也带来多一些灵活性。这一点下面“陷阱”一节再进行说明。

[解决办法]

C/C++ code
#include <stdio.h>#include <stdlib.h>class cmd {public:        cmd() {                table[0] = &cmd::func;        }        void func() {                printf("ca\n");        }        typedef void (cmd::*pfunc)();        pfunc table[10];};int main(int argc, char* argv[]) {        cmd c;        (c.*c.table[0])();        return 0;} 

热点排行