各位 C++ 开发人员能否给我们一个好的描述 RAII 是什么,为什么它很重要,以及它是否可能与其他语言有任何关系?
我知道一点。我相信它代表“资源获取就是初始化”。然而,这个名字与我(可能不正确)对 RAII 的理解不符:我的印象是 RAII 是一种在堆栈上初始化对象的方式,这样,当这些变量超出范围时,析构函数将自动被调用导致资源被清理。
那么为什么不称为“使用堆栈触发清理”(UTSTTC:)?你如何从那里到达“RAII”?
你怎么能在堆栈上做一些东西来清理堆上的东西呢?另外,是否存在不能使用 RAII 的情况?您是否曾经发现自己希望进行垃圾收集?至少一个垃圾收集器可以用于某些对象,同时让其他对象得到管理?
谢谢。
:(
那时我正在阅读整个线程,甚至不认为自己是 C++ 新手!
那么为什么不称为“使用堆栈触发清理”(UTSTTC:)?
RAII 告诉您该做什么:在构造函数中获取您的资源!我要补充:一种资源,一种构造函数。 UTSTTC 只是其中的一种应用,RAII 更多。
资源管理很烂。在这里,资源是使用后需要清理的任何东西。对跨平台项目的研究表明,大多数错误都与资源管理有关——在 Windows 上尤其严重(由于对象和分配器的类型很多)。
在 C++ 中,由于异常和(C++ 风格)模板的结合,资源管理特别复杂。如需深入了解,请参阅 GOTW8)。
C++ 保证当且仅当构造函数成功时才调用析构函数。依靠这一点,RAII 可以解决许多普通程序员甚至可能没有意识到的棘手问题。除了“我的局部变量将在我返回时被销毁”之外,这里还有一些示例。
让我们从一个使用 RAII 的过于简单的 FileHandle
类开始:
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;
}
}
如果构造失败(有一个例外),则不会调用其他成员函数——甚至析构函数——也不会被调用。
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!";
}
}
如果 original
的构造函数失败(因为无法打开 filename1
)、duplex
的构造函数失败(因为无法打开 filename2
)或写入文件,Logger
的构造函数将失败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。
那里有很好的答案,所以我只是添加一些忘记的东西。
0. RAII 是关于范围的
RAII 是关于两者:
在构造函数中获取资源(无论是什么资源),并在析构函数中取消获取它。在声明变量时执行构造函数,并在变量超出范围时自动执行析构函数。
其他人已经回答了,所以我不会详细说明。
1. 使用 Java 或 C# 编码时,您已经使用 RAII...
乔丹先生:什么!当我说,“妮可,给我拖鞋,给我睡帽,”那是散文吗?哲学大师:是的,先生。 MONSIEUR JOURDAIN:四十多年来,我一直在说散文,但我对此一无所知,我非常感谢你教会了我这一点。 ——莫里哀:中产阶级绅士,第 2 幕,第 4 场
正如 Jourdain 先生对散文所做的那样,C# 甚至 Java 人已经在使用 RAII,但是以隐藏的方式。例如,以下 Java 代码(在 C# 中以相同的方式编写,将 synchronized
替换为 lock
):
void foo()
{
// etc.
synchronized(someObject)
{
// if something throws here, the lock on someObject will
// be unlocked
}
// etc.
}
...已经在使用RAII:互斥量获取在关键字(synchronized
或lock
)中完成,退出范围时将完成取消获取。
它的符号非常自然,即使对于从未听说过 RAII 的人来说也几乎不需要解释。
在这里,C++ 相对于 Java 和 C# 的优势是可以使用 RAII 制作任何东西。例如,在 C++ 中没有 synchronized
或 lock
的直接内置等效项,但我们仍然可以拥有它们。
在 C++ 中,它会这样写:
void foo()
{
// etc.
{
Lock lock(someObject) ; // lock is an object of type Lock whose
// constructor acquires a mutex on
// someObject and whose destructor will
// un-acquire it
// if something throws here, the lock on someObject will
// be unlocked
}
// etc.
}
可以很容易地用 Java/C# 方式编写(使用 C++ 宏):
void foo()
{
// etc.
LOCK(someObject)
{
// if something throws here, the lock on someObject will
// be unlocked
}
// etc.
}
2. RAII 有其他用途
白兔:[唱歌]我迟到了/我迟到了/为了一个非常重要的约会。 / 没时间说“你好”。 / 再见。 / 我来晚了,我来晚了,我来晚了。 ——爱丽丝梦游仙境(迪士尼版,1951)
你知道构造函数什么时候被调用(在对象声明处),你也知道它对应的析构函数什么时候被调用(在作用域的出口处),所以你可以只用一行代码写出几乎神奇的代码。欢迎来到 C++ 仙境(至少,从 C++ 开发人员的角度来看)。
例如,您可以编写一个计数器对象(我将其作为练习)并通过声明其变量来使用它,就像上面使用的锁定对象一样:
void foo()
{
double timeElapsed = 0 ;
{
Counter counter(timeElapsed) ;
// do something lengthy
}
// now, the timeElapsed variable contain the time elapsed
// from the Counter's declaration till the scope exit
}
当然,可以再次使用宏以 Java/C# 方式编写:
void foo()
{
double timeElapsed = 0 ;
COUNTER(timeElapsed)
{
// do something lengthy
}
// now, the timeElapsed variable contain the time elapsed
// from the Counter's declaration till the scope exit
}
3.为什么C++最后缺少?
[呼喊] 这是最后的倒计时! — 欧洲:最后的倒计时(对不起,我没有报价,在这里... :-)
finally
子句在 C#/Java 中用于在范围退出(通过 return
或引发的异常)的情况下处理资源处置。
精明的规范读者会注意到 C++ 没有 finally 子句。这不是错误,因为 C++ 不需要它,因为 RAII 已经处理资源处理。 (相信我,编写 C++ 析构函数比编写正确的 Java finally 子句甚至 C# 的正确 Dispose 方法要容易得多)。
不过,有时,finally
子句会很酷。我们可以在 C++ 中做到这一点吗? Yes, we can! 再次使用 RAII。
结论:RAII 不仅仅是 C++ 中的哲学:它是 C++
雷伊?这是C++!!! —— C++ 开发者的愤怒评论,被一个不起眼的斯巴达国王和他的 300 位朋友无耻地抄袭
当您在 C++ 方面达到一定程度的经验时,您会开始考虑 RAII,即构造函数和析构函数的自动执行。
您开始考虑 范围,{
和 }
字符成为代码中最重要的字符。
几乎所有东西都适合 RAII:异常安全、互斥体、数据库连接、数据库请求、服务器连接、时钟、操作系统句柄等,最后但并非最不重要的是内存。
数据库部分也不容小觑,因为如果你愿意付出代价,你甚至可以写成“事务性编程”的风格,执行一行又一行的代码,直到最后决定,如果您希望提交所有更改,或者,如果不可能,将所有更改还原(只要每行至少满足强异常保证)。 (有关事务性编程,请参阅此 Herb's Sutter article 的第二部分)。
就像一个谜题,一切都合适。
RAII 是 C++ 的重要组成部分,没有它,C++ 就不可能是 C++。
这就解释了为什么有经验的 C++ 开发人员如此迷恋 RAII,以及为什么 RAII 是他们在尝试另一种语言时首先搜索的东西。
它还解释了为什么垃圾收集器虽然本身就是一项了不起的技术,但从 C++ 开发人员的角度来看并没有那么令人印象深刻:
RAII 已经处理了 GC 处理的大部分案件
GC 比 RAII 更好地处理纯托管对象的循环引用(通过智能使用弱指针来缓解)
仍然 GC 仅限于内存,而 RAII 可以处理任何类型的资源。
如上所述,RAII 可以做的很多很多……
RAII 使用 C++ 析构函数语义来管理资源。例如,考虑一个智能指针。你有一个指针的参数化构造函数,它用对象的地址初始化这个指针。您在堆栈上分配一个指针:
SmartPointer pointer( new ObjectClass() );
当智能指针超出范围时,指针类的析构函数会删除连接的对象。指针是堆栈分配的,对象是堆分配的。
在某些情况下,RAII 没有帮助。例如,如果您使用引用计数智能指针(如 boost::shared_ptr)并创建一个带有循环的类图结构,您将面临内存泄漏的风险,因为循环中的对象会阻止彼此被释放。垃圾收集将有助于解决这个问题。
我想把它比以前的回答更强烈一些。
RAII,Resource Acquisition Is Initialization表示所有获取的资源都应该在对象初始化的上下文中获取。这禁止“裸”资源获取。基本原理是 C++ 中的清理工作基于对象,而不是基于函数调用。因此,所有清理都应该由对象完成,而不是函数调用。从这个意义上说,C++ 比 Java 更面向对象。 Java 清理基于 finally
子句中的函数调用。
我同意cpitis。但想补充一点,资源可以是任何东西,而不仅仅是内存。资源可以是文件、临界区、线程或数据库连接。
之所以称为资源获取即初始化,是因为在构造控制资源的对象时获取资源,如果构造函数失败(即由于异常),则不获取资源。然后,一旦对象超出范围,资源就会被释放。 c++保证栈上所有构造成功的对象都会被销毁(这包括基类和成员的构造函数,即使超类构造函数失败)。
RAII 背后的合理性是使资源获取异常安全。无论哪里发生异常,所有获取的资源都会被正确释放。然而,这确实依赖于获取资源的类的质量(这必须是异常安全的,这很难)。
垃圾收集的问题在于您失去了对 RAII 至关重要的确定性破坏。一旦变量超出范围,则由垃圾收集器决定何时回收该对象。对象持有的资源将继续持有,直到调用析构函数。
RAII 来自 Resource Allocation Is Initialization。基本上,这意味着当构造函数完成执行时,构造的对象已完全初始化并可以使用。它还暗示析构函数将释放对象拥有的任何资源(例如内存、操作系统资源)。
与垃圾收集语言/技术(例如 Java、.NET)相比,C++ 允许完全控制对象的生命周期。对于堆栈分配的对象,您将知道何时调用对象的析构函数(当执行超出范围时),这在垃圾收集的情况下不受真正控制。即使在 C++ 中使用智能指针(例如 boost::shared_ptr),您也会知道当没有对指向对象的引用时,将调用该对象的析构函数。
你怎么能在堆栈上做一些东西来清理堆上的东西呢?
class int_buffer
{
size_t m_size;
int * m_buf;
public:
int_buffer( size_t size )
: m_size( size ), m_buf( 0 )
{
if( m_size > 0 )
m_buf = new int[m_size]; // will throw on failure by default
}
~int_buffer()
{
delete[] m_buf;
}
/* ...rest of class implementation...*/
};
void foo()
{
int_buffer ib(20); // creates a buffer of 20 bytes
std::cout << ib.size() << std::endl;
} // here the destructor is called automatically even if an exception is thrown and the memory ib held is freed.
当 int_buffer 的实例存在时,它必须具有大小,并且它将分配必要的内存。当它超出范围时,它的析构函数被调用。这对于诸如同步对象之类的东西非常有用。考虑
class mutex
{
// ...
take();
release();
class mutex::sentry
{
mutex & mm;
public:
sentry( mutex & m ) : mm(m)
{
mm.take();
}
~sentry()
{
mm.release();
}
}; // mutex::sentry;
};
mutex m;
int getSomeValue()
{
mutex::sentry ms( m ); // blocks here until the mutex is taken
return 0;
} // the mutex is released in the destructor call here.
另外,是否存在不能使用 RAII 的情况?
不,不是。
您是否曾经发现自己希望进行垃圾收集?至少一个垃圾收集器可以用于某些对象,同时让其他对象得到管理?
绝不。垃圾收集只解决了动态资源管理的一小部分。
这里已经有很多很好的答案,但我想补充一下:对 RAII 的简单解释是,在 C++ 中,分配在堆栈上的对象在超出范围时被销毁。这意味着,将调用对象析构函数并可以进行所有必要的清理。这意味着,如果创建的对象没有“new”,则不需要“delete”。这也是“智能指针”背后的理念——它们驻留在堆栈上,并且本质上包装了一个基于堆的对象。
RAII 是 Resource Acquisition Is Initialization 的首字母缩写词。
这种技术对于 C++ 非常独特,因为它们支持构造函数和析构函数,并且几乎自动支持与传入的参数匹配的构造函数,或者在最坏的情况下调用默认构造函数,如果显式提供则调用析构函数,否则调用默认构造函数如果您没有为 C++ 类显式编写析构函数,则调用由 C++ 编译器添加的。这只发生在自动管理的 C++ 对象上——这意味着不使用空闲存储(使用 new,new[]/delete,delete[] C++ 运算符分配/释放的内存)。
RAII 技术利用这种自动管理的对象特性来处理在堆/空闲存储上创建的对象,方法是使用 new/new[] 显式请求更多内存,应该通过调用 delete/delete[] 显式销毁这些对象.自动管理对象的类将包装在堆/空闲存储内存上创建的另一个对象。因此,当运行自动管理对象的构造函数时,将在堆/空闲存储内存上创建包装对象,并且当自动管理对象的句柄超出范围时,自动调用该自动管理对象的析构函数,其中包装对象使用 delete 销毁对象。使用 OOP 概念,如果您将此类对象包装在私有范围内的另一个类中,您将无法访问包装的类成员和方法,这就是智能指针(又名句柄类)的设计目的。这些智能指针将包装的对象作为类型化对象暴露给外部世界,并允许调用暴露的内存对象组成的任何成员/方法。请注意,智能指针根据不同的需求有不同的风格。您应该参考 Andrei Alexandrescu 的 Modern C++ Programming 或 boost 库的 (www.boostorg) shared_ptr.hpp implementation/documentation 以了解更多信息。希望这可以帮助您了解 RAII。
不定期副业成功案例分享
Foo
拥有Bar
,并且Boz
对其进行了变异,...