难度:
文前说明:下面涉及到的内容讨论了在GCC 3.2和MS 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-Byte。MEM_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)();
其中调整量分别是4和8其本质是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()定义为虚函数。然后将上面的调整量4和8分别设定为sizeof(Base1)和sizeof(Base1)+sizeof(Base2),最后编译运行。程序会在输出1和2之后被中断。而输出3时却出错,为什么呢?
我们先来了解一下这时Derived的对象模型
其中多了两个vptr,vptr1是由Base1部分和Derived派生出来的这部分使用,vptr2是由Base2部分使用。
mem_ptr = &Derived::fun2; 打算取Derived::fun2的地址(注:由于Base2::fun2是虚函数,它的实际地址只能在运行期才能决定。所以这里用了“打算”二字)。有一点我们可以肯定Base2::fun2被安插在Base2的vtable中的第一个位置。
*(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,所以导致寻找了一个错误的地址而误认为是Base1的vtbl,故发生访问错误。而此时,我们可以为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