今天偶然发现一个很有意思的问题,在vc编译器想检查有没有内存泄露,于是在一个类的构造函数和析构函数各下一个断点,追踪特定分配出来的一个对象实例有没有析构。却发现无论如何都没有析构,但是使用vld内存检测工具却没有发现内存泄露,到底有没有析构呢?
原先我用的方法是直接下断点, 在监视器里看this的值,发现根本找不到对应的this被析构。有点抓狂,后来怕出错,把this赋给一个变量,打印到输出窗口里,却发现一一配对,构造和析构,不存在泄露问题!。更抓狂了,怎么回事呢?
断点在析构函数,
~some()
{
void * pThis=this;
}
这下看清楚了,鼠标晃上去或者在监视窗口里头发现pThis和this的值并不相等,pThis毫无疑问是预期的,this却总是多了一些字节,像是某个偏移量。于是联想到vc编译器虚函数相关实现机制和asm汇编出来的代码,总算搞清楚这个问题了。下面以一个简单的实例来说明:
假设有如下类定义:
class A
{
int xxx;
public:
virtual void func1(){}
};
class B
{
int xxx;
public:
virtual void *func2(){return NULL;}
};
class C:public A,public B
{
int ddd;
public:
virtual void *func2(){
return this;
}
virtual void func3(){}
};
作如下调用:
C t1;
t1.func2();
跟踪func2的执行过程,会发现,因为func2是一个虚调用,并且是从B继承下来的,所以在执行func2调用的时候this' 指针指向的是父类的地址.这通过汇编代码可以很容易发现
005C2930 lea ecx,[ebp-28h]
005C2933 call C::C (4BAA2Fh)
这两行对应构造t1的函数调用.请注意,ecx值即this指针为ebp-28
005C299A lea ecx,[ebp-20h]
005C299D call C::func2 (4B7843h)
这两行对应t1.func2()的调用.ecx值b变成了ebp-20 this'=this+8
所以进入C类的func2函数调用的时候,this的值实际上是父类B的首地址,会加个偏移量8,记为this'. 所以你用鼠标悬停查看this或者在监视窗口中看this总是这个值this'。但是根据this的c++标准定义,它应该等于调用时成员函数所在类的首地址.所以你在C的func2调用中存取this的值的时候它又会换算成正确的值this=this'-8.这在汇编代码中可以得到确认:
004C2CEC mov eax,dword ptr [this]
004C2CEF sub eax,8
最后返回的值eax=this'-8
总结如下:
1. 在c++代码中存取this的值得时候它总是等于本类的实例首地址
2.实际发生虚调用的时候要传this的值(thiscall通过ecx传递),它等于父类的实例地址。单继承的情况下两者一致,多继承的时候就可能差一个偏移量了.
所以在虚函数调用中看到this关键字,在汇编层面它并不一定等于自己的首地址哦.但是在语言层面是相等的.
(这在委托调用的时候传递this指针的值时需要注意,否则会出错)
最后再提一点,对于上面的虚函数调用
C t1;
t1.func2()
实际的汇编代码并不是一个虚调用,因为是通过类成员方法调用的,已经知道调用地址在哪里了.vc的编译器做了这个优化.
但对于指针的虚函数调用一般都是确实的虚调用.例如:
B * p1=&t1;
C * p2=&t1;
p1->func2();
p2->func2();
都会生成如下的汇编代码:
005C2966 mov ecx,dword ptr [ebp-34h]
005C2969 add ecx,8 //调整this指针
005C296C mov eax,dword ptr [ebp-34h]
005C296F mov edx,dword ptr [eax+8] //查找虚表调用地址
005C2972 mov esi,esp //保存esp,因为是debug版会检测esp值
005C2974 mov eax,dword ptr [edx]
005C2976 call eax