各位 C++ 开发人员能否给我们一个关于什么是 RAII 的很好的描述,为什么它很重要,以及它是否可能与其他语言有任何相关性?
我知道一点。我相信它代表“资源获取即初始化”。但是,该名称与我对 RAII 是什么(可能不正确)的理解不一致:我的印象是 RAII 是一种在堆栈上初始化对象的方法,这样,当这些变量超出范围时,析构函数将自动被调用导致资源被清理。
那么为什么不叫“使用堆栈触发清理”(UTSTTC:)?你如何从那里到“RAII”?
你怎么能在堆栈上做一些东西来清理堆上的东西?另外,是否存在无法使用 RAII 的情况?你有没有发现自己希望垃圾收集?至少有一个垃圾收集器可以用于某些对象同时让其他对象受到管理?
谢谢。
最佳答案
So why isn't that called "using the stack to trigger cleanup" (UTSTTC:)?
RAII 告诉您该做什么:在构造函数中获取您的资源!我想补充一点:一种资源,一种构造函数。 UTSTTC 只是其中的一种应用,RAII 则更多。
资源管理很烂。 在这里,资源是使用后需要清理的任何东西。跨多个平台的项目研究表明,大多数错误与资源管理有关 - 在 Windows 上尤其糟糕(由于对象和分配器的多种类型)。
在 C++ 中,由于异常和(C++ 风格)模板的组合,资源管理特别复杂。如需了解引擎盖下的一瞥,请参阅 GOTW8 )。
C++ 保证析构函数被调用 当且仅当 构造函数成功了。依靠这一点,RAII 可以解决许多普通程序员甚至可能不知道的棘手问题。这里有一些超出“每当我返回时我的局部变量将被销毁”的例子。
让我们从一个过于简单的
FileHandle
开始使用 RAII 的类:class FileHandle
{
FILE* file;
public:
explicit FileHandle(const char* name)
{
file = fopen(name);
if (!file)
{
throw "MAYDAY! MAYDAY";
}
}
~FileHandle()
{
// The only reason we are checking the file pointer for validity
// is because it might have been moved (see below).
// It is NOT needed to check against a failed constructor,
// because the destructor is NEVER executed when the constructor fails!
if (file)
{
fclose(file);
}
}
// The following technicalities can be skipped on the first read.
// They are not crucial to understanding the basic idea of RAII.
// However, if you plan to implement your own RAII classes,
// it is absolutely essential that you read on :)
// It does not make sense to copy a file handle,
// hence we disallow the otherwise implicitly generated copy operations.
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// The following operations enable transfer of ownership
// and require compiler support for rvalue references, a C++0x feature.
// Essentially, a resource is "moved" from one object to another.
FileHandle(FileHandle&& that)
{
file = that.file;
that.file = 0;
}
FileHandle& operator=(FileHandle&& that)
{
file = that.file;
that.file = 0;
return *this;
}
}
如果构造失败(有异常(exception)),则不会调用其他成员函数 - 甚至析构函数 - 不会被调用。
RAII 避免使用处于无效状态的对象。 在我们使用对象之前,它已经让生活变得更轻松。
现在,让我们看看临时对象:
void CopyFileData(FileHandle source, FileHandle dest);
void Foo()
{
CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest"));
}
三种错误情况需要处理:无法打开文件、只能打开一个文件、两个文件都可以打开但复制文件失败。在非 RAII 实现中,
Foo
必须明确处理所有三种情况。RAII 会释放已获取的资源,即使在一个语句中获取了多个资源。
现在,让我们聚合一些对象:
class Logger
{
FileHandle original, duplex; // this logger can write to two files at once!
public:
Logger(const char* filename1, const char* filename2)
: original(filename1), duplex(filename2)
{
if (!filewrite_duplex(original, duplex, "New Session"))
throw "Ugh damn!";
}
}
Logger
的构造函数如果 original
会失败的构造函数失败(因为 filename1
无法打开),duplex
的构造函数失败(因为 filename2
无法打开),或写入 Logger
中的文件的构造函数体失败。在任何这些情况下,Logger
的析构函数不会被调用 - 所以我们不能依赖 Logger
的析构函数来释放文件。但如果 original
被构造后,它的析构函数将在 Logger
的清理过程中被调用。构造函数。RAII 简化了部分构建后的清理工作。
负分:
负分?所有问题都可以用 RAII 和智能指针解决 ;-)
当您需要延迟获取,将聚合对象推送到堆上时,RAII 有时很笨拙。
想象一下 Logger 需要一个
SetTargetFile(const char* target)
.在那种情况下,句柄仍然需要是 Logger
的成员。 , 需要驻留在堆上(例如在智能指针中,以适本地触发句柄的销毁。)我从来没有真正希望垃圾收集。当我使用 C# 时,我有时会感到片刻的幸福,我只是不需要在意,但更多的是我想念所有可以通过确定性破坏创建的很酷的玩具。 (使用
IDisposable
只是不会削减它。)我有一个特别复杂的结构,它可能从 GC 中受益,其中“简单”的智能指针会导致对多个类的循环引用。我们通过仔细平衡强指针和弱指针来蒙混过关,但无论何时我们想要改变某些东西,我们都必须研究一个大的关系图。 GC 可能会更好,但一些组件持有应该尽快释放的资源。
关于 FileHandle 示例的说明:它并不打算完整,只是一个示例 - 但结果不正确。感谢 Johannes Schaub 指出并感谢 FredOverflow 将其转化为正确的 C++0x 解决方案。随着时间的推移,我已经接受了这种方法 documented here .
关于c++ - 理解术语和概念的含义——RAII(Resource Acquisition is Initialization),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/712639/