辣子鸡丁::天水涧

在天堂与地狱之间有个地方,叫作尘世

原创 混沌 IN C++::Pointers-to-Member functions 解迷收藏

新一篇: 磨刀霍霍向STL::distance | 旧一篇: 混沌 IN C++::Exception思维

难度:

文前说明:下面涉及到的内容讨论了在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

发表于 @ 2004年07月23日 02:25:00|评论(loading...)|编辑

新一篇: 磨刀霍霍向STL::distance | 旧一篇: 混沌 IN C++::Exception思维

评论

#狂龙剑客 发表于2004-07-23 11:17:00  IP: 220.114.115.*
如上题
#Antijpn 发表于2004-07-23 03:30:00  IP: 202.102.83.*
占个位子,头坑
#abc 发表于2004-07-23 18:13:00  IP: 218.1.23.*
错误: 当发生d.fun1()或d.fun3()这两个调用时,这两个成员函数得到的this指针都是指向Base1部分的。

尝试在fun1, fun2, fun3中加入cout << (long)this << endl;
可以看到打印出不同地址。
#辣子鸡丁 发表于2004-07-23 19:19:00  IP: 222.183.26.*
>>错误: 当发生d.fun1()或d.fun3()这两个调用时,这两个成员函数得到的this指针都是指向Base1部分的。
----------------------------------------------
你确认 fun1和fun3输出的this 不是同一个地址吗? 也许你用的编译器不是我说明的编译器。
如果fun1和fun3的this地址不一样,那么说明这个编译器所做的事就要多一些。
1、传入的this指针会被调整
2、如果fun3里,访问了基类的protected 或 public 数据成员,那么又将做一些偏移来访问数据。

而fun1和fun3的this指针一致,那么编译器只需要对访问派生类部分的数据成员 做偏移来访问
#辣子鸡丁 发表于2004-07-23 19:28:00  IP: 222.183.26.*
to 狂龙剑客
>>为什么所有的类都是struct不是class
------------------------------------------
在C++中的struct,并不是与C中的struct等价,它其实就是class
我用struct是图方便,下面这个连接中说明了C++中的struct
http://community.csdn.net/Expert/topic/3198/3198572.xml?temp=.2388422
#me 发表于2004-12-02 17:24:00  IP: 219.130.46.*
You'd better to product a new language..
#猪头流氓 发表于2004-12-14 18:08:00  IP: 222.35.8.*
#include <iostream>
using namespace std;
struct Base

{
int i;
Base():i(1){}
void fun1(){ cout<<"BASE:"<<i<<endl; }
};

struct Derived: public Base
{

int j;
Derived():j(2){}
void fun2(){ cout<<"DERIVED:"<<j<<endl;}
};

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

int main()
{
MEM_PTR mem_b = &Base::fun1;
MEM_PTR2 mem_d = &Derived::fun1;

Derived d;
Base b;
//输出两个函数指针的地址
cout<<*(reinterpret_cast<int*>(&mem_b))<<endl;
cout<<*(reinterpret_cast<int*>(&mem_b)+1)<<endl;

cout<<*(reinterpret_cast<int*>(&mem_d))<<endl;
cout<<*(reinterpret_cast<int*>(&mem_d)+1)<<endl;

*(reinterpret_cast<int*>(&mem_b) + 1) = 0;
(b.*mem_b)();

*(reinterpret_cast<int*>(&mem_d) + 1) = 0;
(d.*mem_d)();

*(reinterpret_cast<int*>(&mem_d) + 1) = 4;
(d.*mem_d)();

system("pause");
}

看结果后面四个字节记录的类里面的data member的偏移量
#thinkinnight 发表于2006-04-07 14:21:00  IP: 221.226.124.*
单继承this应该是没有偏移的,不过现在有个疑问:

//http://blog.csdn.net/Jinhao/archive/2004/07/23/49366.aspx#FeedBack
#include <iostream>
using namespace std;

