前言
文章讲述的是C++This指针的本质,偏底层,得懂点汇编的知识,当然,如果你不懂也没关系,我会适当地补充。
1.问题
#include <iostream>
#include <string>
using namespace std;
class CNullPointCall
{
public:
static void Test1();
void Test2();
void Test3(int iTest);
void Test4();
private:
static int m_iStatic;
int m_iTest;
};
int CNullPointCall::m_iStatic = 0;
void CNullPointCall::Test1()
{
cout << m_iStatic << endl;
}
void CNullPointCall::Test2()
{
cout << " Very Cool! " << endl;
}
void CNullPointCall::Test3(int iTest)
{
cout << iTest << endl;
}
void CNullPointCall::Test4()
{
cout << m_iTest << endl;
}
int main()
{
CNullPointCall *p=NULL;
p->Test1(); //call 1
p->Test2(); //call 2
p->Test3(10); //call 3
p->Test4();//call 4
return 0;
}
先看下这段代码,也可以直接复制到IDE,跑一跑,看下效果怎么样。
如果出现了问题,把call 4这行去掉,看看效果怎么样。
结果是:
去掉call 4这行,代码运行正确,这是为什么?
2.补充
在回答这个问题之前,补充一下知识点,这些知识点希望大家记住,它们贯穿全文。
1.不同类中的成员属性和成员函数名是可以相同的,那也就是说不同类的函数或属性之间没有任何关系。
2.每一个非静态成员函数都含有一个this指针(函数形参)
3.this指针的声明形式:A * const this (A为类型名)
对 A * const this 这种声明方式补充:
意思就是定义了一个指针变量,这个指针变量可以更改所指向的那块存储段中的数据,但是只可以存储对象地址,并且只能存储某一个对象的地址。
4.对于类成员函数而言,并不是一个对象对应一个单独的成员函数体,而是此类的所有对象共用这个成员函数体。 当程序被编译之后,此成员函数地址即已确定。
5.成员函数之所以能把属于此类的各个对象的数据区别开, 就是靠这个this指针。函数体内所有对类数据成员的访问, 都会被转化为this->数据成员的方式。
6.一个对象的this指针并不是对象本身的一部分,不会影响sizeof(“对象”)的结果。
补充:一个对象在内存中所占总字节数就是非静态成员变量所占内存字节总和(要考虑内存对齐)
7.this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。
补充:this指针的声明周期和非静态成员函数的生命周期是相同的
编译器不会给静态成员函数传this值
8.对象的实例化后(定义了一个对象),非静态成员变量在栈中的位置有点特殊,先定义的变量地址更低,而不是通常情况下的先定义的变量地址更高。
9.dword 双字 就是四个字节
ptr pointer 缩写 即指针
[]里的数据是一个地址值,这个地址指向一个双字型数据
比如mov eax, dword ptr [12345678] 把内存地址12345678中的双字型(32位)数据赋给eax
3.解答
你肯定会很奇怪,一个值为NULL的指针怎么可以用来调用类的成员函数呢?!可是事实却很让人吃惊:除了call 4那行代码以外,其余3个类成员函数的调用都是成功的,都能正确的输出结果,而且包含这3行代码的程序能非常好的运行。
经过细心的比较就可以发现,call 4那行代码跟其他3行代码的本质区别:类CNullPointCall的成员函数中用到了this指针。
对于类成员函数而言,并不是一个对象对应一个单独的成员函数体,而是此类的所有对象共用这个成员函数体。 当程序被编译之后,此成员函数地址即已确定。而成员函数之所以能把属于此类的各个对象的数据区别开, 就是靠这个this指针。函数体内所有对类数据成员的访问, 都会被转化为this->数据成员的方式。
而一个对象的this指针并不是对象本身的一部分,不会影响sizeof(“对象”)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。
对于上面的例子来说,this的值也就是pNull的值。也就是说this的值为NULL。而Test1()是静态函数,编译器不会给它传递this指针,所以call 1那行代码可以正确调用
补充:由于类中的成员函数是所有该类对象共有的,因此任意一个对象在调用成员函数的时候或者是以指针的形式调用成员函数,都可以理解为:
p->Test1()== CNullPointCall::Test1()
对于Test2()和Test3()两个成员函数,虽然编译器会给这两个函数传递this指针,但是它们并没有通过this指针来访问类的成员变量,因此call 2和call 3两行代码可以正确调用;而对于成员函数Test4()要访问类的成员变量,因此要使用this指针,这个时候发现this指针的值为NULL,就会造成程序的崩溃。
其实,我们可以想象编译器把Test4()转换成如下的形式:
void CNullPointCall::Test4(CNullPointCall * const this )
{
cout << this -> m_iTest << endl;
}
而把call 4那行代码转换成了下面的形式:
CNullPointCall::Test4(pNull);
上面的C++代码编译生成的汇编代码是下面的形式:
通过比较静态函数Test1()和其他3个非静态函数调用所生成的的汇编代码可以看出:非静态函数调用之前都会把指向对象的指针pNull(也就是this指针)放到ecx寄存器中(mov ecx,dword ptr [pNull])。这就是this指针的特殊之处。看call 3那行C++代码的汇编代码就可以看到this指针跟一般的函数参数的区别:一般的函数参数是直接压入栈中(push 0Dh),而this指针却被放到了ecx寄存器中。在类的非成员函数中如果要用到类的成员变量,就可以通过访问ecx寄存器来得到指向对象的this指针,然后再通过this指针加上成员变量的偏移量来找到相应的成员变量。
下面再通过另外一个例子来说明this指针是怎样被传递到成员函数中和如何使用this来访问成员变量的。
依然是一个很简单的类:
#include <iostream>
#include <string>
using namespace std;
class CTest
{
public:
void SetValue();
int m_iValue1;
int m_iValue2;
};
void CTest::SetValue()
{
m_iValue1 = 13;
m_iValue2 = 13;
}
int main()
{
CTest test;
test.SetValue();
return 0;
}
main函数中的汇编代码为:
同样的,首先把指向对象的指针放到ecx寄存器中;然后调用类CTest的成员函数SetValue()。地址0751488h那里存放的其实就是一个转跳指令,转跳到成员函数SetValue()内部。
接下来,我们看下CTest::Setvalue函数的功能
1. 将ecx寄存器中的值压栈,也就是把this指针压栈。
2、ecx寄存器出栈,也就是this指针出栈。
3、将ecx的值放到指定的地方,也就是this指针放到[ebp-8]内。
4、取this指针的值放入eax寄存器内。此时,this指针指向test对象,test对象只有两个int型的成员变量,在test对象内存中连续存放,也就是说this指针目前指向m_iValue1。
5、给寄存器eax指向的地址赋值0Dh(十六进制的13)。其实就是给成员变量m_iValue1赋值13。
6、同4。
7、给寄存器eax指向的地址加4的地址赋值。在4中已经说明,eax寄存器内存放的是this指针,而this指针指向连续存放的int型的成员变量m_iValue1。this指针加4(sizeof(int))也就是成员变量m_iValue2的地址。因此这一行就是给成员变量m_iValue2赋值。
CTest::Setvalue函数的功能看不懂也没关系,因为你看这篇文章的目的是学C++This指针的本质
总结
在看解答之前,一定要把补充那部分好好看看。
本篇文章讲述的是C++This指针的本质,讲的还是比较深的,希望你能够从中得到一些收获。