C++ 對象是怎麼退出的?進程篇

 要說 C++ 對象是怎麼死的,得先從 C++ 的析構函數說起。這玩意兒是我本人很喜歡的一個語言特性(可惜有好幾個語言沒有類似的玩意兒,具體就不點名了,免得引發口水戰)。我們可以利用 C++ 的構造和析構函數,來實現 Guard 模式,寫出比較清晰、簡練和異常安全的代碼。由於 Guard 模式在 C++ 程序中運用挺多,所以保證【

所有對象被析構】就是一個很重要很嚴肅的問題。由於前幾天的帖子聊了架構設計的多進程問題,所以今天想起來要聊一下與“C++進程終止”相關的那些事。與前幾個 C++ 帖子的風格類似,今天聊的內容,儘量侷限於標準 C++ 範疇,儘量不涉及特定的操作系統平臺。

★關於進程的三種死法

由於今天講的是“進程篇”,自然得先搞明白進程的幾種死法。其實進程和大活人一樣,也有三種死法,分別是“自然死亡、自殺、它殺”。這三種死亡方式具體如下:

◇自然死亡

望文生義,自然死亡就是最自然的進程退出方法。具體表現為通過return語句結束main函數。由於這種方法最優雅(後面會說),如果沒有其它特殊原因,強烈建議採用這種死法。

◇自殺

所謂的自殺,就是進程自己調用某些 API 來自行了斷。在標準 C++ 中,這幾個函數(exit、abort、terminate、unexpected)可以用於進程自殺。如果沒有額外設置,unexpected 函數默認會調用 terminate 函數,terminate 函數默認會調用 abort 函數。所以自殺的方式基本上也就是 exit 和 abort 兩種。exit 相對 abort 來說溫和一些,所以下文稱 exit 為

溫和自殺;相對地,把 abort 稱為激進自殺

◇它殺

它殺其實也挺好理解,就是當前進程被其它進程殺死。標準 C++ 沒有提供用於它殺的 API 函數,因此常用的方法是通過某些跨平臺的庫(如 ACE)提供的 API 函數或者調用某些外部命令(如 Posix 系統的 kill 命令)來實現。

上面說了這幾種死法,有同學要問了:進程不同的死法和 C++ 對象有什麼關係捏?其實關係大大滴,請聽我細細道來。

★類對象的析構(銷燬)

首先把類對象分為三種:局部非靜態對象、局部靜態對象、非局部對象(出於習慣,以下簡稱全局對象)。對於尚不清楚這幾種對象差異的同學,請先找本 C++ 入門書拜讀一下。

進程不同的死法對於這幾種對象是否能銷燬會有很大的影響。請看如下的對照表:

  1. ---------------------------------
  2. | | 局部非靜態對象 | 局部靜態對象 | 全局對象 |
  3. |自然死亡 | 能 |  能 |  能 |
  4. |溫和自殺 | 不能 |  能 |  能 |
  5. |激進自殺 | 不能 |  不能 |  不能 |
  6. |它殺 |  不能 |  不能 |  不能 |

---------------------------------

從這個對照表可以看出,激進自殺和它殺的效果類似(各種類對象都【無法】正常銷燬)。所以我們在寫程序時要極力避免上述這兩種情況。

另外,溫和自殺也有不爽之處:不能正確地銷燬局部非靜態對象。準確地說,應該是:在調用exit之前已經構造但是尚未析構的局部非靜態對象將再也不會被析構。所以溫和自殺也要避免使用。

綜上所述,最正經、最靠譜的死法就是第一種:自然死亡。

★析構的順序

那麼,是不是隻要讓進程自然死亡就萬事大吉了?非也!即使所有的類對象都會被析構,還有另一個棘手的問題:析構的順序。

先來看下面一個例子:

class CFoo

{

public:

CFoo()

{

cout << "CFoo" << endl;

}

virtual ~CFoo()

{

cout << "~CFoo" << endl;

}

};

上述示例挺簡單的(有效代碼僅6行),大夥兒能看出有什麼問題嗎?(如果你一眼就看出問題之所在,恭喜你——你的 C++ 水平夠高,後面的內容你不用看了)

對於用戶定義的全局對象,在 C++ 標準中並【沒有】規定它們構造和析構的先後順序;對於諸如標準輸入輸出流的 cout、cerr 等全局對象,在 C++ 03 標準中(27.4.2.1.6章節)有提及如何保證它們在最後析構。但由於某些老式編譯器並未完全遵照標準實現,導致標準輸入輸出流的幾個全局對象【有可能】被提前析構。

基於上述原因,假如 CFoo 類也定義了一個全局對象 g_foo。當 g_foo 析構的時候,cout 對象【有可能】已經先死了(取決於具體的環境,詳見《關於標準輸入輸出流的進一步探討》)。在這種情況下,CFoo 析構函數的打印語句由於引用了已死的對象,可能會導致【不可預料的後果】(比如進程崩潰)。

從上面的例子可以看出,如果你在程序中使用了全局對象或者靜態對象,那你要非常小心地編寫相關 class/struct 的析構函數代碼,儘量【不要】在它們的析構函數中引用其它的全局對象或靜態對象。當然啦,假如能避免使用全局對象和靜態對象,就更好了。

另外,在 C++ 經典名著《Modern C++ Design》的第6章詳細描述了關於單鍵/單例(Singleton)銷燬的一些細節、場景及解決方法。大夥兒可以去拜讀一下。


分享到:


相關文章: