C++ 为什么会引入(需要)异常?
The C++ Programming Language: 一个库的作者可以检测出发生了运行时错误,但一般不知道怎样去处理它们(因为和用户具体的应用有关);另一方面,库的用户知道怎样处理这些错误,但却无法检查它们何时发生(如果能检测,就可以再用户的代码里处理了,不用留给库去发现)。
C++ primer: Exceptions let us separate problem detection from problem resolution(错误检测和错误处理分离开).
构造函数中的异常
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构造失败了。
析构函数中的异常
析构函数的作用是释放资源,如果某一行代码抛出了异常,后面的代码将得不到执行,造成内存泄露。
详细可以去看 Effective C++ item 08: Prevent exceptions from leaving destructors.
总结
看似这个问题简单,很容易得到解决。然而,实际开发中面临的情况会比上面复杂(恶劣)的多,比如 10 个指针,只完成了 3 个指针的初始化,某个指针的一个操作引发了异常。即便我们有 cleanup() 函数,因为其他指针没有得到任何初始化(随机值),在 delete 的时候一样会程序崩溃。
我相信没有一个解决此类的问题的通用方案。但是,我们可以用一些原则来避免出现问题:
- 构造函数/析构函数应该保持简单,只完成成员的初始化和释放资源,不要夹杂其它无关操作。
- 构造函数/析构函数应该在内部处理掉异常,不要依靠外围程序来处理异常。
- 避免在构造函数中抛出异常,禁止在析构函数中抛出异常。
- 如果可以的话,可以把复杂的操作封装成函数,在构造/析构函数中直接调用。比如构造函数仅仅完成基本的初始化(指针赋空等),用init()来做实际的初始化,用cleanup()做资源释放,析构函数只调用。
其实核心思想就是保持程序逻辑的简单即可,如果你的设计足够合理,那么就不会面临这种问题。
原文地址:http://www.perfect-is-shit.com/exception-handle-in-constructor-destructor-.html