引言
在本篇文章中,我們主要剖析c++中的動態內存管理,包括malloc、new expression、operator new、array new和allocator內存分配方法以及對應的內存釋放方式和他們之間的調用關系,另外也包括一些會引發的陷阱如內存泄漏。
動態內存管理函數及其調用關系
c++中的動態內存分配和釋放方式有很多,主要包括:
- malloc與free
- new expression與delete expression
- array new 與array delete
- operator new和operator delete
- allocator中的allocate與deallocate
除此之外還有placement new
,但需要注意placement new
不是用來內存分配和釋放的,而是在已分配的內存上構造對象。
他們之間的調用關系如下:
下面我們來具體看下每一種分配和釋放方式的使用和原理。
malloc與free
void *p1 = malloc(32); //分配32字節的內存
free(p1);//釋放指針p1指向的內存
malloc函數以字節數為參數,返回指向分配的內存的首地址的void指針;而free函數釋放給定指針指向的內存。
事實上,malloc
分配的返回給用戶使用的內存外面上下會有兩個cookie:
這兩個cookie用戶並不能感受到,但malloc函數實際從操作系統取得的內存實際上是返回給用戶的內存加上cookie以及一些對齊填充(請注意,除了最兩邊的cookie,用戶實際得到的內存旁邊還有對齊填充字節等其他的overhead,上圖以及下面的計算沒有考慮這點)。
在VC6中,上下兩個cookie記錄的是實際分配的內存大小。
以上面的代碼舉個例子,若上下兩個cookie各占4個字節,那么cookie中的值為32+4*2=40或者41(取決於當前內存是否被分配給用戶使用)。
正是由於cookie
的存在,使得free
函數回收內存時,只需要32個字節的首地址,減去4個字節即是真正分配的內存的首地址,而大小也已經知道。free
函數不需要大小參數正是由於cookie
的存在。
operator new與operator delete
void *p6 = ::operator new(32); //分配32字節
::operator delete(p6);
PS:底層調用malloc
和free
。gnu的實現:
_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;
/* malloc (0) is unpredictable; avoid it. */
if (__builtin_expect (sz == 0, false))
sz = 1;
while ((p = malloc (sz)) == 0)
{
new_handler handler = std::get_new_handler ();
if (! handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler ();
}
return p;
}
_GLIBCXX_WEAK_DEFINITION void
operator delete(void* ptr) noexcept
{
std::free(ptr);
}
new expression與delete expression
首先來看下簡單的使用:
int *p2 = new int;
delete p2;
string *p3 = new string("hello");
delete p3;
new expression
完成兩樣工作:
-
申請並分配內存。
-
調用構造函數。
string *p3 = new string("hello");
被編譯器替換成下面的工作:
string *p3;
try{
void * tmp_p = operator new(sizeof(string));
p3 = static_cast<string *>(tmp_p);
//string 通過宏被替換為basic_string,string的實際實現是basic_string,這里不是重點。
p3 -> basic_string::basic_string("hello"); //編譯器可以這么調用,但我們自己寫代碼時不能。即我們不能以這種方式通過指針顯式調用構造函數。
}catch (std::bad_alloc){
//若分配失敗,構造函數不執行
}
我們看到,原來new expression
的內存申請和分配是通過調用operator new()
來完成的。
delete expression
也完成兩樣工作:
- 調用析構函數。
- 釋放內存。
delete p3;
被編譯器替換成下面的工作:
p3 -> ~string();//通過指針直接調用析構函數。我們自己寫代碼時也可以這么做。
operator delete(p3);//釋放內存
array new 與array delete
//Complex為自定義類,只需要知道Complex類中沒有指針成員。
Complex *pca = new Complex[3];//3次構造函數
delete[] pca;//3次析構函數
string *psa = new string[3];//3次構造函數
delete[] psa;//3次析構函數
array new
調用一次內存分配函數(底層源碼實現中,其實是調用operator new,只是調用的時候計算好了大小。因此,有上下兩個cookie。)和多次構造函數。正因為調用多次構造函數,因此只能調用無參構造函數。
Complex和string的很大不同之處在於,string有指針成員,布局如下圖:
array delete
調用多次析構函數,一次內存釋放函數(底層源碼實現中其實是調用一次operator delete
)。
我們來看下,如果本應該使用array delete
的地方使用了delete expression
會發生什么:
Complex *pca = new Complex[3];//3次構造函數
delete pca;//1次析構函數
string *psa = new string[3];//3次析構函數
delete psa;//1次析構函數
對於Complex
,我們使用了array new
調用了3次構造函數,卻沒有使用array delete
而使用了delete expression
,因此只調用了一次析構函數。那么,會發生內存泄漏嗎? 不會。因為Complex
的析構函數是無關痛癢的(trivial),因為沒有要釋放的關聯的內存(Complex對象自身所占內存之外沒有隱式占用的內存)。
同樣,對於string
,我們使用了array new
調用了3次構造函數,卻沒有使用array delete
而使用了delete expression
,因此只調用了一次析構函數。那么,會發生內存泄漏嗎? 會。因為string
的析構函數不是無關痛癢的(non-trivial),因為要釋放關聯的內存(我們知道string底層是通過char[]存儲的,析構時會釋放掉那些實際存儲字符的內存)。
PS: 具體的內存布局例子(涉及到cookie、對齊填充padding等等)。
int *p = new int[10];
delete[]p;
//delete p 亦可。int無關痛癢。
VC6中的內存布局如下:
另:
Demo *p = new Demo[3];//Demo為析構函數non-trivial的自定義class
delete[] p;
//delete p; //錯誤
VC6中的內存布局(注意紅框內的3
):
allocate與deallocate
#ifdef __GNUC__ //GNUC環境下
void *p7 = allocator<int>().allocate(4); //非static函數,通過實例化匿名對象調用allocate,分配4個int的內存。
allocator<int>().deallocate((int *)p7, 4);
void *p8 = __gnu_cxx::__pool_alloc<int>().allocate(4);
__gnu_cxx::__pool_alloc<int>().deallocate((int *)p8, 4);
#endif
allocator
為模板,實例化時需提供模板類型參數,上面的程序中模板類型參數為<int>
,allocate的參數為4
則allocate函數分配時就分配4
個int
的內存。釋放內存時需要給出指向所要釋放的內存位置的指針,以及要釋放的內存大小,單位為模板類型參數類型的大小。
__pool_alloc
也為模板,除底層調用malloc的時機不同外(__pool_alloc使用內存池降低cookie帶來的overhead),使用和上面的allocator
相同。
placement new
用法:
char *buf = new char[sizeof(Complex) * 3];
Complex *pc = new(buf) Complex(1, 2);
new(buf + 1) Complex(1, 3);
new(buf + 2) Complex(1, 3);
delete[] buf;
Complex *pc = new(buf) Complex(1, 2);
被編譯器替換成如下的工作:
Complex *pc;
try{
void *tmp = operator new(sizeof(Complex), buf);//該重載版本並不分配內存。buf指針已經指向內存。
pc = static_cast<Complex*>(tmp);
pc->Complex::Complex(1, 2);//構造函數
}catch(std::bad_alloc){
//若分配失敗則不執行構造函數。實際上沒有分配,因為之前已經分配完。
}
上面使用的GNU庫重載版本的operator new()函數如下:
// Default placement versions of operator new.
_GLIBCXX_NODISCARD inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }
可以看到確實沒有分配內存。
重載內存管理函數
new expression
、delete expression
都不可重載。
operator new
、operator delete
可以重載:
- 重載global
operator new
、operator delete
,即::operator new(size_t)
與::operator delete(void *)
。(一般不會重載全局的該函數,因為影響太廣) - 重載某個class的
operator new
、operator delete
若某個類重載了operator new
、operator delete
,則用new expression
實例化該類時,調用的是類的operator new
、operator delete
,否則,調用globaloperator new
、operator delete
。
array new
、array delete
也可以重載。同樣分全局的和類所屬的。
具體如何重載這些內存管理函數,以及如何使用重載的內存管理函數,將在下一篇文章中分析。
參考資料
[1] 《STL源碼剖析》
[2] 《Effective C++》3/e
[3] 《C++ Primer》5/e
[4] 侯捷老師的課程
[5] gcc開源庫:https://github.com/gcc-mirror/gcc