如何優雅地管理C++ 中的內存

C++的內存管理

C++是一門Native Language,而說到Native Languages就不得不說資源管理,其中內存管理又是資源管理中的一個大問題,因爲堆內存須要手動分配和釋放,因此必須確保申請的內存獲得正確的釋放。對此通常的原則是"誰分配的誰釋放",但即使如此仍然會出現內存泄漏,野指針等問題。ios

託管語言們爲了解決這個問題引入了GC(Garbage Collection),它們認爲內存過重要了,不能交給程序員來作。但GC對於Native開發經常有它本身的問題。而其另外一方面Native界也經常詬病GC,說內存過重要了,不能交給機器作程序員

C++提供了一種折中的解決方案,即:既不是徹底交給機器作,也不是徹底交給程序員作,而是程序員如今代碼中指定怎麼作,至於何時作,如何確保必定會獲得執行,則交給編譯器來肯定。數組

首先是C++98提供了語言機制:對象在超出做用域的時候其析構函數會自動被調用。接着,C++之父Bjarne Stroustrup定義了RAII(Resoure Acquisition is Initialization)範式(即:對象構造的時候所需的資源應該在構造函數中初始化,而對象析構的時候應該釋放資源)。RAII 告訴咱們應該應用類來封裝和管理資源。bash

沿着這一思想,首先要介紹的內存管理小技巧即是使用智能指針socket

智能指針

對於內存管理而言,Boost第一個實現了工業強度的智能指針,現在的智能指針(shared_ptr和unique_ptr)已是C++11中的一部分,簡單來講有了智能指針,你的C++代碼幾乎就不該該出現delete了。函數

雖然智能指針被稱爲」指針「,它的行爲像一個指針,但本質上它實際上是個類。正如前面所說的:ui

RAII 告訴咱們應該應用類來封裝和管理資源atom

智能指針在對象初始化的時候獲取內存的控制權,在析構的的時候自動釋放內存,來正確的管理內存。spa

C++11中,shared_ptrunique_ptr是最經常使用的兩個智能指針,都須要包含頭文件<memory>指針

unique_ptr

unique_ptr是惟一的,適用於存儲動態分配的舊C風格的數組。 在聲明變量的時候,使用automake_unique搭配效率更高。此外,unique_ptr正如它的名字同樣,一個資源應該只有一個unique_ptr進行控制,在須要轉移控制權時,應該使用std::move,失去控制權的指針沒法再繼續訪問資源。

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    int size = 5;
    auto x1 = make_unique<int[]>(size);
    unique_ptr<int[]> x2(new int[size]);        // 兩種聲明unique_ptr的方式

    x1[0] = 5;
    x2[0] = 10;                                 // 像指針同樣賦值

    for(int i = 0; i < size; ++i)
        cout << x1[i] << endl;                  // 輸出: 5 0 0 0 0

    auto x3 = std::move(x1);                    // 轉移x1的全部權
    for(int i = 0; i < size; ++i)
        cout << x3[i] << endl;                  // 輸出: 5 0 0 0 0

}
複製代碼

unique_ptr對象在析構的時候釋放所控制的資源,當發生控制權轉移的時,有一種狀況特別要注意,即千萬不要將控制權轉移給一個局部變量。由於局部變量退出做用域後會被析構,從而釋放資源,此時外部再要訪問一個被釋放的資源時,就會出錯。 下面的例子說明了這種狀況

#include <iostream>
#include <memory>

using namespace std;

class A
{
public:
    A():a(new int(10))                      // 初始化a爲10
    {
        cout << "Create A..." << endl;
    }

    ~A()
    {
        cout << "Destroy A..." << endl;
        delete a;                           // 釋放a
    }

    int* a;
};

void move_unique_ptr_to_local_unique_ptr(unique_ptr<A>& uptr)
{
    auto y(std::move(uptr));                // 轉移全部權
}                                           // 函數結束,y進行析構,便釋放了A的資源

int main()
{
    auto x = make_unique<A>();
    move_unique_ptr_to_local_unique_ptr(x);

    cout << *(x->a) << endl;                  // 內存訪問錯誤,x中的資源以及被局部變量釋放了
}
複製代碼

shared_ptr

shared_ptr的用法與unique_ptr相似。使用automake_shared搭配效率更高。此外,與unique_ptr不一樣的是,shared_ptr採用引用計數的方式管理內存,所以一個資源能夠有多個shared_ptr同時引用,而且在引用計數爲0時,釋放資源(引用計數能夠用use_count來查看)

