1. 程式人生 > >new/delete 詳解

new/delete 詳解

一、new/delete 簡介

new 和 delete 是 C++ 用於管理 堆記憶體 的兩個運算子,對應於 C 語言中的 malloc 和 free,但是 malloc 和 free 是函式,new 和 delete 是運算子。除此之外,new 在申請記憶體的同時,還會呼叫物件的建構函式,而 malloc 只會申請記憶體;同樣,delete 在釋放記憶體之前,會呼叫物件的解構函式,而 free 只會釋放記憶體。

new 運算子的內部實現分為兩步:

  • 記憶體分配

    呼叫相應的 operator new(size_t) 函式,動態分配記憶體。如果 operator new(size_t)

    不能成功獲得記憶體,則呼叫 new_handler() 函式用於處理new失敗問題。如果沒有設定 new_handler() 函式或者 new_handler() 未能分配足夠記憶體,則丟擲 std::bad_alloc 異常。“new運算子”所呼叫的 operator new(size_t) 函式,按照C++的名字查詢規則,首先做依賴於實參的名字查詢(即ADL規則),在要申請記憶體的資料型別T的 內部(成員函式)、資料型別T定義處的名稱空間查詢;如果沒有查詢到,則直接呼叫全域性的 ::operator new(size_t) 函式。

  • 建構函式

    在分配到的動態記憶體塊上 初始化 相應型別的物件(建構函式)並返回其首地址。如果呼叫建構函式初始化物件時丟擲異常,則自動呼叫 operator delete(void*, void*)

    函式釋放已經分配到的記憶體。

delete 運算子的內部實現分為兩步:

  • 解構函式

    呼叫相應型別的解構函式,處理類內部可能涉及的資源釋放。

  • 記憶體釋放

    呼叫相應的 operator delete(void *) 函式。呼叫順序參考上述 operator new(size_t) 函式(ADL規則)。

關於 new/delete 的內部實現,參考如下程式碼。

class T{
public:
    T(){
        cout << "建構函式。" << endl;
    }

    ~T(){
        cout << "解構函式。"
<< endl; } void * operator new(size_t sz){ T * t = (T*)malloc(sizeof(T)); cout << "記憶體分配。" << endl; return t; } void operator delete(void *p){ free(p); cout << "記憶體釋放。" << endl; return; } }; int main() { T * t = new T(); // 先 記憶體分配 ,再 建構函式 delete t; // 先 解構函式, 再 記憶體釋放 return 0; }

結果如下:
這裡寫圖片描述


每個 new 獲取的物件,必須用 delete 析構並釋放記憶體,以免 記憶體洩漏。

舉例說明:

class Test{
public:
    Test(){
        str = new char[2];
    }
    ~Test(){
        delete [] str;
    }
private:
    char * str;
};

int main(){

    // ①
    Test * t = new Test;
    free(t);

    // ②
    Test * t2 = (Test*)malloc(sizeof(Test));
    delete t2;

    return 0;
}
  • 對於 ①,new Test 的時候將會產生兩方面的記憶體:

    • Test 物件本身的記憶體( Win32環境,4 Bytes 儲存 char * 指標);

    • str 所指向的 2 bytes 堆記憶體。

    如果呼叫 free 釋放記憶體,那麼由於 free 並不會呼叫 Test 的解構函式,所以 free 只能釋放 Test 物件的記憶體(4 bytes),而 str 所指向的 2-bytes 堆記憶體並不能得到釋放,因此而造成 記憶體洩漏

  • 對於 ②,malloc 並不會呼叫類的建構函式,所以只分配了 Test 物件的記憶體,str 並未初始化為指向一塊堆記憶體。所以當呼叫 delete 釋放記憶體的時候,將呼叫類的解構函式 ( delete [] str ),此時 delete 一塊沒有使用權的記憶體,程式崩潰

總之,編寫C++程式時,在進行動態記憶體分配的時候,最好使用 new 和 delete。並且記住,new 出來的物件用 delete “消滅”它。

二、new/delete 表示式語法

2.1 new 表示式語法

2.1.1 記憶體分配

1)普通的 new 運算子表示式

new 的基本語法 :

type  * p_var = new type;   // int * a = new int; // 分配記憶體,但未初始化,垃圾值

通過new初始化物件,使用下述語法:

type  * p_var = new type(init); // int * a = new int(8); //分配記憶體時,將 *a 初始化為 8

其中 init 是傳遞給建構函式的實參表或初值。

2)動態生成物件陣列的 new 運算子表示式

new 也可建立一個物件陣列:

