几天前看到CSDN上有人问起这个问题。原贴见:http://community.csdn.net/Expert/TopicView.asp?id=3875259。因为我最近也在学习C++,所以对这个问题产生了兴趣。在这篇文章中,我会对该贴中提出的一些错误方法进行逐一分析,并给出解决这个问题的一点建议。
正如该贴中的有些回答所提到的那样——指针delete后,最好将它赋值为NULL。这样方便在以后通过检查该指针来确定对象是否已经析构。但在某些情况下,我们会将会将一个对象的地址赋于多个指针变量。这样一来,在delete对象时,很难做到将所有指向该对象的指针全部赋值为NULL。
首先,有人提出用IsBad*系列的Debug API来判断指针指向的内存是否可读写。当然,这种方法是不可行的。C++对象的内存是分配在栈或者堆中的。不管何种情况下,栈内存永远是可读写的。而在系统堆中分配的内存的一个特性就是无法直接干预内存分页的提交和回收。也既是说,用new操作符分配的内存,即使delete后,也无法预料该内存是否被系统回收,它仅仅被系统标记为未使用而已。所以IsBad*系列API是无法判断某指针指向的对象是否已经被析构。
有人又提出了使用使用C++的错误处理来处理异常。这也是不可行的。除非该对象包含虚函数,并且你调用了虚函数,才有可能发生异常。而在大多数情况下,使用一个被析构的对象并不会发生异常。但是你的代码会影响到程序中的其他的数据,最终导致程序的运行结果异常。
还有人提出了如下的方法:
class cfoo
{
public bool isValid()
{
if (this != NULL)
return true;
else
return false;
}
}
这种方法显然是可笑的。此人估计是个新手(当然,我也是新手)。对象内部的this指针时由调用者决定的。调用者在调用成员函数前,会将当前对象的地址保存到ECX寄存器。成员函数内部便使用ECX寄存器中保存的值来作为this指针的值。下面的代码也许可以让你更深刻得理解:
#include <stdio.h>
class ctest
{
public:
void PrintThisPtr();
};
void ctest::PrintThisPtr()
{
printf("0x%08X\n",this);
}
int main()
{
ctest *p=(ctest*)0x12345678;
p->PrintThisPtr(); //It will print "0x12345678"
/*------------------------
这句代码的汇编形式为:
mov ecx, 12345678H
call ctest::PrintThisPtr
--------------------------*/
return 0;
}
最后又有人提出了一个方法:
class foo
{
public:
foo():m_Valid(TRUE){}
~foo(){m_Valid = FALSE;}
BOOL IsValid()
{
try
{
if(m_Valid)
return TRUE;
else
return FALSE;
}
catch(...)
{
return FALSE;
}
}
private:
BOOL m_Valid;
};
可惜,这个方法仍旧不可行。在一个对象被析构后,该对象原先占用的内存就被标识为未使用(堆和栈的做法是不一样的)。在程序运行一段时间后,原先的这块内存会被一些无法预见的数据所覆盖,m_Valid的值也无法保证其正确性。后来,提出该方法的人又改进了代码:给m_Valid赋一个特殊的值,比如0xEFEFEFEF。这仅仅是一个讨巧的办法,你也许可以在混乱编程大赛中使用该方法,但是你不能在正式的工程中使用这个方法。这个方法无法保证100%不出问题。一个最容易想到的反例就是:在这块内存中,又分配了一个新的foo对象,新对象的m_Valid成员和旧对象的重合。
经过前面的分析,能够得到一个结论:C++对象的指针指向的仅仅是块普通的内存区域,和C语言中的结构体没有什么区别。所以,要判断一个指针指向的对象是否已经被析构必须借助于不依赖对象的外部标记才能实现。一个经常使用的方法就是双重指针。直接指向对象的指针仅有一个,引用对象必须使用一个指向该指针的双重指针。当对象析构时,将那个直接指向对象的指针赋值为NULL。其他的双重指针就可以根据这个指针的值来判断对象是否被析构。
#include <stdio.h>
class CTest{
public:
int m;
};
int main()
{
CTest *handle=new CTest;
CTest** p1=&handle;
CTest** p2=&handle;
CTest** p3=&handle;
delete handle;
handle=NULL;
if(NULL!=handle)
{
(*p1)->m=100;
(*p2)->m=200;
(*p3)->m=300;
}else{
printf("对象已经析构");
}
return 0;
}
这个直接指向对象的指针在此处就起到了句柄的作用。还有一种方法:
#include <stdio.h>
class CTest{
public:
CTest(bool *pIsValid)
{
*pIsValid=true;
m_pIsValid=pIsValid;
};
~CTest()
{
*m_pIsValid=false;
};
private:
bool *m_pIsValid;
};
bool IsValid;
int main()
{
CTest *ptr=new CTest(&IsValid);
delete ptr;
if(IsValid)
{
printf("对象还存在");
}else{
printf("对象已经析构");
}
return 0;
}
每创建一个对象,必须有一个IsValid对应。如果要动态创建多个对象的话,IsValid也需要动态创建,并且IsValid的生存时间必须大于它标示的对象。但是IsValid的生存时间也不能是永久的,这样就产生内存泄漏了。
不管采用什么方法,必须依靠外部标示来表示对象的生存期,而且要尊循以下原则:每个对象必须与唯一的外部标示对应;每个外部标示也对应唯一的对象;外部标示的生存时间必须大于对象的生存时间;当确定不再需要查询对象的生存期时,应当释放外部标示。如果情况复杂的话,最好实现句柄表来查询。
#include <stdio.h>
#define HT_LEN 256
class foo
{
public:
foo(){};
~foo(){};
void print(){printf("%d\n",m);};
int m;
};
typedef struct
{
unsigned int handle;
foo *ptr;
} HTABLE;
HTABLE g_HandleTable[HT_LEN];
unsigned int g_MaxHandle;
foo* GetObject(unsigned int handle)
{
int i;
for(i=0;i<HT_LEN;i++)
{
if(handle==g_HandleTable[i].handle)
return g_HandleTable[i].ptr;
}return NULL;
}
bool DeleteObject(unsigned int handle)
{
int i;
for(i=0;i<HT_LEN;i++)
{
if(handle==g_HandleTable[i].handle)
{
delete g_HandleTable[i].ptr;
g_HandleTable[i].handle=0;
g_HandleTable[i].ptr=NULL;
return true;
}
}return false;
}
unsigned int CreateObject()
{
int i;
for(i=0;i<HT_LEN;i++)
{
if(0==g_HandleTable[i].handle)
{
g_HandleTable[i].ptr=new foo;
g_HandleTable[i].handle=++g_MaxHandle;
return g_HandleTable[i].handle;
}
}return 0;
}
int main()
{
unsigned int hfoo=CreateObject();
foo *p=GetObject(hfoo);
if(NULL!=p)
{
p->m=100;
p->print();
printf("HANDLE VALUE:%d\n",hfoo);
}else{
printf("对象不存在");
}
DeleteObject(hfoo);
return 0;
}
以上代码还比较简陋,还可以慢慢完善。