深入探讨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而不是叫self(smalltalk)?第一个问题是。当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++中keywordmutable,如上定义的mutable int iData2;// mutable变量,这样我们就能够在const函数中改动iData2的值。事实上这时的mutable和public,private,protected是同样的,这些keyword仅仅是在编译时刻实用。编译后变量类型是没有差别的。更深一步说。强制类型转换也是对编译器来说。是通过编译器编译过程中推断类型转换的正误。

那么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的地址。我们还是在上面的程序中设置断点

在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 a。b。那么a,b对象也就同样了,这样的方式肯定是不正确的。

结论就是同一个类创建多个对象时,多个对象的this指针是同一个指针。也就是说在单进程单线程中this对象在放入CPU寄存器中时都是同一个地址。仅仅是指向不同的对象而已。

上面的測试是在DEBUG状态下的測试结果。

那么在Release是什么样?要多亏VC7.0支持Release下的断点,我们在Release下。启动调试。

这时须要在Release状态下设置,优化状态为禁用(/Od)

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

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

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

在Release状态下&this,this。*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

通过上能够知道在debug和Release模式下iRegData都没有直接放入寄存器,而是在内存中开辟了内存空间。至于怎样能够在运行时候看出register变量是放到寄存器,而不是内存中。我还不得而知,所以哪位高人知道,麻烦告诉我一声。

看来this指针也不是register类型的,或者我如今的能力还不能确定this是register。后来才知道register对编译器仅仅是一个提示,编译器能够运行也能够不运行。就像inline一样。

可是至少我们能够使用__inline宏。能够确保函数被inline,可是register?有没有这样的策略。我如今还不得而知。

补充:定义变量类型有四中各自是

1:Auto:非static,const类型变量。比方局部变量。int i;char c等。都是auto int i;auto char c;

2:static:静态变量,static int i。static char c;

3:const:常量变量,值不可改动。Const int i,static char c;

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

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

開始的使用已经举了样例,类内函数在解释函数时,把this指针作为函数的第一个參数进行传递。可是,当高级语言被编译成计算机能够识别的机器码时,有一个问题就凸现出来:在CPU中。计算机没有办法知道一个函数调用须要多少个、什么样的參数,也没有硬件能够保存这些參数(你讲看到this是一个例外)。也就是说,计算机不知道怎么给这个函数传递參数。传递參数的工作必须由函数调用者和函数本身来协调。为此,计算机提供了一种被称为栈的数据结构来支持參数传递。

栈是一种先进后出的数据结构,栈有一个存储区、一个栈顶指针。栈顶指针指向堆栈中第一个可用的数据项(被称为栈顶)。用户能够在栈顶上方向栈中增加数据,这个操作
被称为压栈(Push),压栈以后,栈顶自己主动变成新增加数据项的位置,栈顶指针也随之修
改。用户也能够从堆栈中取走栈顶。称为弹出栈(pop),弹出栈后。栈顶下的一个元素变
成栈顶。栈顶指针随之改动。

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

stdcall
cdecl
fastcall
thiscall
naked call

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

thiscall是唯一一个不能明白指明的函数修饰。由于thiscall不是keyword(所以不要在C++keyword中找了)。它是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调用
0401C1D push 2
00401C1F push 1
00401C21 lea ecx,[ebp-8]
00401C24 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调用
00401C29 push 3
00401C2B push 2
00401C2D push 1
00401C2F push 3
00401C31 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同样
00401C34 push eax
00401C35 call function2
00401C3A add esp,14h

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

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

或者拆成两句:指针是一个变量。它的值是另外一个变量的地址。

 

參考资料

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

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

《C++编程思想》

 