type  p_var = new type [size]; // int * a = new int[3] ; // 分配了 3個 int 大小的連續記憶體塊, 但未初始化

C++98 標準規定,new 建立的物件陣列不能被顯式初始化, 陣列所有元素被預設初始化。如果陣列元素型別沒有預設初始化(預設建構函式),則編譯報錯。但 C++11 已經允許顯式初始化,例如:

int *p_int = new int[3] {1,2,3};

如此生成的物件陣列,在釋放時必須呼叫 delete [] 表示式。

2.1.2 placement new 運算子表示式

placement new 運算子表示式 就是 在使用者指定的記憶體位置上構建新的物件 ,這個構建過程並不需要額外分配記憶體,只需要呼叫物件的建構函式即可。

placement new 的語法是:

new ( expression-list ) new-type-id ( optional-initializer-expression-list );

使用這種 placement new 運算子表示式,原因之一是 使用者的程式不能在一塊記憶體上自行呼叫其建構函式,必須由編譯系統生成的程式碼呼叫建構函式。原因之二是可能需要把物件放在特定硬體的記憶體地址上,或者放在多處理器核心的共享的記憶體地址上。(PS:建構函式沒辦法直接這麼呼叫 p->A(),而解構函式可以直接這麼呼叫 p->~A()。)

釋放這種 placement new 運算子物件時,不能呼叫 placement delete,應直接呼叫解構函式,如:pObj->~ClassType() ; 然後再自行釋放記憶體。

注意: C++ 中並沒用與 placement new 運算子 功能相對應 的 placement delete 運算子(沒有placement delete 運算子的概念,但是有 placement delete 函式)。^_^

解釋:

  1. class Arena {
    public:
            void * allocate(size_t);
            void deallocate(void\*);
              .... 
    };
    void * operator new(size_t sz, Arena& a)
    {
          return a.allocate(sz);
    }
    Arena a1(some arguments);
    Arena a2(some arguments);
    
    X* p1 = new(a1) X;
    Y* p2 = new(a1) Y;
    Z* p3 = new(a2) Z;

    對於上述程式碼,C++的型別機制並不能推斷 p1 指向的物件是否位於 a1 之上。那麼直接呼叫 delete(a1) p1; 就容易出錯。所以為了安全,C++不提供 placement delete 運算子。

  2. placement new 運算子不另外分配記憶體,換句話說,不是new運算子。它完成的功能是在給定地址上呼叫建構函式。如果提供p->T(),那麼 placement new 運算子就不需要了。如果存在功能對應的 placement delete 運算子,那麼功能就應該是在給定地址上呼叫解構函式。但因為 C++已經提供了p->~T(),就沒必要有 placement delete 運算子。

  3. 如果存在對應的 placement delete 運算子,其實就是呼叫解構函式。而本身解構函式就可以自行主動呼叫,那麼自己呼叫就好了,但是物件本身所佔用這塊記憶體還可以繼續使用。如果想 placement delete 運算子像打洞一樣,連物件記憶體一起回收,那 operator new(size_t ) 的大塊蜂窩煤記憶體如何 delete 。這不科學,既然整塊記憶體是 operator new(size_t) 的,就應該由 operator delete(void *) 回收,而不能用 placement delete 運算子部分回收。

  4. 總之,沒有與 placement new 運算子功能相對應的 placement delete 運算子。而且需要注意的是,運算子和函式是兩個不同的概念,C++有 placement new 運算子和函式的概念,但是沒有 placement delete 運算子的概念,有 placement delete 函式的概念

所以,對於 placement new 運算子,我們需要主動呼叫物件的解構函式。如下示例:

#include <iostream>

using namespace std;

class Test{
public:
    Test(){
        cout << "Test 構造" << endl;
        str = new char[2];
    }
    ~Test(){
        cout << "Test 析構" << endl;
        delete [] str;
    }
private:
    char * str;
};

int main(int argc, char* argv[])
{
    char buf[100];  // 棧變數
    Test *p = new(buf) Test(); // Test()產生的臨時變數用於初始化 指定記憶體地址

    p->~Test(); // 一定要主動呼叫解構函式,避免記憶體洩漏。 而且呼叫必須在 buf 生命週期內呼叫才有效。

    // buf 指向的棧記憶體並不需要程式設計師主動釋放。
    // 棧變數過了生命週期會自動釋放記憶體
    // 其實棧記憶體的釋放也不叫記憶體釋放,只是棧頂指標移動,如果該塊棧記憶體沒有被其他程式重新整理,那麼該棧記憶體的值依然不變。


    char * buf2 = new char[100];
    Test * p2 = new(buf2) Test();

    p2->~Test(); // 切記,主動呼叫解構函式

    delete [] buf2; // 堆記憶體需要主動釋放

    return 0;
}