void copy_shared_ptr_to_local_shared_ptr(shared_ptr<A>& sptr)
{
    auto y(sptr);                                                         // 複製shared_ptr,擁有同一片資源
    cout << "After copy, use_count : " << sptr.use_count() << endl;       // After copy, use_count : 2
}

int main()
{
    auto x = make_shared<A>();
    cout << "use_count: " << x.use_count() << endl;          // use_count: 1
    copy_shared_ptr_to_local_shared_ptr(x);
    cout << *(x->a) << endl;                                 // 內存未被釋放,能夠正常訪問
}
複製代碼

unique_ptrshared_ptr還能夠指定如何釋放內存,這大大方便了咱們對文件、socket等資源的管理

#include <iostream>
#include <memory>

using namespace std;

void fclose_deletor(FILE* f)
{
    cout << "close a file" << endl;
    fclose(f);
}

int main()
{
    unique_ptr<FILE, decltype(&fclose_deletor)> file_uptr( fopen("abc.txt", "w"),  &fclose_deletor);
    shared_ptr<FILE> file_sptr( fopen("abc.txt", "w"),  fclose_deletor);
}
複製代碼

智能指針unique_ptrshared_ptr利用RAII範式,爲咱們的內存管理提供極大的方便,可是在使用時,存在一些弊端(C++ shared_ptr四宗罪),其中我以爲最使人頭痛的問題就是:接口污染。

例如,我想傳一個int*到函數中去,因爲全部權在智能指針上,爲了保證全部權的正確轉移,我就不得不將函數的參數類型改成unique_ptr<int>。一樣的,返回值也有相似的狀況。

上述這種狀況,若是在開發初期,明確全部指針都使用智能指針的話,並非什麼大問題。可是目前多數代碼都是創建在舊代碼的基礎上,在調用舊代碼時,你須要用智能指針中的get方法來返回所控制的資源。調用了get也就意味着智能指針失去了對資源的徹底控制,也就是說,它再也沒法保證資源的正確釋放了。

Scope Guard

RAII範式雖然好,可是還不夠易用,不少時候咱們並不想爲了一個closeHandle,ReleaseDC等去大張旗鼓的寫一個類出來;智能指針方便了咱們對內存的管理,但仍屬於「指針」的範疇,對非指針的資源使用起來不太方便,另外加上接口污染的問題,因此這些時候咱們每每會由於怕麻煩而直接手動去釋放函數,手動調的一個壞處就是,若是在資源申請和資源釋放之間發生了異常,那麼釋放將不會發生。此外,手動釋放須要在函數全部可能的出口都去調用釋放函數,萬一某天有人修改了代碼,多了一個處return,而return以前忘記了調用釋放函數,資源就泄露了。理想狀況,咱們但願可以這樣使用:

#include <fstream>
using namespace std;

void foo()
{
    fstream file("abc.txt", ios::binary);
    ON_SCOPE_EXIT{ file.close() };
}
複製代碼

ON_SCOPE_EXIT裏面的代碼就像在析構函數同樣:不管是以怎樣的方式退出,都好比會被執行

最開始,這種ScopeGuard的想法被提出的時候,因爲C++沒有太好的機制來支持這個想法,其實現很是的繁瑣和不完美。再後來,C++11發佈了,結合C++11的Lambda Function和tr1::function就可以簡化其實現

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

    ~ScopeGuard()
    {
        onExitScope_();
    }

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

private: // noncopyable
    ScopeGuard(ScopeGuard const&) = delete;
    ScopeGuard& operator=(ScopeGuard const&) = delete;
};
複製代碼

這個類使用很是簡單,你交給他一個std::function,它負責在析構的時候執行,絕大多數這個std::function是一個lambda,例如:

void foo()
{
    fstream file("abc.txt", ios::binary);
    ScopeGuard on_exit([&]{
        file.close();
    });
}
複製代碼

on_exit在析構的時候會執行file.close。爲了不給這個對象起名字的麻煩,能夠定義一個宏,把行號混入其中,這樣每次定義一個ScopeGuard對象都是惟一命名的:

#define SCOPEGUARD_LINENAME_CAT(name, line) name##line
#define SCOPEGUARD_LINENAME(name, line) SCOPEGUARD_LINENAME_CAT(name, line)
#define ON_SCOPE_EXIT(callback) ScopeGuard SCOPEGUARD_LINENAME(EXIT, __LINE__)(callback)
複製代碼

自從有了ON_SCOPE_EXIT以後,在C++中申請和釋放資源就變得很是方便啦

fstream file("abc.txt", ios::binary);
ON_SCOPE_EXIT( [&] { file.close(); })

auto* x = new A()
ON_SCOPE_EXIT( [&] { delete x; })
複製代碼

