转载

深入探讨this指针

深入探讨 this 指针

为了写这篇文章,准备了好长时间,翻遍了箱底的书籍。但是现在还是不敢放开手来写,战战兢兢。不是担心自己写错,而是唯恐自己错误误导别人。同时也希望这篇文章能给你一点收获。既然是深入探讨 this 指针,所以建议初学者,最好具有一定编译基础,调试基础。如果大家认为这片文章有不满的地方 , 就给我发信批评一下,以便及时修正。

关于 this 指针的描述我们一般从语言层次上讲;

this 指针作为一个隐含参数传递给非静态成员函数,用以指向该成员函数所属类所定义的对象。当不同的对象调用同一个类的成员函数代码时,编译器会依据该成员函数的 this 指针所指向的不同对象来确定应该引用哪个对象的数据成员。简单例子

我们定义一个简单 stack

// 定义 stack

class Stack

{

public :

     Stack(); // 构造函数

     ~Stack(); // 析构函数

public :

     void push( char c); // 压栈函数

private :

     char *top; // 栈顶元素

char *max; //

};

// 压栈函数

void Stack::push( char c)

{

if (top > max)

{

ERROR;

}

*top++ = c;

}

// 定义公共函数,操作栈对象中的 push 函数

void FunStack(Stack *p)

{

p->push('c');

}

上面的代码我们加入 this 概念,以 C 代码形式显示 ( 你可以理解编译 C++C 代码后, Cfront 开始就是这么做的 )

// 用普通 C 描述类成员函数

void Stack__push( this ,c); // 普通 C 代码

{

if ( this ->top > this ->max)

{

ERROR;

}

*( this ->top)++ = c;

}

void FunStack(p) // Stack *p;

{

Stack__push(p,'c');

}

C++ this 指针是从 Simula (只是听说没有使用过)里的 THIS 引用的翻版,有时候有人会问,为什么 this 是指针而不是一个引用?为什么叫 this 而不是叫 selfsmalltalk )?第一个问题是,当 this 引入带类的 C 时,在那时的是 C ++中还没有引用机制,所以只能是 this 指针而不是引用了。第二个问题,更简单了,就是因为 this 是从 simula 来,而不是从 smalltalk 来。

    上面是简单的讨论,我们将逐步深入讨论 this

我们通过 this 访问对象(已经成惯例了)中函数和变量时一般这样使用

    this->top;// 访问变量

    this->push();// 访问函数

    (*this).top;// 访问变量

    (*this).push();// 访问函数

通过上面例子,我们从语言层次上说 this 是一个指针(也许你说 this 本来就是一个指针,就叫 this 指针,不要着急听我慢慢说来)。那么 this 是一个什么样子的指针,比如我们最常见的指针有。

int *p;

Const int *p;

int * const p;

那么 this 指针是不是其中一种?下面我们分别验证。

    我们定义类 , 作为验证对象

    class A

{

public :

     int iData; // 简单期间我们定义为 int

     mutable int iData2; // mutable

int Fun1(){ return ++iData;}; // 普通函数

     int Fun2() const { return ++iData;}; // const 的函数

};

上面的 函数可以正确执行。

上面 函数,不能通过编译,我们知道在 const 函数中,不允许修改类中变量。那么最终原因是什么?其实在上面的例子中,我们用 C 实现

int A_Fun2(const A* this);

const 函数本质是 const this 的原因,所以不允许修改 iData 值。

至少现在我们可以确定 this 指针,不是一个 const 常量指针。因为如果 this 是常量指针,我们就不能修改类中变量的值了。捎带我们提一下 C ++中关键字 mutable ,如上定义的 mutable int iData2; // mutable 变量 ,这样我们就可以在 const 函数中修改 iData2 的值。其实这时的 mutablepublicprivateprotected 是相同的,这些关键字只是在编译时刻有用,编译后变量类型是没有区别的。更深一步说,强制类型转换也是对编译器来说,是通过编译器编译过程中判断类型转换的正误。

    那么 this 对象是否是 A *const this 的值哪?首先我们先看一个例子

static int iTest = 1;

class A

{

public :

     int iData; // 简单期间我们定义为 int

mutable int iData2; // mutable

int Fun1()

{

int iTemp = 4;

return ++iData;

     }; // 普通函数

     int Fun2() const { return iData;}; // const 的函数

};

int _tmain( int argc, _TCHAR* argv[])

{

A a;

static int iTest1 = 2;

a.Fun1();

static int iTest2 = 3;

system("pause");

return 0;

}

我们通过上面的例子查看 this 的地址,我们定义 static 对象的目的就是为了用 this 指针的地址和 static 变量的地址进行对比,看一看 this 指针到底分配到哪里?

    注意我们在这里不能直接使用 &this 获得 this 的指针,如果我们这样定义会提示

