浅析C++中的this指针
在编写C++程序的时候,我们可以不需要知道this指针的存在,但是,如果你想深入了解c++.。或者从事软件逆向。这里我们是很有必要知道的。下面,我们就一步一步的探究this指针的秘密。
为何引入this指针:
类中每个数据成员对该类的每个对象都有一个拷贝,要在内存中为其划分一块内存单元,分别存储从类中拷贝的数据成员,静态变量除外,因为类的每个对象都可以共享静态变量。而类的成员函数对该类的每个对象只有一个拷贝。对于该成员函数类来讲,它使用类辨别进行哪个对象的操作。因此C++语言引入了this指针,每个成员函数都隐含这一个常量指针类型的型参,即this指针。
概念:
This指针是类或者结构体,联合体中的成员函数中隐含存在的指针。它指向一个对象,来指出具体的是哪个对象的成员函数被调用了。值得注意的是静态成员函数中不存在this指针。
当一个类对象的非静态数据成员被调用的时候,对象的地址作为一个隐含的存数被传递给函数。举个例子:
myDate.setMonth(3);
我们可以将其翻译成如下的形式:
setMonth(&myDate,3);
注意:修改this指针在最近版本的C++中是不合法的。
This指针的一个实例:
Void Date::setMonth(int mn)
{
Month =mn;
this->month=mn;
(*this).month=mn;
}
例子中,这三种声明方式相等的,为何相等,看了如下的反汇编,你会清楚!
反汇编:
#include<iostream>
using namespace std;
class feng
{
static int a;
int b;
int c;
public:
feng(int b,int c)
{
this->b=b;
this->c=c;
}
static void out()
{
cout<<a<<endl;
}
int outof()
{
cout<<b<<" "<<c<<endl;
return 0;
}
virtual int getout()
{
return a;
}
};
int feng::a=1234;
void main()
{
feng xiang(13,14);
xiang.out();
xiang.outof();
cout<<xiang.getout()<<endl;
}
源码中,我建立了三种不同的成员函数,以此来研究在静态成员函数,普通类成员函数,和虚函数中,this指针是否存在,及存在形式。
下面是相应的main代码及注释:
proc near ; CODE XREF: _mainj
.text:004015A0
.text:004015A0 var_4C = byte ptr -4Ch
.text:004015A0 object = byte ptr -0Ch
.text:004015A0
.text:004015A0 push ebp ; 改名为object是为了一看就知道是一个对象的存放地址变量
.text:004015A1 mov ebp, esp
.text:004015A3 sub esp, 4Ch
.text:004015A6 push ebx
.text:004015A7 push esi
.text:004015A8 push edi
.text:004015A9 lea edi, [ebp+var_4C]
.text:004015AC mov ecx, 13h
.text:004015B1 mov eax, 0CCCCCCCCh
.text:004015B6 rep stosd ; 上面的操作是内存初始化过程
.text:004015B8 push 14
.text:004015BA push 13 ; 将对象的两个操作数压栈,转换为十进制便于识别。
.text:004015BC lea ecx, [ebp+object] ; 将对象存放的首地址传递给exc寄存器
.text:004015BF call j_feng__feng ; 实例化类对象。
.text:004015C4 call j_feng__out ; 这里是静态成员函数的调用。
.text:004015C9 lea ecx, [ebp+object] ; 实例化对象的首地址传给ecx。
.text:004015CC call j_feng__outof ; 调用普通类成员函数,outog()
.text:004015D1 push offset loc_4010C8
.text:004015D6 lea ecx, [ebp+object] ; 实例化对象的首地址再次传给ecx。
.text:004015D9 call j_feng__getout ; 调用虚函数getout()
.text:004015DE push eax ; 虚函数的返回值传到了eax中,并入栈,为后面的输出作准备。
.text:004015DF mov ecx, offset std__cout ; c++标准输入输出流库函数cout压栈
.text:004015E4 call sub_401104 ; 执行数据输出操作,返回值保存到eax中
.text:004015E9 mov ecx, eax ; 输出操作入栈。
.text:004015EB call j_std__basic_ostream_char_std__char_traits_char_____operator__ ; 这里长长的调用是C++标准库文件函数,在这里的用途是:将有多个连续的数据输出时,与压入参数顺序相反输出同时输出结束符:endl
.text:004015F0 pop edi
.text:004015F1 pop esi
.text:004015F2 pop ebx
.text:004015F3 add esp, 4Ch
.text:004015F6 cmp ebp, esp
.text:004015F8 call __chkesp ; 到这里检查堆栈是否平衡,和程序结束的收尾工作。
.text:004015FD mov esp, ebp
.text:004015FF pop ebp
.text:00401600 retn
.text:00401600 main endp
.text:00401600
下面我们一一来看三个成员函数调用。
先看第二个普通成员函数的调用。
xiang.outof();
对应其代码:
.text:004015C9 lea ecx, [ebp+object]
.text:004015CC call j_feng__outof
我们看到,在调用outof()函数之前,ecx中压入了类对象的地址。通过调试,我们看看ecx中的状态。
图1.
如图1所示,在调试中我们发现,此时ecx中存放的是object。也就是存放的是类实例化对象的地址。
我们进入函数内部去看ecx的作用。
.text:004016E0 feng__outof proc near ; CODE XREF: j_feng__outofj
.text:004016E0
.text:004016E0 var_44= byte ptr -44h
.text:004016E0 var_4= dword ptr -4
.text:004016E0
.text:004016E0 push ebp
.text:004016E1 mov ebp, esp
.text:004016E3 sub esp, 44h ; Integer Subtraction
.text:004016E6 push ebx
.text:004016E7 push esi
.text:004016E8 push edi
.text:004016E9 push ecx
.text:004016EA lea edi, [ebp+var_44] ; Load Effective Address
.text:004016ED mov ecx, 11h
.text:004016F2 mov eax, 0CCCCCCCCh
.text:004016F7 rep stosd ; Store String
这里是函数初始化部分。
.text:004016F9 pop ecx
.text:004016FA mov [ebp+var_4], ecx
此时,我们将ecx弹出堆栈,并且将其放到ebp-4的地址中。
.text:004016FD push offset loc_4010C8
.text:00401702 mov eax, [ebp+var_4]
.text:00401705 mov ecx, [eax+8]
这里将对象地址保存到了eax中。同时通过eax(即对象地址)+8取出参数14放入ecx中保存。
.text:00401708 push ecx
.text:00401709 push offset asc_46F020 ; " "
这两句时将参数14 ,和空格符“ ”压栈。
.text:0040170E mov edx, [ebp+var_4]
.text:00401711 mov eax, [edx+4]
.text:00401714 push eax
而这里将对象保存到edx中,通过对象地址+4取出参数13放入eax中,同时将数据压栈。
.text:00401715 mov ecx, offset std__cout
.text:0040171A call sub_401104 ; Call Procedure
.text:0040171F push eax
到这里,是C++标准库文件实现cout操作,输出13。并将cout操作压栈
.text:00401720 call j_std__operator__ ; Call Procedure
实现空格输出。
.text:00401725 add esp, 8 ; Add
这里为后面调用sub_401104 做准备。
.text:00401728 mov ecx, eax
.text:0040172A call sub_401104 ; Call Procedure
将cout操作保存到ecx中,同时,调用sub_401104 操作,输出14.
.text:0040172F mov ecx, eax
.text:00401731 call j_std__basic_ostream_char_std__char_traits_char_____operator__ ; Call Procedure
这里是实现一串数据输出的收尾工作。相当于<<endl操作。
.text:00401736 xor eax, eax ; Logical Exclusive OR
寄存器eax清零。
.text:00401738 pop edi
.text:00401739 pop esi
.text:0040173A pop ebx
.text:0040173B add esp, 44h ; Add
.text:0040173E cmp ebp, esp ; Compare Two Operands
.text:00401740 call __chkesp ; Call Procedure
.text:00401745 mov esp, ebp
.text:00401747 pop ebp
函数结束收尾工作。
通过对这里普通成员函数的调用,我们发现。当类对象在给实例化对象的普通成员函数传送数据成员的时候,是通过对对象的地址加上数据成员所在的偏移地址来传送的。这里如果不知道对象的地址,我们普通数据成员函数是无法从类初始化对象中获得相应的数据成员的。
而这个作用也正是this指针的作用,让普通数据成员函数与类对象有了间接的联系方式——通过this指针。
下面我们来看静态成员函数是如何访问数据的。
xiang.out();
此函数为静态成员函数,其调用方式我们看到直接
text:004015C4 call j_feng__out
函数调用之前,并不是和普通成员函数一样现将相应对象的地址放入到ecx中。也就是说,静态成员函数并不需要什么方式来寻找调用它的对象。而这时静态成员函数的属性来决定的。静态成员具有共享的特征,也就是说,当程序建立,如果类中有静态成员函数,或数据,它一旦初始化,无论有多少个对象建立,都只有一个内存存储空间。
也就是这样,我们调用out()函数时,不需要与对象有任何联系,如果任何对象中对静态成员数据有所更新,静态数据成员会更新,不产生副本。
虚函数的调用。
xiang.getout()
尽管在虚函数的函数调用的方式上是与普通函数有所不同,但是,我们通过对代码的观察我们会发现,成员函数的数据在与对象发生“关系”(这里是指有数据传输)时,同样需要this指针来指明是哪个对象的什么数据成员被那个成员函数调用。
但是getout()内部,我们调用的确是静态成员函数,所以,没有用到this指针。
虚函数的调用方式,在“浅析c++中虚函数的调用”详细揭示了其原理。
小结:
类中,this指针的实质就是指向实例对象的指针。它作用于类非静态成员。它是类中对象与非静态成员函数的一个发生相互“关系”的纽带。
通常在调用非静态成员函数时,this指针通过ecx传入到函数中。函数中通常需要用到this的值来访问相关的数据。