這麼作的好處在於申請資源和釋放資源的代碼牢牢的靠在一塊兒,永遠不會忘記.更不用說只要在一個地方寫釋放的代碼,下文不管發生什麼錯誤,致使該做用域退出,咱們都可以正確的釋放資源啦.

Leaked Object Detector

內存泄露最多見的緣由就是new了一個資源,忘記delete了,雖然智能指針和scope guard可以有效地幫助咱們正確地釋放內存,但因爲種種緣由和限制,仍是會出現忘記釋放內存的問題,如何監控沒有正確釋放的內存呢? 也許咱們須要一個Leaked Object Detector,讓它在發生泄漏的時候通知咱們.

具體的,咱們但願能它有這樣的做用:

int main()
{
   auto* x = new A();
} // 報錯,由於沒有delete
複製代碼

在JUCE的源碼中,我發現了一個LeakedObjectDetector類,它可以實現咱們想要的。LeakedObjectDetector內部維護了一個計數器,在OwnerClass被建立時,計數器+1,OwnerClass析構時,計數器-1

template <typename OwnerClass>
class LeakedObjectDetector
{
public:
    LeakedObjectDetector() noexcept
    {
        ++(getCounter().num_objects);
    }

    LeakedObjectDetector(const LeakedObjectDetector&) noexcept
    {
        ++(getCounter().num_objects);
    }

    ~LeakedObjectDetector()
    {
        if(--(getCounter().num_objects) < 0)
        {
            cerr << "*** Dangling pointer deletion! Class: " << getLeakedObjectClassName() << endl;

            assert(false);
        }
    }

private:
    class LeakCounter
    {
    public:
        LeakCounter() = default;

        ~LeakCounter()
        {
            if(num_objects > 0)
            {
                cerr << "*** Leaked object detected: " << num_objects << " instance(s) of class" << getLeakedObjectClassName() << endl;
                assert(false);
            }
        }

        atomic<int> num_objects{0};
    };

    static const char* getLeakedObjectClassName()
    {
        return OwnerClass::getLeakedObjectClassName();
    }

    static LeakCounter& getCounter() noexcept
    {
        static LeakCounter counter;
        return counter;
    }
};
複製代碼

由於計數器是靜態的,它的生命週期是從程序開始到程序結束,所以在程序結束時,計數器作析構,析構函數進行判斷,若是計數器>0,說明有實例被建立可是沒有釋放。

另外一個判斷在LeakedObjectDetector的析構函數中,若是計數器<0,說明被delete了屢次

另外只要出現了內存泄露或者屢次delete,就用assert來強制中斷

配合宏,使用起來就很是方便

#define LINENAME_CAT(name, line) name##line
#define LEAK_DETECTOR(OwnerClass) \
        friend class LeakedObjectDetector<OwnerClass>;  \
        static const char* getLeakedObjectClassName() noexcept { return #OwnerClass; } \
        LeakedObjectDetector<OwnerClass>  LINENAME_CAT(leakDetector, __LINE__);

class A
{
public:
    A() = default;

private:
    LEAK_DETECTOR(A);
};
複製代碼

只要用上LEAK_DETECTOR(ClassName),就能夠監控類的內存釋放被正確釋放了,例如

int main()
{
    auto* x = new A();
    return 0;
}
// 忘記delete,出現警告:
// *** Leaked object detected: 1 instance(s) of classA
// Assertion failed: (false), function ~LeakCounter, file /Users/hw/Development/work/leaked_object_detector/main.cpp, line 44.
int main()
{
    auto* x = new A();

    delete x;
    delete x;
    return 0;
}
// 屢次delete,出現警告
// *** Dangling pointer deletion! Class: A
// Assertion failed: (false), function ~LeakedObjectDetector, file /Users/hw/Development/work/leaked_object_detector/main.cpp, line 29.
複製代碼

總結

在C++中,內存管理是半自動的,你須要告訴程序如何如何作,編譯器保證正確作。在介紹完以上三種內存管理的技巧後,這裏作一個小小的總結

  • RAII告訴咱們,應該用類將資源進行封裝,保證類初始化時資源獲得初始化,類析構時資源獲得釋放.所以考慮用vector這樣的類來替代原生的數組指針
  • 儘量的使用智能指針,可是要注意全部權的轉移
  • 用scope guard來管理局部資源,它可以保證不管以什麼方式退出做用域,資源都可以被正確地釋放
  • LeakedObjectDetector可以監控內存釋放正確釋放,在資源泄露時給出警告,若是你擔憂它會形成運行效率下降,那麼沒必要要在全部類上添加它,而是當你懷疑某個類出現了內存泄漏時,再加上它
相關文章
相關標籤/搜索