Error C2102 & 要求一个 L

    通过上面至少我们知道, this 不是一个个人定义的变量,只是在运行时刻有效。所以这时如果直接对 this 取地址,在编译时刻无法通过,提示如上错误。

    既然我们在程序中无法通过 &this 取得 this 的地址。那么我们有什么办法取得 this 的地址?我们上面已经提到 this 是在运行时刻有效,我们就以据这点查找 this 的地址。

    为了在取得 this 的地址,我们使用 VC7.0 下的命令窗口,在命令窗口中我们使用命令 eval ,通过这个命令我们可以取得 this 的地址。我们还是在上面的程序中设置断点

深入探讨this指针

debug 下,我们运行上面的程序,并进入断点后,进行取址操作。

>eval &iTest

0x0044afa0 iTest

>eval &iTest1

0x0044afa4 iTest1

>eval &this// 注意只有我们进入 Fun1 ()函数体内才能取得 &this 的值

0x0012fdf0 " _"

>eval &iTest2

0x0044afa8 iTest2

通过对比我们可以看出 static 变量 iTest,iTest1,iTest2 存放在全局变量区域,而 &this 0x0012fdf0 )的地址比 &iTest 0x0044afa0 )地址还要底,而 static 变量存放在单独全局

区域,并且这个区域是从底地址到高地址递增的。所以通过上面的对比至少我们可以肯定一点 this 指针的创建要比 static 变量(或者全局变量)早。那么更比创建 A a ;对象时调用 A 的构造函数早,只是创建 a 对象后, this 指向 a 对象;

当我们创建两个 A 类对象时,会发现 this 指针的地址是相同的,但是 this 指针指向对象不同。当然不同了,如果相同。 A ab ;那么 ab 对象也就相同了,这种方式肯定是不对的。结论就是同一个类创建多个对象时,多个对象的 this 指针是同一个指针。也就是说在单进程单线程中 this 对象在放入 CPU 寄存器中时都是同一个地址,只是指向不同的对象而已。上面的测试是在 DEBUG 状态下的测试结果。

那么在 Release 是什么样?要多亏 VC7.0 支持 Release 下的断点,我们在 Release 下,启动调试。这时需要在 Release 状态下设置,优化状态为 禁用 (/Od)

>eval &this CXX0069: 错误 : 变量需要堆栈帧

>eval this CXX0069: 错误 : 变量需要堆栈帧

>eval *this CXX0069: 错误 : 变量需要堆栈帧

    Release 状态下 &thisthis*this 不存在了,提示是变量需要堆栈帧,说明此时的 this 指针不存在了。难到 this 指针只是在 debug 模式下有,在 Release 模式下没有?而 C ++语言特性中并没有说 this 指针在调试状态下有而在 Release 模式下没有啊?只是强调 this 指针作为一种隐含参数传递。也就是在正确(请这样理解)的程序中 this 应该是不存在的,至少可以肯定的是说在内存中不存在 this 指针。

    我们使用 C ++的时候知道有一种变量定义方式,也不存放到内存,而是直接放到寄存器中。我想你已经猜到了就是 register 类型变量,下面我们测试 register 类型变量是否和 this 指针是一样的结果。

    在程序中定义: register int iRegData;

Debug

>eval iRegData

5

>eval &iRegData

0x0012fec4// 注意这个地址,看看是否和 >eval &this// 注意只有我们进入 Fun1 ()函数体内才能取得 &this 的值 0x0012fdf0 "_" 在地址上很接近啊!一个是 0x0012fec4 ,另一个是 0x0012fdf0

    Release

>eval iRegData

5

>eval &iRegData

0x0012fee0

通过上可以知道在 debugRelease 模式下 iRegData 都没有直接放入寄存器,而是在内存中开辟了内存空间,至于如何可以在运行时候看出 register 变量是放到寄存器,而不是内存中,我还不得而知,所以哪位高人知道,麻烦告诉我一声。看来 this 指针也不是 register 类型的,或者我现在的能力还不能确定 thisregister 。后来才知道 register 对编译器只是一个提示,编译器可以执行也可以不执行,就像 inline 一样。但是至少我们可以使用 __inline 宏,可以确保函数被 inline ,但是 register ?有没有这种策略,我现在还不得而知。

补充:定义变量类型有四中分别是

1 Auto:staticconst 类型变量,比如局部变量, int ichar c 等。都是 auto int iauto char c

2 static :静态变量, static int istatic char c

3 const :常量变量,值不可修改。 Const int istatic char c

4 register :内存变量,编译器把此值直接放入寄存器。 Register int iregister char c

上面讨论我们都是从类中变量进行讨论的,但是无法确定 this 到底是什么?那么我们继续从类中的函数开始讨论 this 。并且我们也将逐渐深入编译状态下。

