面向對象編程(C++篇3)——析構
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容器來管理動態記憶體,從而避免顯示使用析構函數。