混沌 IN C++::Pointers-to-Member functions 解迷

来源:互联网 发布:js 获取classname 编辑:程序博客网 时间:2024/05/08 23:32

难度:

文前说明:下面涉及到的内容讨论了在GCC 3.2MS Visual C++6/.NET中,指向成员函数的指针的实现。如果您将本文读完,别忘了文章最后的一点说明。

以前有过将指向成员函数的指针转换成一个long而被编译器拒绝的经历吗?这里将说出真相。先来一段颇为“神奇”的代码

struct Base1

{

   int i;

   Base1():i(1){}

   void fun1(){   cout<<i<<endl; }

};

 

struct Base2

{

   int i;

   Base2():i(2){}

   void fun2(){   cout<<i<<endl; }

};

 

struct Derived: public Base1, public Base2

{

   int i;

   Derived():i(3){}

   void fun3(){   cout<<i<<endl;}

};

 

typedef void (Derived::*MEM_PTR)();

 

int main(){

   MEM_PTR mem_ptr = &Derived::fun2;

   Derived d;

   *(reinterpret_cast<int*>(&mem_ptr) + 1) = 0;

   (d.*mem_ptr)();

   *(reinterpret_cast<int*>(&mem_ptr) + 1) = 4;

   (d.*mem_ptr)();

   *(reinterpret_cast<int*>(&mem_ptr) + 1) = 8;

   (d.*mem_ptr)();

}

 

程序输出是多少呢?

 

我们来剖析一下这个Derived


Derived
this指针,存在两个情况

1、指向Base1部分

当发生d.fun1()d.fun3()这两个调用时,这两个成员函数得到的this指针都是指向Base1部分的。

2、指向Base2的部分

当发生d.fun2()的调用时,这个成员函数得到的this指针是指向Base2部分的。

从上面这两种情况可以看出,在对多重继承的对象调用成员函数时,会对this指针进行调整。d.fun1()/d.fun2()/d.fun3()在编译时,对于对象d和这三个成员函数来说有足够的类型信息,编译器会自动对this指针进行调整。那么,如果对成员函数取地址,在进行(obj.*mem_ptr)()(ptr->*mem_ptr)()调用时,编译器无法完全知道mem_ptr是指向哪个成员函数,所以编译器无法对这类的调用进行直接调整,而是放在运行期,根据环境进行调整。那么在运行期,这调整的依据是什么呢?

cout<<sizeof(MEM_PTR)<<endl;  //MEM_PTR是上面代码中的typedef-name

发现了吗?MEM_PTR这个指向成员函数的指针的大小是8-Byte,而不是我们常说的指针大小是4-ByteMEM_PTR的前4-Byte就是函数的地址,而后4-Byte就是需要调整的量。我们可以把通过指向成员函数的指针调用模型看作下面这样

((对象地址+调整量).*函数地址)(); ((对象地址+调整量)->*函数地址)();

在上面的代码中*(reinterpret_cast<int*>(&mem_ptr) + 1)其实就代表了后4-Byte的内存,即调整量。

mem_ptr = &Derived::fun2; mem_ptr 指向的是Derived::fun2

*(reinterpret_cast<int*>(&mem_ptr) + 1) = 0;  把调整量设定为0

(d.*mem_ptr)(); 伪码:((&d - 0).*mem_ptr)(); this指针未改变,所以fun2中访问的i其实是Base1::i

*(reinterpret_cast<int*>(&mem_ptr) + 1) = 4;  把调整量设定为4

(d.*mem_ptr)(); 伪码:((&d - 4).*mem_ptr)(); this指针改变了,所以fun2中访问的i其实是Base2::i

*(reinterpret_cast<int*>(&mem_ptr) + 1) = 8;

 (d.*mem_ptr)();

其中调整量分别是48其本质是sizeof(Base1)sizeof(Base1)+sizeof(Base2)

 

现在我们再来一段“神奇”的代码。

