淺談C++記憶體管理
new和delete
在C++中,我們習慣用new申請堆中的記憶體,配套地,使用delete釋放記憶體,
class LiF;
LiF* lif = new LiF(); // 分配記憶體給一個LiF物件
delete lif; // 釋放資源
lif = nullptr; // 指標置空,保證安全
與C的malloc相比,我們發現,new操作在申請記憶體的同時還完成了物件的構造,這也是new運算子做的一層封裝,
記憶體是怎樣申請的
從new這個例子可以看出,C++的記憶體管理大有門道,而記憶體管理也是C++中最為重要的一部分,在硬體層之上的第一層封裝就是作業系統,高級語言撰寫的程式也將作為行程在這里接受行程調度,其中就涉及到記憶體的分配,從這個意義上理解,可以說,記憶體是向作業系統申請的(不嚴格正確),
在C++應用層(Application),我們最常用的是C++ primitive(原語)操作,new、new[]、new()、::operator new()等,申請記憶體,在primitive之上,C++的Library還為我們提供了各種各樣的allocator(容器,或者說分配器),如std::allocator,可以通過這些容器分配記憶體,但其實容器還是通過new和delete運算子去實作記憶體的申請與釋放,在new之下,則是Microsoft的CRT庫提供的malloc和free,new操作是對malloc的封裝,再往下就是作業系統的API,這些記憶體管理的API的關系大致如下:

