参考网址:
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{...}
float Point3d::manitude3d()const{...}
这两种方式是等价的,编译器在内部将后者转化为前者,因此
obj.magintude();变成了 maginitude_7Point3dFv(&obj);
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;
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
__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()
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++程序设计语言》
因为时间和水平的问题,本人的文章可能会有误,如有疑问和指正请和我联系:)