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 函数,用来清理该类在堆上的资源。这么做的好处:

  1. 当构造函数中基于某种原因抛出异常时,手动把资源释放,避免内存泄露。
  2. 抛出一个空的异常,通知外围的程序,TestC构造失败了。

2. 析构函数中的异常

析构函数的作用是释放资源,如果某一行代码抛出了异常,后面的代码将得不到执行,造成内存泄露。

详细可以去看 Effective C++ item 08: Prevent exceptions from leaving destructors.

3. 总结

看似这个问题简单,很容易得到解决。然而,实际开发中面临的情况会比上面复杂(恶劣)的多,比如 10 个指针,只完成了 3 个指针的初始化,某个指针的一个操作引发了异常。即便我们有 cleanup() 函数,因为其他指针没有得到任何初始化(随机值),在 delete 的时候一样会程序崩溃。

我相信没有一个解决此类的问题的通用方案。但是,我们可以用一些原则来避免出现问题:

  1. 构造函数/析构函数应该保持简单,只完成成员的初始化和释放资源,不要夹杂其它无关操作。
  2. 构造函数/析构函数应该在内部处理掉异常,不要依靠外围程序来处理异常。
  3. 避免在构造函数中抛出异常,禁止在析构函数中抛出异常。
  4. 如果可以的话,可以把复杂的操作封装成函数,在构造/析构函数中直接调用。比如构造函数仅仅完成基本的初始化(指针赋空等),用init()来做实际的初始化,用cleanup()做资源释放,析构函数只调用。

其实核心思想就是保持程序逻辑的简单即可,如果你的设计足够合理,那么就不会面临这种问题。

First created: 2013-12-22 00:00:00
Last updated: 2022-12-11 Sun 12:49
Power by Emacs 27.1 (Org mode 9.4.4)