深度this指针的更多相关文章

  1. C语言深度剖析-----指针数组和数组指针的分析

    指针数组和数组指针的分析 数组类型 定义数组类型 数组指针 这个不可能为数组指针,指向数组首元素 例 指针数组 例    main函数的参数 例 小结

  2. leetcode-每个节点的右向指针(填充同一层的兄弟节点)

    给定一个二叉树 struct TreeLinkNode { TreeLinkNode *left; TreeLinkNode *right; TreeLinkNode *next; } 填充它的每个 ...

  3. PCL 点云数据操作 OpenCV遍历数据

    1.对于点云类型实例cloud,对其第i个点进行赋值操作,使用cloud.point[i].x 和 cloud.point[i].y 和cloud.point[i].z 分别对其XYZ坐标赋值. cl ...

  4. 详解SkipList跳跃链表【含代码】

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天继续介绍分布式系统当中常用的数据结构,今天要介绍的数据结构非常了不起,和之前介绍的布隆过滤器一样,是一个功能强大原理简单的数据结构.并且 ...

  5. java中传值及引伸深度克隆的思考(说白了Java只能传递对象指针)

    java中传值及引伸深度克隆的思考 大家都知道java中没有指针.难道java真的没有指针吗?句柄是什么?变量地址在哪里?没有地址的话简直不可想象! java中内存的分配方式有两种,一种是在堆中分配, ...

  6. [Leetcode] Copy list with random pointer 对带有任意指针的链表深度拷贝

    A linked list is given such that each node contains an additional random pointer which could point t ...

  7. 指针与数组的区别 —— 《C语言深度剖析》读书心得

    原书很多已经写的很清楚很精炼了,我也无谓做无意义的搬运,仅把一些基础和一些我自己以前容易搞混的地方写一下. 1. 意义: 指针: 指针也是一种类型,长度为4字节,其存放的内容只能是一个地址(4字节). ...

  8. C++11智能指针的深度理解

    平时习惯使用cocos2d-x的Ref内存模式,回过头来在控制台项目中觉得c++的智能指针有点生疏,于是便重温一下.首先有请c++智能指针们登场: std::auto_ptr.std::unique_ ...

  9. C语言初学者关于数组指针的深度讨论

    一.什么是数组指针? 即是数组的指针.首先它是一个指针,指向数组,指针本身占4个字节. 二.数组指针的使用 int a[3][5]; int (*p)[5]; p=&a; 第二行定义了一个数组 ...

随机推荐

  1. thinkphp 3.2.3 入门示例

    原文:thinkphp3.2 1.安装WAMPServer,到D:\wamp\. 2.下载ThinkPHP3.2.3核心版.解压缩后,放到D:\wamp\www\MyWeb\.打开浏览器,输入网址:h ...

  2. 记录break和continue的区别

    我对break 和 continue 还是有点搞不清除,今天在看<Thinking in Java>,看到这个,学习了一下.       break的作用是跳出这个循环(如果这个break ...

  3. ActiveReports 9 新功能:创新的设计分层报告

     在最新的ActiveReports 9报表控件添加了几个新功能,为了帮助您创建一个漂亮的外观在较短的时间内.强大的报表系统.本文重点讨论创新的分层设计报告,分组报告内容管理和设计,于实现报表套打 ...

  4. filter与servlet对照

    最近在开java物自,还记得刚开始使用servlet这是一个调试ajax什么时候,然后,我不知道怎么用,你知道写的路径来调用,总是提示404错,,到最后自己一点点的调通了,知道servlet是须要se ...

  5. poj1459(最大流)

    传送门:Power Network 题意:在一个网络图中有n个点,其中有np个发电站,nc个用户,m条电线;每个发电站,用户,和电线都对应有一个最大的电流;让求出该网络中最大的电流. 分析:最大流裸题 ...

  6. 通过 Jersey Http请求头,Http响应头,客户端 API 调用 REST 风格的 Web 服务

    原地址:http://blog.csdn.net/li575098618/article/details/47853263 Jersey 1.0 是一个开源的.可以用于生产环境的 JAX-RS(RES ...

  7. Ubuntu下将vim配置为Python IDE(转)

    工欲善其事,必先利其器. 配置好了Django的环境,该把vim好好配置一下当做python的IDE来用. 在Windows下用惯了各种现成的工具,转到Linux下,一下没了头绪……好歹google出 ...

  8. hdu 5282 Senior&#39;s String 两次dp

    题链:http://acm.hdu.edu.cn/showproblem.php?pid=5282 Senior's String Time Limit: 2000/1000 MS (Java/Oth ...

  9. 命令行參数选项处理:getopt()及getopt_long()函数使用

         在执行某个程序的时候,我们通常使用命令行參数来进行配置其行为.命令行选项和參数控制 UNIX 程序,告知它们怎样动作. 当 gcc的程序启动代码调用我们的入口函数 main(int argc ...

  10. Leetcode 细节实现 Set Matrix Zeroes

    Set Matrix Zeroes Total Accepted: 18139 Total Submissions: 58671My Submissions Given a m x n matrix, ...