析构函数是C++中一个神奇的部分,在调用析构函数时,并不需要像普通函数一样写出函数调用的代码,而是由编译器将析构函数插入到程序中合适的调用地方。如果你不清楚这些插入析构函数的地方,就会出现一些很难解决的错误。
在分析析构函数的执行时,一个经典的示例是全局变量的析构过程。我们来考虑下面的代码:
01.
#include< STDAFX.H >
02.
CcomPtr g_pUnKnow;
03.
int
__cdecl main(
int
argc,
char
** argv)
04.
{
05.
if
(SUCCEEDED(CoInitialize(NULL)))
06.
{
07.
g_pUnKnow.CoCreateInstance(CLSID_IXMLDOMDocument);
08.
……
09.
CoUninitialize();
10.
}
11.
}
当运行这个程序时,将会在调用g_pUnKnow的析构函数时发生崩溃。其中的原因是:全局变量的析构函数是主程序退出时才调用的,而在主程序退出时,COM环境也将被卸载。COM的卸载工作包括释放在初始化COM环境时所加载的动态链接库。然后当你释放全局变量指针时,程序将崩溃,因为程序试图与一个不存在的DLL通信。
这个问题并不仅限于全局变量,有时候局部变量也会出现这样的情况:
01.
void
Sample()
02.
{
03.
if
(SUCCEEDED(CoInitialize(NULL)))
04.
{
05.
CcomPtr p;
06.
if
(SUCCEEDED(p.CoCreateInstance(CLSID_IXMLDOMDocument)))
07.
{
08.
……
09.
}
10.
CoUninitialize();
11.
}
12.
}
这段程序非常简单,在代码中有一个错误。智能指针的析构函数在什么时候被调用?答案是:当智能指针超出作用域的时候被调用。由于已经卸载了COM环境,当你再试图访问一个指向COM对象的指针时,将发生与前面一样的错误。
要修正这个问题,就必须在CoUninitialize之前释放左右的COM指针。方法就是加入一个看上去似乎没有必要的作用域:
01.
void
Sample()
02.
{
03.
if
(SUCCEEDED(CoInitialize(NULL)))
04.
{
05.
{
06.
CcomPtr p;
07.
if
(SUCCEEDED(p.CoCreateInstance(CLSID_IXMLDOMDocument)))
08.
{
09.
……
10.
}
11.
}
12.
CoUninitialize();
13.
}
14.
}
不过你要确保在代码中留下相应的注释,确保不会被阅读这段代码的人删除这两个“多余的”大括号。
有些人可能会认为这个解决方案很不直观。那么下面将给出另外一个解决方案:将CoUninitialize放在某个对象的析构函数中。
01.
Class CCoInitialize
02.
{
03.
public
:
04.
CCoInitialize () : m_hr(CoInitialize(NULL)){}
05.
~ CCoInitialize (){
if
(SUCCEEDED(m_hr)) CoUninitialize();}
06.
Operator
HRESULT
()
const
{
return
m_hr;}
07.
HRESULT
m_hr;
08.
}
09.
void
Sample()
10.
{
11.
CCoInitialize init;
12.
if
(SUCCEEDED(init))
13.
{
14.
CcomPtr p;
15.
if
(SUCCEEDED(p.CoCreateInstance(CLSID_IXMLDOMDocument)))
16.
……
17.
}
18.
}
//在这里调用CoUninitialize
现在即使你将智能指针放在同样的作用域中依然可行。只要保证智能指针是位于CCoInitialize对象之后:
1.
void
Sample()
2.
{
3.
CCoInitialize init;
4.
CcomPtr p;
5.
……
6.
}
这段代码是没有问题的,因为自动储存类型对象在调用析构函数时的顺序与声明这些对象的顺序是相反的。所以对象p首先被析构,然后才是对象init。
到目前为止,我们已经看到了一些在错误时刻调用的析构函数。现在,再来看一些不会被调用的析构函数。
假设有一个ObjectLock类,在这个类的构造函数中将获得一个锁,并在其析构函数中释放这个锁:
1.
DWORD
ThreadProc(
LPVOID
p)
2.
{
3.
……
//第一部分操作
4.
ObjectLock lock(p);
5.
……
//第二部分操作
6.
return
0;
7.
}
在这段代码中,第一部分的操作是在没有加锁的情况下完成的,而第二部分操作则是在加锁的情况下完成的。当函数返回时,这个锁将自动被释放。然而如果在这个函数中增加了下面这样一行代码:
01.
DWORD
ThreadProc(
LPVOID
p)
02.
{
03.
……
//第一部分操作
04.
ObjectLock lock(p);
05.
……
//第二部分操作
06.
if
(p->cancelled)
07.
ExitThread(1);
08.
……
09.
return
0;
10.
}
这段代码的意思是:如果对象被取消了,就提前退出线程。但是ObjectLock对象的析构函数在什么时候被调用呢?
这个析构函数将在return语句中运行,因为此时ObjectLock对象已经超出作用域。然而,在调用ExitThread函数之前,析构函数是不会被调用的。结果就是,程序使一个对象被永久锁定。
有些人可能会争论:调用ExitThread是不好的变成习惯,我们应该通过执行到线程函数的最后来结束一个线程。然而,有一种情况你必须通过退出函数来退出线程:如果是一个工作线程,虽然这个线程的生命周期并没有被进程显示管理,但线程的代码是在一个DLL中。这种情况下,标准的做法是:当工作线程启动的时候,调用LoadLibrary(Load Count)函数来增加DLL的加载计数,而当工作线程结束时,调用FreeLibraryAndExitThread函数(当然,你也可以同样使用GetModuleHandleEx函数来增加加载计数)。如果使用这种方法,线程看起来就像这样:
1.
DWORD
ThreadProc(
LPVOID
p)
2.
{
3.
….
4.
ObjectLock lock(p);
5.
….
6.
FreeLibraryAndExitThread(g_hinst, 0);
7.
//不会执行到这个位置
8.
}
其中g_hinst是一个全局变量,在这个变量中保存的是DLL的实例句柄。在这种情况下,你会遇到和前面同样的问题:ObjectLock的析构函数是在函数的括号结束处执行的,但是FreeLibraryAndExitThread函数退出线程并且不会再回到函数中。因此,析构函数永远不会被执行。同样,我们依然可以使用一个嵌套的作用域来强制析构函数的执行:
1.
DWORD
ThreadProc(
LPVOID
p)
2.
{
3.
{
4.
……
5.
ObjectLock lock(p);
6.
….
7.
}
8.
}