再談new和delete
new expression
通常,我們會使用new在堆中申請一塊記憶體,并把這塊記憶體的地址保存到一個指標,這個操作就是new操作,但嚴格來說,它其實應該稱為new expression(new運算式),
LiF* lif = new LiF(); // new expression
但其實,new是一個復合操作,通常會被編譯器轉換為類似如下的形式:
LiF* lif;
try {
void* mem = operator new(sizeof(LiF)); // apply for memory
lif = static_cast<LiF*>(mem); // static type conversion
lif->LiF::LiF(); // constructor
} catch(std::bad_alloc) {
// exception handling
}
new做了什么
- 呼叫
operator new申請足夠存放物件大小的記憶體; - 把申請到的記憶體交給我們的指標;
- 最后呼叫建構式構造物件,
operator new
在try/catch塊的第一句,new expression呼叫了operator new,它的原型是:
// 位于<vcruntime_new.h>
_Ret_notnull_ _Post_writable_byte_size_(_Size)
_NODISCARD _VCRT_ALLOCATOR void* __CRTDECL operator new(
size_t _Size
);
_Ret_maybenull_ _Success_(return != NULL) _Post_writable_byte_size_(_Size)
_NODISCARD _VCRT_ALLOCATOR void* __CRTDECL operator new(
size_t _Size,
std::nothrow_t const&
) noexcept;
而在operator new()會去呼叫::operator new(),最后,::operator new()的內部實際上是呼叫了malloc,operator new()的作業就是通過malloc不斷申請記憶體,直到申請成功,在operator new的第二個多載中可以看到,這是一個noexcept的函式,因為我們可以認為,記憶體的申請總是可以成功的,因為在operator new()內部,每當申請失敗時,他都會呼叫一次new handler,可以把new handler理解為一個記憶體管理策略,它會釋放掉一些不需要的記憶體,以便當前的malloc可以申請到記憶體,可以說,operator new的作業就是申請記憶體,
placement new
在new拆解得到的第三步,它呼叫了物件的建構式,而且在表達上比較特殊:lif->LiF::LiF();,編譯器通過物件指標直接呼叫了物件的建構式,但如果我們在程式中這樣寫,編譯一般是無法通過的,這不是源代碼的語法,在上面的陳述句中,我們已經完成了記憶體的分配作業,顯然這一步是在進行物件的構造,這個操作也被稱為placement new,即定點構造,在指定的記憶體塊中構造物件,
new expression是operator new和placement new的復合,
delete expression
在我們不再需要某一個物件時,通常使用delete析構該物件,delete操作嚴格來說,與new對應,它應該稱為delete expression(delete運算式),
delete lif; // delete expression
lif = nullptr;
同樣,delete也是一個復合操作,通常會被編譯器轉換為類似如下的形式:
lif->~LiF(); // destructor
operator delete(lif); // free the memory
delete做了什么
- 呼叫物件的解構式;
- 釋放記憶體,
operator delete
在delete操作的第二步,實際上是執行了operator delete(),它的原型是:
void __CRTDECL operator delete(
void* _Block,
size_t _Size
) noexcept;
而operator delete其實是呼叫了全域的::operator delete(),::operator delete()又呼叫了free進行記憶體的釋放,
也就是說,new和delete是對malloc和free的一層封裝,這也對應了上面圖中的內容,
array new和array delete
array new即new[],顧名思義,它用于構造一個物件陣列,
class LiF {
public:
LiF(int _lif = 0): lif(_lif) {}
int lif;
};
LiF* lifs = new LiF[3]; // right
LiF* lifs = new LiF[3](); // right
LiF* lifs = new LiF[3](1); // wrong, no param accepted
LiF* lifs = new LiF[3]{1}; // right, but only lifs[0].lif equals 1
array new的作業是申請一塊足以容納指定個數的物件的記憶體(在本例中是3個LiF物件),在前兩種寫法中,array new呼叫的是默認建構式,這種情況下只能默認構造物件,但如果又想要給物件賦予非默認的初值,那么就需要使用到placement new了,
LiF* lifs = new LiF[3];
LiF* p = lifs;
for (int i = 0; i < 3; ++i) {
new(p++)LiF(i+1); // placement new
cout << lifs[i].lif << endl;
}
直觀地,placement new并不會分配記憶體,它只是在已分配的記憶體上構造物件,對應地,使用array new構造的物件需要使用array delete釋放記憶體,
delete[] lifs;
相較于array new,array delete不需要提供陣列長度引數,這是因為,在使用array new構造物件的時候,還有一塊額外的空間用于存放cookie,也就是這塊記憶體的一些資訊,其中就包括這個記憶體塊的大小和物件的數量等等,
class LiF {
public:
//...
~LiF() { cout << "des" << endl; }
};
delete[] lifs; // array delete
此時我們顯式地定義解構式,并且在解構式被呼叫時列印資訊,在運行到delete[]的時候,程式就會根據cookie中的資訊,準確地釋放對應的記憶體塊,本例中,“des”會被列印三次,即3個物件的解構式都被呼叫了,此時如果錯誤地呼叫delete而非array delete,那么就可能會發生記憶體泄漏,
delete lifs; // delete
這時只會呼叫一次解構式,但本例中并不會發生泄漏,這個簡單的類中并沒有包含其他物件,再看下面這種情況:
class LiF2 {
public:
LiF2() : lif(new LiF()) {}
LiF2(const LiF& _lif) : lif(new LiF(_lif.lif)) {}
~LiF2() { delete lif; lif = nullptr; }
private:
LiF* lif;
};
LiF2* lif2 = new LiF2[3];
delete lif2; // call "delete" by mistake
這時,由于錯誤地使用了delete,解構式只會被呼叫一次,也就是說,還有另外兩個物件,雖然物件本身被銷毀了,但物件中的lif指標所指的物件卻沒有被銷毀,即:物件本身不會發生泄漏,泄漏的是物件中指標保存的記憶體,
深入placement new
之前提到的new()操作以及new expression拆解的第三步,其實都是placement new,在主動使用placement new時,它的一般格式為:
new(pointer)Constructor(params);
// or
::operator new(size_t, void*);
它的作用是:把物件(object)構造在已分配的記憶體(allocated memory)中,同樣也可以在vcruntime_new.h中找到相關定義:
#ifndef __PLACEMENT_NEW_INLINE
#define __PLACEMENT_NEW_INLINE
_Ret_notnull_ _Post_writable_byte_size_(_Size) _Post_satisfies_(return == _Where)
_NODISCARD inline void* __CRTDECL operator new(size_t _Size, _Writable_bytes_(_Size) void* _Where) noexcept
{
(void)_Size;
return _Where;
}
inline void __CRTDECL operator delete(void*, void*) noexcept
{
return;
}
#endif
可以看到,placement new并沒有做任何作業,它只是把我們傳遞的指標又return了回來,結合下面的例子就不難理解這個邏輯,
class LiF {
public:
LiF(int _lif = 0): lif(_lif) {}
int lif;
};
LiF* lifs = new LiF[3]; // array new
LiF* lif = new(lifs)LiF(); // placement new
我們在array new得到的LiF物件陣列中的第一個物件上使用了placement new,同樣拆解這個new操作可以得到類似上面普通new的一個try/catch塊:
LiF* lif;
try {
void* mem = operator new(sizeof(LiF), lifs); // placement new
lif = static_cast<LiF*>(mem); // static type conversion
lif->LiF::LiF(); // constructor
} catch(std::bad_alloc) {
// exception handling
}
此外,在__PLACEMENT_NEW_INLINE宏還包含了一個placement delete的定義:
inline void __CRTDECL operator delete(void*, void*) noexcept
{
return;
}
可以看到,它也是不做任何作業的,所謂的placement delete只是為了形式上的統一,
總結
- 記憶體的申請釋放可以在不同層面上進行,但只要是在作業系統之上,都是基于malloc/free,
- 在C++ primitive層,通常使用new和delete系列,new是對malloc的封裝,delete是對free的封裝,
- 通常new是指new expression,嚴格來說,new的含義有三種:new expression、operator new和placement new,new expression是operator new和placement new的復合,operator new負責記憶體的申請,placement new負責物件的構造;此外還有new[],
- 所有的記憶體申請/釋放操作都必須配套使用,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/102074.html
標籤:C++
上一篇:抽象類和介面