把上面代码的每个成员函数里的cout<<i<<endl;改为cout<<i<<’/t’<<this<<endl; ,然后再在main()的最后面加上d.fun1(); d.fun2(); d.fun3(); 最后编译运行,会得到两组输出,但是在3 的那一组,我们会发现两个输出的this指针不同,为什么呢?也许你已经想到。线索就在上面的文字里。

 

当成员函数被定义为virtual这个世界会变成怎么样呢?

将最先那段颇为“神奇”的代码中的Base1::fun1()Base2::fun2()定义为虚函数。然后将上面的调整量48分别设定为sizeof(Base1)sizeof(Base1)+sizeof(Base2),最后编译运行。程序会在输出12之后被中断。而输出3时却出错,为什么呢?

我们先来了解一下这时Derived的对象模型


其中多了两个vptrvptr1是由Base1部分和Derived派生出来的这部分使用,vptr2是由Base2部分使用。

mem_ptr = &Derived::fun2; 打算取Derived::fun2的地址(:由于Base2::fun2是虚函数,它的实际地址只能在运行期才能决定。所以这里用了“打算”二字)。有一点我们可以肯定Base2::fun2被安插在Base2vtable中的第一个位置。

*(reinterpret_cast<int*>(&mem_ptr) + 1) = 0;

(d.*mem_ptr)(); 由调整量确定了this指针指向的是Base1部分,然后通过vptr1试图获得第一个vtbl中的第一个虚函数地址,所以,事实上得到的是Base1::fun1的地址,Base1的指针,调用Base1::fun1,固然不会出错,所以输出为1

*(reinterpret_cast<int*>(&mem_ptr) + 1) = sizeof(Base1);

(d.*mem_ptr)(); 和上面同理,由调整量确定了this指向Base2部分,由vptr2得到Base2::fun2地址。所以输出为2

*(reinterpret_cast<int*>(&mem_ptr) + 1) = sizeof(Base1) + sizeof(Base2);

(d.*mem_ptr)();

为什么最后一个会出错呢?注意一个特点,this指针被调整后,会访问调整后的this指针所指向的vptr。而对于class Derived而言,我们可以通过上面的对象模型得知Derived派生出来的部分并没有安插vptr,而是与Base1同用的一个vptr1,所以,由于在被调整的this所指的位置并不存在(正确的)vptr,所以导致寻找了一个错误的地址而误认为是Base1vtbl,故发生访问错误。而此时,我们可以为Derived手动安插一个数据成员来模拟一个vptr来达到原来的目的。

struct Derived: public Base1, public Base2

{

   int vptr;  //安插一个数据成员

int i;

   Derived():i(3)

{

   vptr = *(reinterpret_cast<int*>(this));  //模拟一个vptr

}

   void fun3(){   cout<<i<<endl;}

};

其余代码不变,然后编译运行。现在程序正确了,访问的是fun1(),但是this指针却不是指向的Base1的部分,换句话说,虽然我们成功了,但是却避免不了这样危险的代码。

 

所以,如果我们试图通过某些非语言提供的机制对 指向成员函数的指针 进行转换,是非常危险的。

 

最后我们再做一个实验。还是用第一个“神奇”的代码。在代码中加入

struct Base3{};

struct Derived2:public Base3, public Derived{};

 

然后将main中的代码改为

int main()

{

void (Base3::*pfunc1)();

void (Derived2::*pfunc2)();

pfunc2 = pfunc1;

pfunc1 = pfunc2;

}

 

试想一下为什么pfunc2 = pfunc1; 可以通过编译,而pfunc1 = pfunc2;却不能?

 

最后的一点说明:GCC MS Visual C++ 对实现指向成员函数的指针的区别。MSVC中,单继承中的指向成员函数的指针的大小仍然是4-Byte,也就是说,上面pfunc1的大小是4-Byte,而只有在多重继承中的指向成员函数的指针的大小是8-Byte,也就是说pfunc2的大小是8-Byte。而GCC中,都是8-Byte,也就是在单继承中,它的调整量是0

 

关于上面提到的虚函数地址的获得,可以参考

http://blog.csdn.net/jinhao/archive/2004/01/17/4798.aspx

http://blog.csdn.net/jinhao/archive/2004/01/17/4799.aspx

 

//THE END