资源释放
问题
正如软件可以抽象为信息采集、信息处理和信息输出一样,对于单个函数,同样可以抽象为资源申请、资源使用和资源释放。这些资源包括内存、网络连接、文件局柄等。就象以下代码所示(为简化代码,忽略了错误处理):
int TextFileLines(const char* szTxtFile)
{
//declare arguments
HANDLE hFile;
HANDLE hMap;
char* lpBuf;
int cbLines;
//acquire resources
hFile=CreateFile(szTxtFile,GENERIC_READ,0,0,OPEN_EXISTING,0,0);
hMap=CreateFileMapping(hFile,NULL,PAGE_READONLY,0,0,NULL);
lpBuf=(char*)MapViewOfFile(hMap,FILE_MAP_READ,0,0,0);
//use resources,calculate number of lines in buffer
cbLines =CalcLines(lpBuf);
//clean up resources
UnmapViewOfFile(lpBuf);
CloseHandle(hMap);
CloseHandle(hFile);
return cbLines;
}
然而,资源的释放却远不是想象中的容易,就像上面的代码中所示,所有的资源看似都已经释放,但实际上却隐含着许多的问题:
1、如果在CalcLines函数中发生异常,在此之后的所有代码都无法运行,资源发生泄漏。
2、随着代码的增加,我们必须在函数的每一个退出点上明确地进行各个资源的释放工作。
3、随着代码的增加,非常容易忘记进行资源的释放工作。
4、由于各系统资源间存在一定的内在关系,如某一资源依赖另一资源的情况,释放多个系统资源时应遵循一定的顺序,即最后申请的资源需要最先释放。
显然,我们需要一种机制,自动解决这些问题。
解决
C++的类机制是解决这些问题的自然选择。我们可以在类的构造函数中登记申请的系统资源,在类的析构函数中释放系统资源(就像auto_ptr一样)。首先,C++语言确保在离开函数的作用域时,包括因发生异常而导致的中断,对应的局部对象都会得到正确的析构,这样就保证了资源的申请和资源的释放必然会成对出现,不会发生资源的泄漏。其次,C++语言确保在局部对象的析构过程中,类析构顺序与类构造顺序相反,即最后声明的局部对象会最先得以析构,这样就确保了资源的释放顺序。最后,我们建立一个编码约定,即在申资源成功后,马上进行资源的释放登记工作,这样就解决了资源释放的遗忘问题。
确定采用类机制后,针对每种具体资源,我们不难设计出一个具体的类,实现系统资源的自动释放,就像MFC中的类一样。但这样容易导致类数量增多、代码膨胀、名字空间得到污染。我们需要一个更轻量级的、更通用的机制。类模板便是用来解决这种问题的方法。
然而,还有许多问题。首先,针对不同的资源,对应的资源释放函数的原型、参数数量和参数类型都各不相同,这就需要数量可变的模板参数、数量可变的函数参数,但遗憾的事,C++中不存在“数量可变的模板参数”,因为参数类型不同,也无法使用“数量可变的函数参数”。我们只能凭借经验,确定有限个数的参数,本文约定为4个。其次,C++中不允许同名而参数个数不同的模板类存在,既不存在模板类重载机制,我们只有针对不同的参数数量,分别命名为Func1、Func2…,但那样对于用户而言,会带来很大的麻烦。我们需要重新封装为统一的名字。
实现
按照以上思路,我们的第一次定义可能是这样(针对一个参数):
template<typename PFN,typename ARG1>
class CFuncPack1
{
public:
CFuncPack1(PFN pfn,ARG1 arg1) : m_pfn(pfn),m_arg1(arg1) {}
~CFuncPack1()
{
m_pfn(m_arg1);
}
PFN m_pfn;
ARG1 m_arg1;
};
其调用方法如下:
int main()
{
HANDLE handle=CreateFile(…);
CFuncPack1< BOOL (WINAPI*)(HANDLE),HANDLE > aFunc(CloseHandle,handle);
return 0;
}
以上便实现了文件局柄资源的自动释放。但从以上客户的调用方式来看,需要用户明确指定模板类的参数类型,尤其是第一个参数为函数指针,指定其类型就像一场恶梦。同时,用户还需根据参数数量的不同明确指定不同的模板类。那么有没有更简单的方法,让客户调用时更简单,就像以下的方式一样呢。
int main()
{
HANDLE handle=CreateFile(…);
CfuncPack aFunc(CloseHandle,handle);
return 0;
}
答案当然是肯定的。首先,我们可以使用一个统一的包装类CFuncPack来封装不同参数的模板类CFuncPack1、CFuncPack2…。虽然C++中不允许同名而参数个数不同的模板类存在,但允许存在同名而参数个数不同的模板函数存在,即模板函数重载,包括模板构造函数,在模板函数中生成不同的模板类。其次,在模板函数的调用过程中,用户可以使用C++语言提供的模板参数推导而无需手工指定参数的具体类型。最终的实现如下(针对一个参数):
class CFuncPack
{
//base class
struct InFuncPackBase
{
virtual void DirectCall(BOOL bClear=true)=0; //Manual Call
virtual ~InFuncPackBase(){}; //virtual destructor
virtual void clear()=0; //reset
};
…
//template class of one parameter
template<typename FuncType,typename ArgType1>
struct InFuncPack1 : public InFuncPackBase
{
InFuncPack1(FuncType pFunc,ArgType1 arg1) : m_pFunc(pFunc) , m_arg1(arg1) {}
~InFuncPack1() { DirectCall(); }
virtual void DirectCall(BOOL bClear=true)
{
if(m_pFunc)
{
m_pFunc(m_arg1);
}
if(bClear)
m_pFunc=NULL;
}
virtual void clear (){m_pFunc=NULL;}
FuncType m_pFunc;
ArgType1 m_arg1;
};
//Constructor of one parameter
template<typename FuncType,typename ArgType1>
CFuncPack(FuncType pFunc,ArgType1 arg1)
{
m_pInstance=new InFuncPack1<FuncType,ArgType1>(pFunc,arg1);
}
…
void clear(){m_pInstance->clear();}
InFuncPackBase* m_pInstance; //pointer to derived class
};
运用
使用以上CFuncPack模板类后,本文开始处的TextFileLines函数代码可改为如下,相对于原来代码,在代码未有明显扩充的情况下,系统资源的释放有了充分的保证。
int TextFileLines(const char* szTxtFile)
{
//declare arguments
HANDLE hFile;
HANDLE hMap;
char* lpBuf;
int cbLines;
//acquire resources
hFile=CreateFile(szTxtFile,GENERIC_READ,0,0,OPEN_EXISTING,0,0);
CFuncPack aFile(CloseHandle,hFile);
hMap=CreateFileMapping(hFile,NULL,PAGE_READONLY,0,0,NULL);
CFuncPack aMap(CloseHandle,hMap);
lpBuf=(char*)MapViewOfFile(hMap,FILE_MAP_READ,0,0,0);
CFuncPack aMapofView(UnmapViewOfFile,lpBuf);
//use resources,calculate number of lines in buffer
cbLines =CalcLines(lpBuf);
return cbLines;
}
其实还可以做的更多。看到clear函数吗?它能使函数支持“事务功能”。譬如使用WinCE的对象存储技术进行项目数据的生成函数如下:
bool CreateProject(LPCTSTR szDBFile,LPCTSTR szDesignFile)
{
//Create Database
CEGUID GuidDB;
CeMountDBVol(&GuidDB, szDBFile,CREATE_NEW);
CFuncPack aFile(DeleteFile, szDBFile);
CFuncPack aDB(CeUnmountDBVol,&GuidDB);
//Create Tables
HANDLE hTable1=CreateTable(….); //Create Table 1
CFuncPack aTable1(CloseHandle,hTable1);
HANDLE hTable2=CreateTable(….); //Create Table 2
CFuncPack aTable2(CloseHandle,hTable2);
…
//Import design data
bool bRes=ImportDesignFile(szDesignFile);
if(!bRes)
return FALSE;
…
//Reserve resource(database)
aTable2.clear();
aTable1.clear();
aDB.clear();
aFile.clear();
return true;
}
在上面的函数中,如果所有的操作都成功完成,则项目数据库和数据库表均会得到保存;否则,数据库表和数据库将会关闭,数据库文件将会删除,一切都会回到开始时的状态,不会留下和泄露任何资源,达到“事务处理”的功能。
以上代码,在Win2000+SP2、Visual C++6.0上通过编译。
胡乐秋
2003-3-27