C++ 构造/析构函数中的异常处理
Table of Contents
C++ 为什么会引入(需要)异常?
The C++ 编程语言: 一个库的作者可以检测出发生了运行时错误,但一般不知道怎样去处理它们(因为和用户具体的应用有关);另一方面,库的用户知道怎样处理这些错误,但却无法检查它们何时发生(如果能检测,就可以再用户的代码里处理了,不用留给库去发现)。
C++ primer: Exceptions let us separate problem detection from problem resolution(错误检测和错误处理分离开).
1. 构造函数中的异常
C++ 的构造函数没有返回值,使用异常来处理构造函数中的错误(或者其它)是一种很好的办法。但是一定在构造函数中使用异常一定要小心。
我们知道,当出现异常的时候,会调用类析构函数。然而,在构造函数中抛出异常的时候,不会去调用析构函数,此时如果处理不当,会出现内存泄露。
如下:
class TestA { public: TestA() { std::cout << "TestA Contructor" << std::endl; } ~TestA() { std::cout << "TestA Destructor" << std::endl; } }; class TestB { public: TestB() { std::cout << "TestB Constructor" << std::endl; } ~TestB() { std::cout << "TestB Destructor" << std::endl; } }; class TestC { public: TestC() { ta = new TestA(); tb = new TestB(); throw std::string("something trigger a exception"); std::cout << "TestC() Constructor" << std::endl; } ~TestC() { delete ta; delete tb; std::cout << "TestC() Destructor" << std::endl; } private: TestA* ta; TestB* tb; }; int main() { try { TestC tc; } catch (const std::string& exp) { std::cout << exp << std::endl; } }
输出:
TestA Contructor TestB Constructor something trigger a exception
ta 和 tb 内存泄露。如何避免这种问题呢?
class TestC { public: TestC() { try { ta = new TestA(); tb = new TestB(); throw std::string("something trigger a exception"); } catch(const std::string& exp) { std::cout << exp << std::endl; cleanup(); throw; } std::cout << "TestC() Constructor" << std::endl; } ~TestC() { cleanup(); std::cout << "TestC() Destructor" << std::endl; } void cleanup() { delete ta; ta = NULL; delete tb; tb = NULL; } private: TestA* ta; TestB* tb; }; int main() { try { TestC tc; } catch (...) { std::cout << "construtor failure." << std::endl; } }
输出:
TestA Contructor TestB Constructor something trigger a exception TestA Destructor TestB Destructor construtor failure.
新添加了一个 cleanup
函数,用来清理该类在堆上的资源。这么做的好处:
- 当构造函数中基于某种原因抛出异常时,手动把资源释放,避免内存泄露。
- 抛出一个空的异常,通知外围的程序,TestC构造失败了。
2. 析构函数中的异常
析构函数的作用是释放资源,如果某一行代码抛出了异常,后面的代码将得不到执行,造成内存泄露。
详细可以去看 Effective C++ item 08: Prevent exceptions from leaving destructors.
3. 总结
看似这个问题简单,很容易得到解决。然而,实际开发中面临的情况会比上面复杂(恶劣)的多,比如 10 个指针,只完成了 3 个指针的初始化,某个指针的一个操作引发了异常。即便我们有 cleanup() 函数,因为其他指针没有得到任何初始化(随机值),在 delete 的时候一样会程序崩溃。
我相信没有一个解决此类的问题的通用方案。但是,我们可以用一些原则来避免出现问题:
- 构造函数/析构函数应该保持简单,只完成成员的初始化和释放资源,不要夹杂其它无关操作。
- 构造函数/析构函数应该在内部处理掉异常,不要依靠外围程序来处理异常。
- 避免在构造函数中抛出异常,禁止在析构函数中抛出异常。
- 如果可以的话,可以把复杂的操作封装成函数,在构造/析构函数中直接调用。比如构造函数仅仅完成基本的初始化(指针赋空等),用init()来做实际的初始化,用cleanup()做资源释放,析构函数只调用。
其实核心思想就是保持程序逻辑的简单即可,如果你的设计足够合理,那么就不会面临这种问题。