1. 程式人生 > >C++ 構造/解構函式中的異常處理

C++ 構造/解構函式中的異常處理

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 函式,用來清理該類在堆上的資源。這麼做的好處:

  1. 當建構函式中基於某種原因丟擲異常時,手動把資源釋放,避免記憶體洩露。
  2. 丟擲一個空的異常,通知外圍的程式,TestC構造失敗了。

解構函式中的異常

解構函式的作用是釋放資源,如果某一行程式碼丟擲了異常,後面的程式碼將得不到執行,造成記憶體洩露。

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

總結

看似這個問題簡單,很容易得到解決。然而,實際開發中面臨的情況會比上面複雜(惡劣)的多,比如 10 個指標,只完成了 3 個指標的初始化,某個指標的一個操作引發了異常。即便我們有 cleanup() 函式,因為其他指標沒有得到任何初始化(隨機值),在 delete 的時候一樣會程式崩潰。

我相信沒有一個解決此類的問題的通用方案。但是,我們可以用一些原則來避免出現問題:

  1. 建構函式/解構函式應該保持簡單,只完成成員的初始化和釋放資源,不要夾雜其它無關操作。
  2. 建構函式/解構函式應該在內部處理掉異常,不要依靠外圍程式來處理異常。
  3. 避免在建構函式中丟擲異常,禁止在解構函式中丟擲異常。
  4. 如果可以的話,可以把複雜的操作封裝成函式,在構造/解構函式中直接呼叫。比如建構函式僅僅完成基本的初始化(指標賦空等),用init()來做實際的初始化,用cleanup()做資源釋放,解構函式只調用。

其實核心思想就是保持程式邏輯的簡單即可,如果你的設計足夠合理,那麼就不會面臨這種問題。