开始的使用已经举了例子,类内函数在解释函数时,把 this 指针作为函数的第一个参数进行传递。 但是,当高级语言被编译成计算机可以识别的机器码时,有一个问题就凸现出来:在 CPU 中,计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数(你讲看到 this 是一个例外)。也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。为此,计算机提供了一种被称为栈的数据结构来支持参数传递。     栈是一种先进后出的数据结构,栈有一个存储区、一个栈顶指针。栈顶指针指向堆栈中第一个可用的数据项(被称为栈顶)。用户可以在栈顶上方向栈中加入数据,这个操作
被称为压栈 (Push) ,压栈以后,栈顶自动变成新加入数据项的位置,栈顶指针也随之修
改。用户也可以从堆栈中取走栈顶,称为弹出栈 (pop) ,弹出栈后,栈顶下的一个元素变
成栈顶,栈顶指针随之修改。

函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。在参数传递中,有两个很重要的问题必须得到明确说明:当参数个数多于一个时,按照什么顺序把参数压入堆栈函数调用后,由谁来把堆栈恢复原装在高级语言中,通过函数调用约定来说明这两个问题。常见的调用约定有:

stdcall

cdecl

fastcall

thiscall

naked call

原来函数调用约定也有这么多啊,看这都有点晕了呵呵。因为这篇文章讲的是 this 指针,所以在这里我们主要讨论 thiscall

       thiscall 是唯一一个不能明确指明的函数修饰,因为 thiscall 不是关键字(所以不要在 C ++关键字中找了)。它是 C++ 类成员函数缺省的调用约定。由于成员函数调用有一个 this 指针,因此必须特殊处理, thiscall 意味着:参数从右向左入栈 如果参数个数确定, this 指针通过 ecx 传递给被调用者;如果参数个数不确定, this 指针在所有参数压栈后被压入堆栈。对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈为了说明这个调用约定,定义如下类和使用代码:

class A

{

public:

int function1(int a,int b);

int function2(int a,...);//

定义 VA (可变)函数

};

int A::function1 (int a,int b)

{

return a+b;

}

int A::function2(int a,...)

{

va_list ap;

va_start(ap,a);

int i;

int result = 0;

for(i = 0 i < a i ++)

{

result += va_arg(ap,int);

}

return result;

}

void callee()

{

A a;

a.function1 (1,2);

a.function2(3,1,2,3);

}

callee

函数被翻译成汇编后就变成: // 函数 function1 调用

0401C 1D push 2

00401C 1F push 1

00401C 21 lea ecx,[ebp-8]

00401C

24 call function1 //

注意,这里 this 没有被入栈,而是通过 ECX 传递 this 指针

此时寄存器的各值如下

EAX = 00000003 EBX = 7FFDF000 ECX = 0012EE43

EDX = 00000001 ESI = 00000000 EDI = 0012EE48

EIP = 0041707A ESP = 0012ED70 EBP = 0012EE48

EFL = 00000206

察看 this 指针

>eval this

0x0012ee43// 看看这个值是否和 ECX 相同 // 函数 function2 调用

00401C 29 push 3

00401C 2B push 2

00401C 2D push 1

00401C 2F push 3

00401C

31 lea eax,[ebp-8] //

这里引入 this 指针,并把 this 指针放入栈内

EAX = 00000006 EBX = 7FFDF000 ECX = 0012ED70

EDX = 00000006 ESI = 00000000 EDI = 0012EE48

EIP = 0041708E ESP = 0012ED70 EBP = 0012EE48

EFL = 00000212

察看 this 指针

>eval this

0x0012ee43// 看看这个值是否和 ECX 相同

00401C 34 push eax

00401C 35 call function2

00401C 3A

add esp,14h

到现在,我们对 this 得了解还说不上深入了解。简单得说 this 就是指向对象自身的一个指针,讨论这么多其实就是想了解 this 在反编译阶段是如何传递运行得。也许就 this 的了解我们就可以基于以上讨论已经足够了。但是 this 的应用并不简单的就是这些内容,比如在 ATL 中,就有专门函数用来保存回复 this 指针的策略;我们在重载 operator= 也需要通过 this 判断赋值等号两边对象,是否指向同一个对象。

关于指针 :指针和其它变量( intchar 等)一样,在声明后会在内存中申请内存空间,存储在在程序的堆栈上,大小一般都是一个机器字的长度(比如在 32 位机上是 4 个字节)。简单的说指针是指向内存中地址的变量,可以是数据的地址也可以是函数的地址。 一句话: 指针是一种用于储存“另外一个变量的地址”的变量 。或者拆成两句:指针是一个变量,它的值是另外一个变量的地址。

参考资料

孙晓涛等《 Windows 高级编程》西北工业大学出版社( 1997 10 西安)

逸学堂《关于 this 指针的深入探讨》 CSDN

C ++编程思想》

声明:云栖社区站内文章,未经作者本人允许或特别声明,严禁转载,但欢迎分享。

原文  https://yq.aliyun.com/articles/6344
正文到此结束
Loading...