如上程式碼,如果把 p->~Test(); 註釋掉,上述程式碼的結果將為:

Test 構造

顯然是隻呼叫建構函式。所以對於placement new,我們需要主動呼叫物件的解構函式 pObj->~ClassType()

2.1.3 如何在棧上new?

我們知道,new 是用於管理堆記憶體,那又怎麼可能在棧上 new 出一個物件呢?

通過上面的討論,我們發現,new 除了能用於動態分配記憶體,還能夠使用 placement new 在特定記憶體位置進行初始化。所以,如何在棧上 new 呢?上述程式碼(2.1.2)就是一個很好的例子。

2.1.4 不丟擲異常的new運算子

在分配記憶體失敗時,new運算子的標準行為是丟擲std::bad_alloc異常。也可以讓new運算子在分配記憶體失敗時不丟擲異常而是返回空指標。

new (nothrow) Type ( optional-initializer-expression-list );

new (nothrow) Type[size]; // new (std::nothrow_t) Type[size];

其中 nothrow 是 std::nothrow_t 的一個例項.

2.2 delete 表示式語法

2.2.1 記憶體釋放

1)普通的 delete 運算子

delete 的基本語法是:

delete val_ptr; 

2)釋放物件陣列的 delete 運算子

delete [] val_ptr

2.2.2 沒有 placement delete 運算子表示式

通過上面的討論,我們可以知道 C++ 中並沒有提供與 placement new 運算子功能相對應 placement delete 運算子。但是仍然有placement delete函式的概念,功能在後面有介紹。

C++ 不能使用 placement delete 運算子表示式直接析構一個物件但不釋放其記憶體。因此,對於placement new表示式構建的物件,析構釋放時有兩種辦法:

  1. 是直接寫一個函式,完成析構物件、釋放記憶體的操作:

    void destroy (T * p, A & arena)
    {
        // *p 是在 arena 之上構建的物件,即 T * a = new(&arena) T; 
        p->~T() ;   // 先析構 *p 物件
        arena.deallocate(p) ;  // 再釋放 arena 整個記憶體,而不是位於arena中的部分記憶體(*p)
    }
    A arena ;
    T * p = new (arena) T ;
    ....
    destroy(p, arena) ;
  2. 分兩步顯式 呼叫解構函式 與 帶位置的 operator delete 函式:

    A arena ;
    T * p = new (arena) T ;
    /* ... */
    p->~T() ;    // 先析構
    operator delete(p, arena) ;   // 呼叫 placement delete 函式(非運算子)
    
    // Then call the deallocator function indirectly via 
    operator delete(void *, A &) .

帶位置的 operator delete(void *,void *) 函式,可以被 placement new 運算子表示式自動呼叫。這是在物件的建構函式丟擲異常的時候,用來釋放掉 placement new 函式獲取的記憶體(類內部可能涉及的記憶體分配)。以避免記憶體洩露。

#include <cstdlib>
#include <iostream>

char buf[100];
struct A {} ;
struct E {} ;

class T {
public:
    T() 
    { 
        std::cout << "T 建構函式。" << std::endl;
        throw E(); //丟擲異常
    }

    void * operator new(std::size_t,const A &)
    {
        std::cout << "Placement new called for class T." << std::endl;
        return buf;
    }

    void operator delete(void*, const A &)
    {
        std::cout << "Placement delete called for class T." << std::endl;
    }
} ;

void * operator new ( std::size_t, const A & )
    {
        std::cout << "Placement new called." << std::endl; 
        return buf;
    }

void operator delete ( void *, const A & )
    {
        std::cout << "Placement delete called." << std::endl;
    }

int main ()
{
    A a ;
    try {
        T * p = new (a) T ;
        /* do something */
    } 
    catch (E exp) 
    {
        std::cout << "Exception caught." << std::endl;
    }

    return 0 ;
}

結果如下:
這裡寫圖片描述

C++ 有 placement delete 函式,但是沒有 placement delete 運算子的概念。

2.2.3 delete 類物件時該注意的問題

問題 1

如下一段程式碼,是否是產生記憶體洩漏? 此題的討論詳見 csdn 論壇

class A
{
public:
    A(){}
    virtual void f(){}

private:
    int m_a;
};

class B : public A
{
public:
    virtual void f(){}
private:
    int m_b;
};

int main()
{
    A *pa = new B;
    delete pa;

    pa = NULL;

    return 0;
}

