淺談C++中的資源管理


C++的復雜是一個基本事實,這也成了很多人對C++橫加指責的原因。事實上,正如陳皓在“C++的數組不支持多態”?這篇文章中提到的,很多人在並不真正了解C++的情況下,就喜歡得出這樣的結論。更有甚者,把C語言本身的“坑”也歸結為C++的問題。這樣的人着實不少,C++11作為最具爭議的語言之一,每一次討論到涉及語言選擇的時候,都會引起一場“血戰”,但結果往往不了了之,喜歡C++的繼續堅守C++陣營,討厭C++的把精力留到下次黑C++的時候。對於客觀公正批評C++的,我內心尊敬佩服;而對於還沒搞清楚C++就信口開河的,我表示鄙視。任何一門語言都有自己的歷史背景和定位,C++被設計成這樣,從歷史上來看,是為了兼容C,使得C程序可以不用修改就可以繼續使用;從定位上看,就是三大約束:與C的完全兼容、靜態類型檢查、最高性能。我真心的希望,如果有人不喜歡C++,在搞懂它之后再黑,免得誤人子弟。

每次寫C++的優點之前,都想好好發泄一下,好了,言歸正傳。這篇文章想探討下C++中的資源管理,談到資源管理,就不得不談異常安全,正是因為有了異常,才使得資源管理變得更加重要。C++11提供了一套非常好的編程Idom來處理這個問題,C++11的新特性使得這些Idom變得更加易用。

計算中的資源是個非常廣泛的概念,內存、鎖、文件、Socket等等都是資源,C++中可以通過統一的方式管理這些資源,即RAII(參見The C++ Programming Lauguage, Special Edition, p364, 14.4節)。其基本思路非常簡單,用類來封裝資源,在類的構造函數中獲取資源,在類的析構函數中釋放資源。使用的時候,把這個類在棧上面實例化出一個對象,當這個對象超出作用域時,這個對象的析構函數會被調用,從而釋放資源。正是這個簡單的方式,構成了C++資源管理的基礎,並且這樣的方式是異常安全的,因為:1、如果在對象構造之前發生異常,則資源還沒申請,不會有問題;2、如果在類的構造函數中發生異常,C++編譯器保證資源不會發生資源泄漏(參見Exceptional C++, p26, Item8);3、在對象構造好之后發生了異常,stack unwinding(參見The C++ Programming Lauguage, Special Edition, p355, 14.1節)的過程中,C++標准要求編譯器保證當前棧上面成功構造的對象的析構函數一定會得到調用,內存一定得到釋放。

智能指針就是RAII的實現范例,專門用來管理內存,C++11中有三個智能指針:unique_ptr、shared_ptr和weak_ptr。auto_ptr已經是過時的了,它的功能被unique_ptr取代了,后者可以用於STL容器。

對於其它資源,需要用戶自己去封裝,同樣的資源只要封裝一次,以后使用起來就方便了。如果嫌每個資源都要用類包裝起來麻煩,可以利用ScopeGuard來處理,這個設施由Andrei Alexandrescu發明,劉未鵬在C++11(及現代C++風格)和快速迭代式開發中做了詳細介紹。當然了,ScopeGuard在C++11(得益於std::function和lambda表達式)下,才達到了非常易用的地步。我這邊貼一下SocpeGuard的代碼,有興趣的可以參考劉未鵬的文章。

class ScopeGuard
{
public:
    explicit ScopeGuard(std::function<void()> onExitScope)
        : onExitScope_(onExitScope), dismissed_(false)
    { }

    ~ScopeGuard()
    {
        if(!dismissed_)
        {
            onExitScope_();
        }
    }

    void Dismiss()
    {
        dismissed_ = true;
    }

private:
    std::function<void()> onExitScope_;
    bool dismissed_;

private: // noncopyable
    ScopeGuard(ScopeGuard const&);
    ScopeGuard& operator=(ScopeGuard const&);
};

使用起來很簡單,在以C的方式申請一個資源的時候:

HANDLE h = CreateFile(...);
ScopeGuard onExit([&] { CloseHandle(h); });

這樣申請資源和釋放資源永遠寫在一起,不會忘記,而且保證資源永遠不會泄漏。

說到資源泄漏,當然想到內存泄漏了,Visual C++中可以這樣的方式來檢測內存泄漏:

在cpp文件開始處,添加這么一段代碼:

#ifdef _DEBUG
#define DEBUG_CLIENTBLOCK new( _CLIENT_BLOCK, __FILE__, __LINE__)
#else
#define DEBUG_CLIENTBLOCK
#endif // _DEBUG
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#ifdef _DEBUG
#define new DEBUG_CLIENTBLOCK
#endif // _DEBUG

在某個函數中,添加這么一個函數調用:

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF|_CRTDBG_LEAK_CHECK_DF);

這樣一來,在程序在Debug模式運行結束后,如果有內存泄漏的話,會打印出類似如下的檢測信息。

Detected memory leaks!
Dumping objects ->
{197} normal block at 0x00FBCE98, 44 bytes long.
 Data: <                > 0A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
Object dump complete.

談了C++資源管理的好處,來吐槽下Java。Java是個喜歡喊口號的語言,“一次編譯,到處運行(Write once,run anywhere) ”,“程序員再也不用擔心內存泄漏了”,“純粹的面向對象語言”,諸如此類。其它先不說,就談談它的資源管理。Java中對資源的管理分為兩種,一是內存,通過gc管理;二是其它資源,通過try、catch和finally來管理。第一個問題,不統一,Java里面不統一的地方還有很多,比如基礎數據類型不能作為模板參數、明明是“純粹的面向對象語言”卻還有基本數據類型等等。第二個問題,try、catch和finally語法太丑了,而且最不爽的是在finally里面還要在try一下,太蛋疼了!第三個問題,內存被JVM管理了,導致gc時機不確定,嚴重影響Java的普及范圍(在一個大型游戲中,正打boss呢,突然gc了,然后,然后就沒有然后了)。我承認,Java是個很好用的語言,能很大程度提高程序員的開發效率,特別是JavaEE體系,的確很成熟。我自己也寫過不少Java代碼,最方便的地方是出錯了以后,異常棧能夠快速幫助我定位到bug。但是Java因為屏蔽了太多底層信息,導致它培養了一大批不太合格的程序員。這里不是說Java程序員不優秀,只是如果一直用Java,不去了解C/C++的話,真的很容易退化。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM