this指针探秘

来源:互联网 发布:origin矩阵画图 编辑:程序博客网 时间:2024/06/06 07:25

参考网址:
http://www.cpper.net.ru/Content.asp?ID=27
http://topic.csdn.net/u/20070205/11/3a054c39-29e5-4b2b-a830-37e0eea8533a.html 

 

深度探索C++对象模型对this的描述是,this是一个函数参数
 
float   manitude3d(const   Point3d   *_this){...}
float   Point3d::manitude3d()const{...}

这两种方式是等价的,编译器在内部将后者转化为前者,因此
obj.magintude();变成了maginitude_7Point3dFv(&obj);
 
如何理解“编译器在内部将后者转化为前者”这一句话呢?我们知道世界上第一个C++编译器是Cfront,Bjarne Stroustrup,Lippman等人开发的,那在当时最流行的编程语言是C,在各种机器各种操作系统下都有C的编译器版本,这样C的程序在当时是最易移植的,因此Bjarne Stroustrup决定Cfront生成等价的C代码以获得可移植性。就是说Cfront读入你编写的.cpp文件经过词法分析等编译过程,结果没有生成.obj文件却生成了一个.c文件,然后再用C编译器对.c文件进行编译链接,最终生成的程序是一个彻头彻尾的C程序(所以Cfront也被称为C 预处理器)
 
这样,编译器在看到成员函数的定义时会直接修改源代码:修改函数名称,在函数的参数里添加这么一个名叫“this”的指针定义,对成员函数的调用也采取类似的方式,在调用语句的参数列表偷偷加上对象的地址,最后保存为.c文件,由C编译器进行最终的编译链接,生成可执行程序,可以看到经过编译器编译之后,C++的成员函数其实也就是一个普通的函数,this和element pop(sqstack *s)中的s也没有什么本质上的区别,就是一个指向数据结构的指针而已,今天的C++编译器已经不再是C预处理器,但是依旧沿用这样的思想
 
Ok,说到这里大家对什么是this指针已经有个感性的理解了吧
第一,this指针为什么没有定义就能直接使用,答案是编译器定义了
第二,this指针是一个函数参数,所以它的使用范围仅在成员函数内部
第三,this指针为什么指向对象?调用时偷偷把对象地址给它传递过去了
 
下面我们将进入汇编层次去看一看成员函数的调用以及this指针到底是如何传递的,恩,需要我们有一点汇编知识,以及通过内嵌的汇编来测试我们的想法
 

x.foo();                                 //VC6.0的反汇编代码
lea   ecx,dword   ptr[ebp-4]             //对象地址存储到ecx中
call   @ILT   +10(X::foo)(0040100f)   //转到0040100f处
0040100f   jmp   X::foo               // jmp跳转的函数真正的地址

void   X::foo(){
//...
mov   dword   ptr[ebp-4],ecx
//将对象地址从ecx中读出,写入到foo的ebp-4处
this->a=2;
mov   eax,dword ptr [ebp-4] //通过this指针为对象赋值时从ebp-4读取对象地址
mov   dword ptr [eax],2 // eax = &x; *(int*)(eax+0)= 2
//...
}
 
由此可见,在VC Debug中对象地址是通过寄存器变量(ecx)传递进成员函数,进入函数后做完一些初始化的工作以后,把对象地址写入到foo的ebp-4处,程序如果要读取对象属性都需要从ebp-4中读出对象地址,了解VC成员函数的调用过程,我们可以大胆做一些测试程序
 
void   X::foo(X *p){
        __asm{
            mov  eax,doword   ptr[ebp+8]
            mov  dword   ptr[ebp-4],eax
            //this = p
      }
      this- >a=10;//p- >a=10;
}//偷梁换柱,this实际已经指向*p ,这时通过this对对象的修改都是针对*p

void   X::foo(){
        X   **pTHIS;
        __asm{
            lea    eax,dword   ptr[ebp-4]
            mov   dword   ptr   [ebp-8],eax
            //pTHIS=&this
      }
      (*pTHIS)- >a=10;//this- >a=10;      
}//pTHIS指向了this因此**pTHIS==*this
 
刚才我们是基于VC Debug下的一个讨论,现在讨论下release下的成员函数调用,在release下,对象地址不再写入ebp-4处,从而使this指针变成一个纯粹的寄存器变量,由于release的汇编代码非常难读,我们只写一个测试程序证明我们的观点:)

void X::foo(){
      X *THIS;
     __asm{
         mov  dword ptr [ebp-8],ecx
     //THIS = this
      }
      THIS->a=2;
}
 
因此在this指针在VC中其实是一个彻底的寄存器的变量,VC的成员函数其实是void  X::foo  ( const  register  X  *this)
 
OK,上面说了那么多就是在证明一点:this指针只是一个普通的指针,被编译器定义的一个函数参数,现在,我们要证明另一点,C++的成员函数被编译之后只是普通的函数,由于VC++的this指针是寄存器变量,不能很清楚的显示这个关系,我们选用Borland C++ Builder 6.0 作为我们测试的工具,BCB依靠栈来传递对象地址
 
x.foo();
lea   eax,[ebp-0x04]
push   eax               //将对象地址压栈
call   X::foo()           //调用X::foo()
 
先介绍一点C++对象模型的知识,我们知道C++的类定义包含虚函数,那么这个类产生的对象都会包含一个虚指针,虚指针会指向一个名叫虚表格的函数指针数组,数组存储的都是虚函数的地址,C++依靠这些来实现多态,而通常虚指针又位于对象的前端(我们很幸运,BCB正是这样)
 
既然函数存在与内存中,那么我们可以依靠虚指针和虚表格的指向关系一路找到虚函数,我们再定义一个函数指针指向虚函数,通过函数指针调用虚函数(并把对象地址传递进去)
 
#pragma   hdrstop
#include   <iostream.h >
class   A{
public:
        virtual   void   foo(){
                this- >x=3;
                cout < < "this- >x=   " < <this- >x < <endl;
        }
        virtual   void   bar(){
                this- >x=4;
                cout < < "this- >x=   " < <this- >x < <endl;
        }
        int   x;
};
void   (*pvFunc)   (A*)   ;
 
int   main(){
 
A   a;
int   *vptr=(int*)&a;                  //vptr指向虚指针
int   *vtbl=(int*)*vptr;                //vtbl指向虚表格
pvFunc =(void (*)(A*))*vtbl;          //指向虚表格第一个单元(foo函数)
(*pvFunc)(&a);                        //通过函数指针执行foo函数
vtbl++;                                //vptr指向虚表格下一单元
pvFunc =(void (*)(A*))*vtbl;          //指向虚表格下一单元(bar函数)
(*pvFunc)(&a);                        //通过函数指针执行bar函数
return   0;
}
 
(*pvFunc)(&a); 你可以看到,经过一些非常规的方法我们居然就可以这样调用虚函数,而且我们确实把对象地址给送入到函数里,可以证实有那么一个参数存在,可以证实成员函数就是一个普通函数。
 
参考书籍:
《C++设计与演化》《深入探索C++对象模型》《C++程序设计语言》
 
因为时间和水平的问题,本人的文章可能会有误,如有疑问和指正请和我联系:)
原创粉丝点击