答案:不會產生記憶體洩漏。

  • delete 釋放記憶體時,會呼叫類的解構函式。 但是需要明確的是 解構函式並不會釋放 物件本身 的記憶體 。

  • delete 運算子分為2個階段。 第一個階段是呼叫類的解構函式,第二階段才是釋放物件記憶體(但是這個工作不是解構函式在做)。

  • 解構函式是free()之前的呼叫,而真正釋放記憶體的操作是 free(void *ptr),注意只有指標一個引數,沒有長度引數,這說明了什麼?說明了 A *pa = new B; 時帶著長度sizeof(B)最終呼叫了malloc(sizeof(B))申請的記憶體及長度已經被記錄,當free(pa)是就會釋放掉自pa開始長度為sizeof(B)的記憶體。解構函式僅僅是應用邏輯層次的釋放資源,不是物理層次的釋放資源。(PS:關於new/delete運算子的具體實現後面還會涉及。)

問題 2

修改一下上面的題目,如下是否會造成記憶體洩漏呢?

class A
{
public:
    A(){
        m_a = new int(1);
    }
    ~A(){  // 宣告為virtual, 防止記憶體洩漏
        delete m_a;
    }

private:
    int * m_a;
};

class B : public A
{
public:
    B() : A(){
        m_b = new int(2);
    }

    ~B(){
        delete m_b;
    }

private:
    int * m_b;
};

int main()
{
    A * pa = new B;
    delete pa;

    pa = NULL;

    return 0;
}

答案:會造成記憶體洩漏。

  • delete pa 的時候,只會呼叫基類的解構函式。所以 m_b 指向的記憶體塊沒得到釋放。造成記憶體洩漏。

  • 通過這個例子,應該深刻理解 解構函式的作用: 程式設計師處理類內部可能涉及的記憶體分配、資源釋放。而不是釋放類本身的記憶體。

三、operator new/delete() 的函式過載

平時使用 new 動態生成一個物件,實際上是呼叫了 new 運算子

該運算子首先呼叫了operator new(std::size_t )函式動態分配記憶體,然後呼叫型別的建構函式初始化這塊記憶體。 new / delete 運算子是不能被過載的,但是下述各種 operator new/delete()函式既可以作為 1. 全域性函式過載,也可以作為 2. 類成員函式或 3. 作用域內的函式過載,即由程式設計者指定如何獲取記憶體。

3.1 普通的operator new/delete(size_t size)函式

new 運算子 首先呼叫 operator new(std::size_t ) 函式動態分配記憶體。首先查詢 類內 是否有 operator new(std::size_t)函式可供使用(即依賴於實參的名字查詢)。

  • operator new(size_t )函式的引數是一個 size_t 型別,指明瞭需要分配記憶體的規模。

  • operator new(size_t )函式可以被每個 C++ 類作為成員函式過載。也可以作為全域性函式過載:

