编程中的资源管理(一)
一、 编程中使用的资源
在进行编程时,会用到各种各样的资源,比如文件、网络连接、数据库连接、信号量、事件、线程、内存等。在这里我们也把内存视为资源。这些资源都是非常珍贵的,不能无限期的拥有而不释放,好的编程习惯应该尽早释放这些资源,以下我们讨论使用什么方式来释放他们,以及在有异常的情况下如何保证释放资源。
二、 在C++中利用析构函数
1. 直接释放资源
例子代码如下:
void f(const char* fn)
{
// open fn for reading
FILE * pFile = fopen(fn, "r");
// use file
use_file( pFile ) ;
fclose( pFile ) ;
}
这里我们省略了use_file的处理逻辑。函数f的逻辑是很简单的,打开文件、使用文件,最后关闭文件。如果use_file函数正常结束,一切都没有问题,资源正常释放,但是如果use_file抛出了异常,那么文件将永远不会关闭。如果要修正这样的问题,直接的修改如下:
void f(const char* fn)
{
// open fn for reading
FILE * pFile = fopen(fn, "r");
try {
// use file
use_file( pFile ) ;
}
catch(…) {
fclose( pFile ) ;
}
fclose( pFile ) ;
}
可以看到这样的修改引入了更多的代码,还有两处释放资源代码(红色代码)。如果函数内使用更多的资源,那么这将是很痛苦的。幸运的是C++提供了优雅的解决方案。
2. 使用RAII惯用法
C++语言中的一个惯用法是RAII (Resource acquisition is initialization) 。C++语言保证无论以何种方式退出函数,局部对象的析构函数总会被调用。其基本思想是使用局部对象代表资源,让局部对象的析构函数来释放资源。这样就保证了不会有资源泄漏、资源未释放的问题。
对于以上问题,C++创始人Bjarne Stroustrup的解决方案如下:
class File_handle {
FILE* p;
public:
File_handle(const char* n, const char* a)
{ p = fopen(n,a); if (p==0) throw Open_error(errno); }
File_handle(FILE* pp)
{ p = pp; if (p==0) throw Open_error(errno); }
~File_handle() { fclose(p); }
operator FILE*() { return p; }
// ...
};
void f(const char* fn)
{
// open fn for reading
File_handle f(fn,"r");
// use file through f
use_file( f ) ;
}
这样无论函数f正常退出、还是因为异常而退出,C++都保证调用File_hanlle的析构函数,这样无论如何都将文件句柄关闭。可以看出函数f的代码清晰很多,而且更容易理解和使用,也更安全。
3. 应用的例子:
1) C++标准库中的auto_ptr类。不同的是,这里的资源是内存。题外话,auto_ptr不能用于STL中的容器,不能使用它来释放new出来的数组。
2) ATL中的CComAutoCriticalSection类,类的构造函数初始化临界区对象,析构函数释放临界区对象所使用的资源。
3) STL中的basic_ifstream,basic_ofstream等都在析构函数时关闭文件句柄、释放资源。
三、 在Java中利用Finally子句
使用Java语言,程序员确实可以不考虑内存泄露、内存溢出等头痛的问题了。但是对于在Java编程中使用的资源,如文件、数据库连接、网络连接等,还是需要程序员显式的进行释放资源。释放资源所使用的方法不再是析构函数,而是try、finally语句。
由于内存模型的区别,在Java中没有提供C++析构函数的对等体,而提供了finalize方法。但是finalize方法仅在GC运行时才会被调用。此时可能距对象应该释放该资源已经很长时间了。导致了资源的无必要占用,降低了程序的可靠性和效率。也就是说用finalize方法来释放资源的时机是不确定的,而我们需要一种确定性的方式来释放资源。所以不能依赖于finalize方法来为我们完成释放资源的工作。
但是Java提供了try、finall语法结构,来完成确定性资源回收。语言保证finally子句始终会被执行,无论try块因为异常退出还是正常退出。(有点儿象SHE的__try、__finally语法。)finally子句和C++的析构函数有相似的作用。
同样实现上面所描述的例子,代码如下:
void f(String fn) {
FileReader fr = new FileReader( fn ) ;
try {
// use file
use_file ( fr ) ;
} finally {
try {
if ( fr )
fr.close() ;
}
catch( IOException ) {}
}
}
注意finally子句内,也使用了try,catch语句。请考虑如下情况:
① 如果函数f使用了两个资源:resource1、resource2。
② 如果在释放第一个资源即resource1时发生了异常。
③ 那么就导致了释放resource2的代码不能够执行。
所以要保证第二个资源resource2能够释放,就必须保证释放第一个资源resource1时,不抛出异常。无独有偶,C++也要求在析构函数中不能够抛出异常,具体原因请参见[More Effective C++的条款11]。
四、下次讨论.NET中的Dispose模式、Using语句以及c++/cli中的确定性资源回收。