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_ptr
和unique_ptr
是最經常使用的兩個智能指針,都須要包含頭文件<memory>
指針
unique_ptr
是惟一的,適用於存儲動態分配的舊C風格的數組。 在聲明變量的時候,使用auto
和make_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
的用法與unique_ptr
相似。使用auto
和make_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_ptr
和shared_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_ptr
和shared_ptr
利用RAII範式,爲咱們的內存管理提供極大的方便,可是在使用時,存在一些弊端(C++ shared_ptr四宗罪),其中我以爲最使人頭痛的問題就是:接口污染。
例如,我想傳一個int*
到函數中去,因爲全部權在智能指針上,爲了保證全部權的正確轉移,我就不得不將函數的參數類型改成unique_ptr<int>
。一樣的,返回值也有相似的狀況。
上述這種狀況,若是在開發初期,明確全部指針都使用智能指針的話,並非什麼大問題。可是目前多數代碼都是創建在舊代碼的基礎上,在調用舊代碼時,你須要用智能指針中的get
方法來返回所控制的資源。調用了get
也就意味着智能指針失去了對資源的徹底控制,也就是說,它再也沒法保證資源的正確釋放了。
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; })
複製代碼
這麼作的好處在於申請資源和釋放資源的代碼牢牢的靠在一塊兒,永遠不會忘記.更不用說只要在一個地方寫釋放的代碼,下文不管發生什麼錯誤,致使該做用域退出,咱們都可以正確的釋放資源啦.
內存泄露最多見的緣由就是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++中,內存管理是半自動的,你須要告訴程序如何如何作,編譯器保證正確作。在介紹完以上三種內存管理的技巧後,這裏作一個小小的總結
LeakedObjectDetector
可以監控內存釋放正確釋放,在資源泄露時給出警告,若是你擔憂它會形成運行效率下降,那麼沒必要要在全部類上添加它,而是當你懷疑某個類出現了內存泄漏時,再加上它