void * operator new (std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();

記憶體需要回收的話,呼叫對應的operator delete(void *)函式。

例如,在 new 運算子表示式的第二步,呼叫建構函式初始化記憶體時如果丟擲異常,異常處理機制在棧展開(stack unwinding)時,要回收在new運算子表示式的第一步已經動態分配到的記憶體,這時就會 自動呼叫 對應 operator delete(void*) 函式。(注意:此處呼叫的是非位置delete函式)

struct E{};

class T{
public:
    T(){
        cout << "建構函式。" << endl;
        throw E();
    }

    ~T(){
        cout << "解構函式。" << endl;
    }

    void * operator new(size_t sz){

        T * t = (T*)malloc(sizeof(T));
        cout << "記憶體分配。" << endl;

        return t;
    }

    void operator delete(void *p){

        free(p);
        cout << "記憶體釋放。" << endl;

        return;
    }
};

int main()
{

    try {
        T * p = new T;
        /* do something */
    }
    catch (E exp){
        std::cout << "Exception caught." << std::endl;
    }

    return 0;
}

結果:
這裡寫圖片描述

3.2 陣列形式的operator new/delete[](size_t size)函式

new type[] 運算子,用來動態建立一個物件陣列。這需要呼叫陣列元素型別內部定義的void* operator new[](size_t)函式來分配記憶體。如果陣列元素型別沒有定義該函式,則呼叫全域性的void* operator new[](size_t)函式來分配記憶體。

#include <new> 中聲明瞭 void* operator new[](size_t) 全域性函式:

void * operator new [] (std::size_t) throw(std::bad_alloc);
void operator delete [](void*) throw();

3.3 placement new/delete 函式

void * operator new(size_t,void*) 函式用於帶位置的 new 運算子呼叫。C++標準庫已經提供了operator new(size_t,void*)函式的實現,包含 <new> 標頭檔案即可。這個實現只是簡單的把引數的指定的地址返回,帶位置的new運算子就會在該地址上呼叫建構函式來初始化物件:

// Default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) throw() { return __p; }
inline void* operator new[](std::size_t, void* __p) throw() { return __p; }

// Default placement versions of operator delete.
inline void  operator delete  (void*, void*) throw() { }
inline void  operator delete[](void*, void*) throw() { }

禁止重定義這4個函式。因為都已經作為 <new> 的內聯函數了。在使用時,實際上不需要#include <new>

雖然上面的4個 placement new/delete 函式不能過載,但是仍然可以寫一個自己的 placement new/delete 函式,例如 :

inline void* operator new(std::size_t, A * /* 或者 const A &*/); 
inline void* operator new[](std::size_t, A * /* 或者 const A &*/);

inline void  operator delete  (void*, A* /* 或者 const A &*/);
inline void  operator delete[](void*, A* /* 或者 const A &*/);

但是,基本沒有什麼意義 ^_^。

3.4 保證不丟擲異常的operator new/delete函式

C++標準庫的<new>中還提供了一個nothrow的實現,使用者可寫自己的函式替代:

void* operator new(std::size_t, const std::nothrow_t&) throw();
void* operator new[](std::size_t, const std::nothrow_t&) throw();

void operator delete(void*, const std::nothrow_t&) throw();
void operator delete[](void*, const std::nothrow_t&) throw();

3.5 Clang關於operator new/delete 的實現

以下這段程式碼是Clang編譯器關於operator new(std::size_t)operator delete (void *) 的實現:

void * operator new(std::size_t size) throw(std::bad_alloc) {
    if (size == 0)
        size = 1;
    void* p;
    while ((p = ::malloc(size)) == 0) {
        std::new_handler nh = std::get_new_handler();
        if (nh)
            nh();
        else
            throw std::bad_alloc();
    }
    return p;
}

void operator delete(void* ptr) {
    if (ptr)
        ::free(ptr);
}

這段程式碼很簡單,神祕的 operator new/delete 在背後也不過是在偷偷地呼叫C函式庫的 malloc / free !當然,這跟具體實現有關,Clang libcxx 是這樣實現,不代表其它實現也是如此。

需要意識到的是, operator new 和 operator + () 一樣,只不過是普通的函式,是可以過載的,所謂的 placement new ,也是一個全域性 operator new 的過載版本,在Clang libcxx 中定義如下:

inline _LIBCPP_INLINE_VISIBILITY void* operator new (std::size_t, void* __p) _NOEXCEPT 
{
    return __p;
}

四、小結

new 和 delete 是 C++ 用於管理 堆記憶體 的兩個運算子。

new 運算子 進行動態記憶體申請的時候,包含 2 個階段:

  • 記憶體申請 new。

    根據 Clang 的實現,我們可以猜測 記憶體new 基本就是通過 malloc 進行動態記憶體申請,但是本步驟並不初始化記憶體。本步驟對應 operator new(size_t ) 函式。

  • 建構函式。

delete 運算子 進行記憶體釋放的時候,也包含 2 個階段:

  • 析構物件。

  • 記憶體釋放 delete。

    本步驟對應operator delete(void*) 函式。


除了用於記憶體管理的 new/delete 運算子,還有帶位置的 placement new 運算子,但是沒有帶位置的 placement delete 運算子。

placement new 運算子

  • 解決不能主動呼叫建構函式的“矛盾”。

  • 對應的函式是 operator new(size_t , void *)

placement delete 運算子

  • 沒有此類運算子。

  • 但有帶位置的 placement delete 函式,如全域性的 operator delete(void *,void*)

五、擴充套件 : free/delete 怎麼知道有多少記憶體要釋放 ?

在使用c或者c++的時候我們經常用到malloc/free和new/delete,在使用malloc申請記憶體的時候我們給定了需要申請的記憶體大小,但是在free或者delete的時候並不需要提供這個大小,那麼程式是怎麼實現準確無誤的釋放記憶體的呢?

實際上,在申請記憶體的時候,申請到的地址會比你實際的地址大一點點,他包含了一個存有申請空間大小的結構體。

比如你申請了20byte的空間,實際上系統申請了48bytes的block

  • 16-byte header containing size, special marker, checksum, pointers to next/previous block and so on.
  • 32 bytes data area (your 20 bytes padded out to a multiple of 16))

這樣在 free的時候就不需要提供任何其他的資訊,可以正確的釋放記憶體

這裡有個在 stackoverflow.com 上的提問,可以參考