C++ 编程/RAII
RAII 技术通常用于在多线程应用程序中控制线程锁。另一个典型的 RAII 示例是文件操作,例如 C++ 标准库中的文件流。输入文件流在对象的构造函数中打开,并在对象销毁时关闭。由于 C++ 允许在 堆栈 上分配对象,因此 C++ 的作用域机制可用于控制文件访问。
使用 RAII,我们可以使用类析构函数来保证清理,类似于其他语言中的 finally 关键字。这样做可以自动执行任务,从而避免错误,但也提供了不使用它的自由。
RAII 也用于(如以下示例所示)确保异常安全。RAII 使得在没有大量使用 try
/catch
块的情况下避免资源泄漏成为可能,并在软件行业中得到广泛使用。
动态分配内存(使用new)的拥有权可以使用 RAII 控制。为此,C++ 标准库定义了 auto ptr。此外,共享对象的生存期可以通过具有共享所有权语义的智能指针来管理,例如 C++ 中由 Boost 库 定义的 boost::shared_ptr
或 Loki 库 中基于策略的 Loki::SmartPtr
。
以下 RAII 类是对 C 标准库文件系统调用的轻量级包装器。
#include <cstdio>
// exceptions
class file_error { } ;
class open_error : public file_error { } ;
class close_error : public file_error { } ;
class write_error : public file_error { } ;
class file
{
public:
file( const char* filename )
:
m_file_handle(std::fopen(filename, "w+"))
{
if( m_file_handle == NULL )
{
throw open_error() ;
}
}
~file()
{
std::fclose(m_file_handle) ;
}
void write( const char* str )
{
if( std::fputs(str, m_file_handle) == EOF )
{
throw write_error() ;
}
}
void write( const char* buffer, std::size_t num_chars )
{
if( num_chars != 0
&&
std::fwrite(buffer, num_chars, 1, m_file_handle) == 0 )
{
throw write_error() ;
}
}
private:
std::FILE* m_file_handle ;
// copy and assignment not implemented; prevent their use by
// declaring private.
file( const file & ) ;
file & operator=( const file & ) ;
} ;
此 RAII 类可以按如下方式使用
void example_with_RAII()
{
// open file (acquire resource)
file logfile("logfile.txt") ;
logfile.write("hello logfile!") ;
// continue writing to logfile.txt ...
// logfile.txt will automatically be closed because logfile's
// destructor is always called when example_with_RAII() returns or
// throws an exception.
}
如果不使用 RAII,每个使用输出日志的函数都必须显式管理文件。例如,不使用 RAII 的等效实现如下
void example_without_RAII()
{
// open file
std::FILE* file_handle = std::fopen("logfile.txt", "w+") ;
if( file_handle == NULL )
{
throw open_error() ;
}
try
{
if( std::fputs("hello logfile!", file_handle) == EOF )
{
throw write_error() ;
}
// continue writing to logfile.txt ... do not return
// prematurely, as cleanup happens at the end of this function
}
catch(...)
{
// manually close logfile.txt
std::fclose(file_handle) ;
// re-throw the exception we just caught
throw ;
}
// manually close logfile.txt
std::fclose(file_handle) ;
}
如果 fopen()
和 fclose()
可能抛出异常,则 file
和 example_without_RAII()
的实现将变得更加复杂;但是,example_with_RAII()
不会受到影响。
RAII 习惯用法的主要思想是类 file
封装了任何有限资源的管理,例如 FILE*
文件句柄。它保证在函数退出时资源将被正确释放。此外,file
实例保证一个有效的日志文件可用(如果文件无法打开,则抛出异常)。
在存在异常的情况下,还有一个大问题:在 example_without_RAII()
中,如果分配了多个资源,但在它们的分配之间抛出异常,则没有普遍的方法可以知道在最终的 catch
块中需要释放哪些资源 - 并且释放未分配的资源通常是一件坏事。RAII 可以解决这个问题;自动变量以与它们构造相反的顺序销毁,并且只有在对象完全构造后(构造函数内部没有抛出异常)才销毁该对象。因此,example_without_RAII()
永远不可能像 example_with_RAII()
那样安全,除非对每种情况进行特殊编码,例如检查无效的默认值或嵌套 try-catch 块。事实上,应该注意的是,example_without_RAII()
在本文的早期版本中包含资源错误。
这使得 example_with_RAII()
不必像以前那样显式管理资源。当多个函数使用 file
时,这将简化和减少总体代码大小,并有助于确保程序正确性。
example_without_RAII()
类似于在非 RAII 语言(如 Java)中用于资源管理的习惯用法。虽然 Java 的 try-finally 块允许正确释放资源,但负担仍然落在程序员身上,以确保正确行为,因为每个使用 file
的函数都可能显式地要求使用 try-finally 块销毁日志文件。