物件導向程式設計(C++篇3)——析構

charlee44發表於2022-03-16

1. 概述

類的解構函式執行與建構函式相反的操作,當物件結束其生命週期,程式就會自動執行解構函式:

class ImageEx
{
public:
    ImageEx()
    {
        cout << "Execute the constructor!" << endl;
    }

    ~ImageEx()
    {
        cout << "Execute the destructor!" << endl;
    }
};

int main()
{
    ImageEx imageEx;
    return 0;
}

那麼同樣的問題來了,為什麼要有解構函式呢?

2. 詳論

2.1. 物件生命週期

在經典C++中,需要通過new/delete來手動管理動態記憶體。如果我們在類中申請一個動態陣列,並且通過自定義的函式Release()來釋放它:

class ImageEx
{
public:
    ImageEx()
    {
        cout << "Execute the constructor!" << endl;
        data = new unsigned char[10];
    }

    ~ImageEx()
    {
        cout << "Execute the destructor!" << endl;
    }

    void Release()
    {
        delete[] data;
        data = nullptr;
    }

private:
    unsigned char * data;
};

int main()
{
    {
        ImageEx imageEx;
        imageEx.Release();
    }

    return 0;
}

那麼,當類物件離開作用域,結束生命週期之前,就必須顯示呼叫一次成員函式Release(),否則就會造成記憶體洩漏:物件在呼叫解構函式之後,只會銷燬資料成員data本身,而不是其指向的記憶體。

那麼,一個合理的實現是,將成員函式Release()放入到解構函式:

class ImageEx
{
public:
    ImageEx()
    {
        cout << "Execute the constructor!" << endl;
        data = new unsigned char[10];
    }

    ~ImageEx()
    {
        Release();
        cout << "Execute the destructor!" << endl;
    }

private:
    unsigned char * data;

    void Release()
    {
        delete[] data;
        data = nullptr;
    }
};

int main()
{
    {
        ImageEx imageEx;       
    }

    return 0;
}

這樣,當類物件離開作用域,結束生命週期之前,就自動通過解構函式,實現了動態陣列的釋放。好處是顯而易見的:實現了類似於內建資料型別物件的生命週期管理,我們可以像使用內建資料型別物件一樣使用類物件。這也體現了前文《物件導向程式設計(C++篇1)——引言》中提到的設計原則:類是抽象的自定義資料型別。

2.2. 不一定需要顯式析構

在一些現代高階程式語言(C#、Java、Javascript)中,已經不用去手動管理動態記憶體,取而代之的,是其與作業系統的中介軟體(.net,jvm,瀏覽器)的GC(垃圾回收)機制。而在現代C++中,提倡通過智慧指標(std::shared_ptr、std::unique_ptr、std::weak_ptr)來管理動態記憶體;對於動態陣列,則使用標準容器std::vector則更好。在兩者的內部都實現了前文提到的物件生命週期管理,在離開作用域後,通過解構函式自動釋放管理的記憶體,無需再手動進行回收。

那麼,一個顯而易見的推論就出來了,如果我們在類中使用智慧指標或者vector容器來替代new/delete管理動態記憶體,是不是就可以不用解構函式了?嚴格來說,是不用顯式使用解構函式:

class ImageEx
{
public:
    ImageEx():
        data(10)
    {
        cout << "Execute the constructor!" << endl;        
    }

private:
    std::vector<unsigned char> data;
};

int main()
{
    ImageEx imageEx;      

    return 0;
}

實際上,並不是這個類不存在解構函式,而是編譯器會為它生成一個合成的解構函式,在這個解構函式體中,什麼也不用做。因為類中的動態記憶體,已經交由std::vector容器來管理。當類物件離開作用域呼叫解構函式之後,會銷燬這個std::vector容器資料成員,進而觸發其解構函式,釋放其管理的記憶體。

2.3. 析構的必要性

根據上一節內容,不一定需要顯式析構。因為現代C++的一些機制能夠幫你自動管理動態記憶體。但是解構函式還是必要的,這是由於C++語言本身的性質決定的。作為C語言大部分內容的超集,需要相容C語言手動管理記憶體的特性。更重要的是,現代作業系統幾乎全部由C語言編寫,與底層的互動不可避免的需要手動使用動態記憶體管理。

3. 總結

所以我們就能理解了,C++這門語言的設計哲學就是就是這樣:既想要C語言的高效能,也想要高階語言高度抽象的特性。如果我們必須相容C語言底層設計,那我們最好使用解構函式釋放動態記憶體;否則多數情況下,我們應該使用智慧指標或者stl容器來管理動態記憶體,從而避免顯示使用解構函式。

上一篇
目錄
下一篇

相關文章