2005年03月27日

怎样判断一个指针指向的对象是否已经被析构

几天前看到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*)0×12345678;
 p->PrintThisPtr(); //It will print “0×12345678″
 /*————————
 这句代码的汇编形式为:
 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;
}

以上代码还比较简陋,还可以慢慢完善。

2005年03月20日

VB中的函数或过程的参数有2种传递方式:一种是值传递;一种是引用传递。分别用关键字ByVal和关键字ByRef指出。如果参数是以引用传递的话,函数或过程内部就可以靠这个引用参数来改变外部变量的值。在C语言中,如果要实现在函数内部改变外部变量的值的话,就应该传递这个变量的指针。如果要通过指针访问变量,必须使用指针运算符“*”。这样在源代码中就会显得比较别扭:

void function(int *pval)
{
 *pval=100;
 //pval=100;先不考虑此处类型转换的错误
 //该代码只能改变堆栈中临时指针变量的地址,而不能改变指针指向对象的值
}

int main()
{
 int x=200;
 function(&x);
 return 0;
}

为了能透明地使用指针来访问变量,C++中引入了“引用”的概念:


void function(int &refval)
{
 refval=100;
}

int main()
{
 int x=200;
 function(x);
 //当然,如下调用也可以。但这样做就失去引入”引用”的原本意义了
 int &refx=x;
 function(refx);
 return 0;
}

这样一来,只要改一下函数声明,就可以在源代码的级别上实现指针访问和一般访问的一致性。可以把“引用”想象成一个不需要“*”操作符就可以访问变量的指针。上面的代码的C语言形式的伪代码:

void function(int *refal)
{
 *refval=100;
}

int main()
{
 int x=200;
 int *refx=&x;
 function(&x);
 function(refx);
 return 0;
}

根据函数的声明,C++编译器在遇到“function (x);”语句时,会自动转换成“function(&x);”形式的二进制代码。

来看“int **pp”和“int *&rp”区别。前者是一个指向指针的指针;后者是一个指针的引用。如果这样看不明白的话,变换一下就清楚了:

typedef int * LPINT;
LPINT *pp;
LPINT &rp;

下面这两个函数的二进制代码是一致的:

void function1(int **p)
{
 **p=100;
 *p=NULL;
}

void function2(int *&ref)
{
 *ref=100;
 ref=NULL;
}

在调用function1或function2时,编译器编译的二进制代码都将传递一个双重指针。

“引用”仅仅是为了给重载操作符提供了方便之门,其本质和指针是没有区别的。

2005年03月05日

有时,我为了编译仅有一个源文件的C/C++代码,不得不建立一个VC的工程,工程编译后会生成一堆文件,我不需要搞得如此庞大。后来我改用命令行编译,但是我需要输入冗长的参数来编译。为了一劳永逸地解决这个问题,我就试着将我常用的EditPlus设置为编译器的IDE。

只要修改一下VC安装目录下的”C:\Program Files\Microsoft Visual Studio\VC98\Bin\VCVARS32.BAT”文件,然后在EditPlus的“用户配置工具”中“添加工具”就可以了。

“VCVARS32.BAT”这个批处理文件的作用是设置编译器的环境变量,将该文件复制到EditPlus的安装目录下,在文件末尾添加如下命令:

@echo on
@cl %1 /ML /GX /O2 /D “WIN32″ /D “NDEBUG” /D “_CONSOLE” /D “_MBCS” /link “kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /incremental:no /machine:I386″

如果你还要生成asm中间代码的话可以在此处添加”/Fa”参数。

在EditPlus中添加一个用户工具,名叫“C/C++ 编译”。设置“命令”为”C:\Program Files\EditPlus 2\VCVARS32.BAT”,这个文件就是刚才修改过的VCVARS32.BAT文件。然后设置“参数”为”$(FilePath)”;“初始目录”为”$(FileDir)”即可。现在只要在EditPlus中按下Ctrl+1(如果没有设置其他用户工具的话)就可以编译C/C++了。

不过VC编译器默认编译的EXE的”Subsystem”是”console”。如果要编译windows程序,必须在源代码中添加:

#pragma comment(linker,”/subsystem:windows”)

现在编译后仅仅生成一个obj和一个exe文件。好了,这个世界清静多了。