C++:Memory Management

淺談C++內存管理

new和delete

在C++中,咱們習慣用new申請堆中的內存,配套地,使用delete釋放內存。html

class LiF;
LiF* lif = new LiF(); // 分配內存給一個LiF對象
delete lif; // 釋放資源
lif = nullptr; // 指針置空,保證安全

與C的malloc相比,咱們發現,new操做在申請內存的同時還完成了對象的構造,這也是new運算符作的一層封裝。c++

內存是怎樣申請的

new這個例子能夠看出,C++的內存管理大有門道,而內存管理也是C++中最爲重要的一部分。在硬件層之上的第一層封裝就是操做系統,高級語言編寫的程序也將做爲進程在這裏接受進程調度,其中就涉及到內存的分配。從這個意義上理解,能夠說,內存是向操做系統申請的(不嚴格正確)。express

在C++應用層(Application),咱們最經常使用的是C++ primitive(原語)操做,newnew[]new()::operator new()等,申請內存。在primitive之上,C++的Library還爲咱們提供了各類各樣的allocator(容器,或者說分配器),如std::allocator,能夠經過這些容器分配內存,但其實容器仍是經過newdelete運算符去實現內存的申請與釋放。在new之下,則是Microsoft的CRT庫提供的mallocfreenew操做是對malloc的封裝。再往下就是操做系統的API。這些內存管理的API的關係大體以下:數組

memory-management-1

再談new和delete

new expression

一般,咱們會使用new在堆中申請一塊內存,並把這塊內存的地址保存到一個指針,這個操做就是new操做,但嚴格來講,它其實應該稱爲new expression(new表達式)安全

LiF* lif = new LiF(); // new expression

但其實,new是一個複合操做,一般會被編譯器轉換爲相似以下的形式:cookie

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作了什麼

  1. 調用operator new申請足夠存放對象大小的內存;
  2. 把申請到的內存交給咱們的指針;
  3. 最後調用構造函數構造對象。

operator new

try/catch塊的第一句,new expression調用了operator new,它的原型是:app

// 位於<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()的內部其實是調用了mallocoperator new()的工做就是經過malloc不斷申請內存,直到申請成功。在operator new的第二個重載中能夠看到,這是一個noexcept的函數,由於咱們能夠認爲,內存的申請老是能夠成功的,由於在operator new()內部,每當申請失敗時,他都會調用一次new handler,能夠把new handler理解爲一個內存管理策略,它會釋放掉一些不須要的內存,以便當前的malloc能夠申請到內存。能夠說,operator new的工做就是申請內存。函數

placement new

在new拆解獲得的第三步,它調用了對象的構造函數,並且在表達上比較特殊:lif->LiF::LiF();。編譯器經過對象指針直接調用了對象的構造函數,但若是咱們在程序中這樣寫,編譯通常是沒法經過的,這不是源代碼的語法。在上面的語句中,咱們已經完成了內存的分配工做,顯然這一步是在進行對象的構造,這個操做也被稱爲placement new,即定點構造,在指定的內存塊中構造對象。spa

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作了什麼

  1. 調用對象的析構函數;
  2. 釋放內存。

operator delete

在delete操做的第二步,其實是執行了operator delete(),它的原型是:

void __CRTDECL operator delete(
    void*  _Block,
    size_t _Size
) noexcept;

operator delete實際上是調用了全局的::operator delete()::operator delete()又調用了free進行內存的釋放。

也就是說,newdelete是對mallocfree的一層封裝,這也對應了上面圖中的內容。

array new和array delete

array newnew[],顧名思義,它用於構造一個對象數組。

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.cnblogs.com/Li-F/p/11604288.html

相關文章
相關標籤/搜索