CppCoreGuidelines E.6 用 RAII 避免内存泄漏
28 July 2023
“Use RAII to prevent leaks”
理由
内存泄漏肯定不行。手工管理释放内存资源很容易出错。RAII(Resource Acquisition Is Initialization) 是避免内存泄漏的最简单、最系统化的方式。
例子
void f1(int i) // Bad: possible leak { int* p = new int[12]; // ... if (i < 17) throw Bad{"in f()", i}; // ... }
我们可以在抛出异常之前小心地释放内存资源。
void f2(int i) // Clumsy and error-prone: explicit release { int* p = new int[12]; // ... if (i < 17) { delete[] p; throw Bad{"in f()", i}; } // ... }
但是,这样操作很繁琐。代码量大的时候,在多处可能跑出异常的地方显式地释放资源,既重复是劳动,又容易出错。
void f3(int i) // OK: resource management done by a handle (but see below) { auto p = make_unique<int[]>(12); // ... if (i < 17) throw Bad{"in f()", i}; // ... }
注意
这个方法在隐式地抛出异常的情况下也能正常工作,因为清理工作在被调用函数中处理掉了。
void f4(int i) // OK: resource management done by a handle (but see below) { auto p = make_unique<int[]>(12); // ... helper(i); // might throw // ... }
除非你需要指针语义,不然的话,就用局部的资源对象:
void f5(int i) // OK: resource management done by local object { vector<int> v(12); // ... helper(i); // might throw // ... }
这样更简单、更安全、更高效率。
注意
如果没有明显的资源句柄,或者很难定义合适的 RAII 对象/句柄,最后的办法是通过某个 final_action
对象清理内存资源。
注意
如果我们写的程序不能执行异常怎么办?首先,我们要去质疑一下,为什么?是不是有一些反异常操作的迷信?我们只知道很少一些情况不能用异常:
- 我们的系统很小,支持异常操作会吃掉 2K 内存的大部分。
- 我们使用硬实时系统,没有工具可以确保在规定的时间内处理异常。
- 我们的系统里有大量旧代码,用了大量难以理解的指针,所以异常会导致内存泄漏。
- 我们用的 C++ 版本中的异常处理机制性能很糟糕(慢、耗内存、没法和动态链接库兼容)。请像提供商提议,如果没有用户提议,不会有改进发生。
- 如果我们质疑经理的古老智慧,我们会被炒鱿鱼。
这些理由中,只有第一条是很根本的,所以,只要有可能,尽量通过 RAII 使用异常机制,或者设计你的 RAII 对象,确保永远不会出错。如果没法使用异常机制,就模拟出 RAII 操作。就是在对象创建之后,先检查对象是否有效,然后在析构函数中释放所有资源。其中一个策略是给每一个资源句柄添加一个 valid()
操作。
void f() { vector<string> vs(100); // not std::vector: valid() added if (!vs.valid()) { // handle error or exit } ifstream fs("foo"); // not std::ifstream: valid() added if (!fs.valid()) { // handle error or exit } // ... } // destructors clean up as usual
很明显,这样会增加代码长度,不支持隐式的异常传导,并且也很容易忘记添加
valid()
检查。所以,还是首选使用异常机制。
请查看:使用 noexcept 规则