struct Base1
{
int i;
Base1():i(1){}
void fun1(){ cout<<"In fun1"<<endl; cout<<i<<'\t'<<this<<endl; }
};

struct Base2
{
int i;
Base2():i(2){}
void fun2(){ cout<<"In fun2"<<endl; cout<<i<<'\t'<<this<<endl; }
};

struct Derived: public Base1, public Base2
{
int i;
Derived():i(3){}
void fun3(){ cout<<"In fun3"<<endl; cout<<this->i<<'\t'<<this<<endl;}
};

typedef void (Base1::*B1MEM_PTR)();
typedef void (Base2::*B2MEM_PTR)();
typedef void (Derived::*MEM_PTR)();

int main(){
B1MEM_PTR b1mem_ptr1 = &Base1::fun1;

B2MEM_PTR b2mem_ptr2 = &Base2::fun2;

MEM_PTR mem_ptr1 = &Derived::fun1;
MEM_PTR mem_ptr2 = &Derived::fun2;
MEM_PTR mem_ptr3 = &Derived::fun3;
Derived d;

cout<<"得到Base1对应的function地址"<<endl;
cout<<"b1fun1: "<<(reinterpret_cast<int*>(&b1mem_ptr1))<<endl;
cout<<"b1fun1: "<<(reinterpret_cast<int*>(&b1mem_ptr1)+1)<<endl;

co
#thinkinnight 发表于2006-04-10 10:36:00  IP: 221.226.124.*
试验环境为VC.NET,只能说明该编译器的实现。

对于“猪头流氓”的代码,打印出
sizeof(MEM_PTR)和sizeof(MEM_PTR2)的结果是4和4,说明this在单继承中是不需要考虑偏移的,所以下面对偏移的赋值什么的都是不对的。

而对应JinHao的代码则是有偏移的,
typedef void (Base1::*B1MEM_PTR)();
typedef void (Base2::*B2MEM_PTR)();
typedef void (Derived::*MEM_PTR)();
打印出来的
sizeof(B1MEM_PTR) --- 4
sizeof(B2MEM_PTR) --- 4
sizeof(MEM_PTR) --- 8
说明多继承是有一个计算偏移量的。放在MEM_PTR中。
而对于上面的我提交的代码,其执行的结果是(我去除了一些东西,加上了d这个实例的地址):
行号 结果内容
1 d的地址:0012FE84
2 Function 2 call compare:
3 In fun1
4 1 0012FE84
5 In fun2
6 1 0012FE84

7 In fun2
8 2 0012FE88
9 In fun2
10 2 0012FE88

11 In fun3
12 3 0012FE84
13 In fun2
14 3 0012FE8C

15 Function 3 call compare:
16 In fun3
17 3 0012FE84
18 In fun3
19 3 0012FE84

20 In fun2
21 2 0012FE88
22 In fun3
23 2 0012FE80

24 In fun1
25 1 0012FE84
26 In fun3
27 1 0012FE7C

从结果可以看出,fun3中调用的this指针地址是在d的地址的上方(第23和27行),
d的地址为0012FE84(第1行),而fun3调用的分
#thinkinnight 发表于2006-04-10 10:38:00  IP: 221.226.124.*
似乎图显示有些问题,在编辑框中和实际显示出来有些不同,我在notpad中重新编辑了一下,希望这次可以显示正确。
____________
0012FE84 d---> | Base1::i |
|____________|
0012FE88 ---> | Base2::i |
|____________|
0012FE8C ---> | Derived::i |
|____________|


____________
0012FE7C ---> | Base1::i |
|____________|
0012FE80 ---> | Base2::i |
|____________|
0012FE84 d---> | Derived::i |
|____________|
#thinkinnight 发表于2006-04-10 10:45:00  IP: 221.226.124.*
不好意思,刚看到您的短消息,似乎了解了一点。你的说法应该是有道理的。
发表评论  


当前用户设置只有注册用户才能发表评论。如果你没有登录,请点击登录
Csdn Blog version 3.1a
Copyright © 辣子鸡丁