1. 程式人生 > >C++中的new/delete和new[]/delete[]

C++中的new/delete和new[]/delete[]

1、瞭解new-handler的行為

通俗來講就是new失敗的時候呼叫的回撥函式,直接看程式碼:

#include<iostream>
#include<string.h>
#include <stdlib.h> 

using namespace std;

int main(int argc,char* argv[])
{
    int* pBigDataArray = new int[10000000000L];
    cout << pBigDataArray[0] << endl;
    delete [] pBigDataArray;
    return 0;
}

申請記憶體失敗時會丟擲"bad_alloc"異常,此前會呼叫一個由std::set_new_handler()指定的錯誤處理函式(”new-handler”)。“new-handler”函式通過std::set_new_handler()來設定,std::set_new_handler()定義在<new>中:
namespace std{
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) throw();
}
set_new_handler()的使用也很簡單:
#include<iostream>
#include<string.h>
#include <stdlib.h> 

using namespace std;

void overmem()
{
    cout<<"Unable to alloc memory" << endl;
    abort();
}

int main(int argc,char* argv[])
{
	set_new_handler(overmem);
    int* pBigDataArray = new int[10000000000L];
    cout << pBigDataArray[0] << endl;
    delete [] pBigDataArray;
    return 0;
}

關於”new-handler”的更詳細說明請參看經典書籍:“Eeffective C++”中的條款49。

2、new和delete關鍵字

1)new關鍵字

C++ Prim Plus中如此描述new運算子:new運算子根據型別來確定需要多少位元組的記憶體。然後它找到這樣的記憶體,並返回它的地址。
要想了解new運算子具體幹了那些事,我們就必須分析new的具體執行過程。先看一下new簡單型別的情況(注意new的實現雖然根據不同的編譯環境不同,但大體過程基本相同):

1.1)new 簡單資料型別(包括基本資料型別和不需要建構函式的型別)

//C++原始碼
int* p = new int;

//彙編程式碼
00E54C44  push        4    
00E54C46  call        operator new (0E51384h)    
00E54C4B  add         esp,4    

分析:傳入4byte的引數後呼叫operator new。其原始碼如下:

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
        {       // try to allocate size bytes
        void *p;
        while ((p = malloc(size)) == 0)
                if (_callnewh(size) == 0)
                {       // report no memory
                        _THROW_NCEE(_XSTD bad_alloc, );
                }

        return (p);
        }
分析:我們仔細觀察就會發現 operator new 現是呼叫了malloc函式申請記憶體,而當申請失敗也就是返回空指標時,判斷  _callnewh(size) 返回值是否為0,若為0則丟擲一個異常,非0則繼續迴圈執行malloc。_callnewh的作用就是呼叫上文提到的new_handler的函式,這裡注意,在有些文章中說_callnewh是一個new_handler這是不正確的,_callnewh只是呼叫new_handler,作用類似與回撥函式!看清operator new的內部結構後我們發現,在簡單的資料型別情況下,原來這個函式的功能十分簡單,就是以malloc為主體,對malloc申請失敗的情況做了一下特殊的處理。
總結:簡單型別直接呼叫operator new分配記憶體;可以通過new_handler來處理new失敗的情況;new分配失敗的時候不像malloc那樣返回NULL,它直接丟擲異常;要判斷是否分配成功應該用異常捕獲的機制;

1.2)new 複雜資料型別(需要由建構函式初始化物件)

通過上面這個例子我們知道了new在簡單型別下的具體執行過程,那麼new在複雜型別下又是怎麼執行的呢?我們看一下下面這個例子:

//C++原始碼
class Object
{
public:
	Object()
	{
		_val = 1;
	}
	~Object()
	{
	}
private:
	int _val;
};
void main()
{
	Object* p = new Object();
}
//彙編程式碼
00AD7EDD  push        4  
00AD7EDF  call        operator new (0AD1384h)  		//operator new
00AD7EE4  add         esp,4  
00AD7EE7  mov         dword ptr [ebp-0E0h],eax  
00AD7EED  mov         dword ptr [ebp-4],0  
00AD7EF4  cmp         dword ptr [ebp-0E0h],0  
00AD7EFB  je          main+70h (0AD7F10h)  
00AD7EFD  mov         ecx,dword ptr [ebp-0E0h]  
00AD7F03  call        Object::Object (0AD1433h)        //在new的地址上呼叫建構函式
00AD7F08  mov         dword ptr [ebp-0F4h],eax  
00AD7F0E  jmp         main+7Ah (0AD7F1Ah)  
00AD7F10  mov         dword ptr [ebp-0F4h],0  
00AD7F1A  mov         eax,dword ptr [ebp-0F4h]  
00AD7F20  mov         dword ptr [ebp-0ECh],eax  
00AD7F26  mov         dword ptr [ebp-4],0FFFFFFFFh  
00AD7F2D  mov         ecx,dword ptr [ebp-0ECh]  
00AD7F33  mov         dword ptr [p],ecx  

分析:通過上面的程式碼我們就可以很直觀的看出new在複雜型別時的執行過程:先呼叫operator new分配空間,再呼叫建構函式進行初始化。
總結:new 複雜資料型別的時候先呼叫operator new,然後在分配的記憶體上呼叫建構函式。

2)delete關鍵字

C++ primer plus中如此描述delete關鍵字:當需要記憶體時,可以使用new來請求。另一方面是delete運算子,它使得在使用完記憶體後,能夠將其歸還給記憶體池。delete也分為兩種情況:

2.1)delete簡單資料型別(包括基本資料型別和不需要解構函式的型別)

//C++原始碼
int *p = new int(1);
delete p;
//彙編程式碼
00275314  mov         eax,dword ptr [p]  
00275317  mov         dword ptr [ebp-0D4h],eax  
0027531D  mov         ecx,dword ptr [ebp-0D4h]  
00275323  push        ecx  
00275324  call        operator delete (0271127h) 
分析:傳入引數p之後呼叫operator delete,其原始碼如下:
void operator delete( void * p )
{
    RTCCALLBACK(_RTC_Free_hook, (p, 0));

    free( p );
}

分析:RTCCALLBACK是一個空的巨集定義,這就意味著對於簡單型別operator delete只是簡單的呼叫了free。
總結:delete簡單資料型別預設只是呼叫free函式。

2.2)delete複雜資料型別(需要由解構函式銷燬物件)

通過上面這個例子我們知道了delete在簡單型別下的具體執行過程,那麼delete在複雜型別下又是怎麼執行的呢?我們看一下下面這個例子:

//C++原始碼
class Object
{
public:
	Object()
	{
		_val = 1;
	}

	~Object()
	{
		cout << "destroy object" << endl;
	}
private:
	int _val;
};
void main()
{
	Object* p = new Object;
	delete p;
}
//彙編程式碼
012241F0  mov         dword ptr [this],ecx  
012241F3  mov         ecx,dword ptr [this]  
012241F6  call        Object::~Object (0122111Dh)                           //先呼叫解構函式
012241FB  mov         eax,dword ptr [ebp+8]  
012241FE  and         eax,1  
01224201  je          Object::`scalar deleting destructor'+3Fh (0122420Fh)  
01224203  mov         eax,dword ptr [this]  
01224206  push        eax  
01224207  call        operator delete (01221145h)                           //再呼叫operator delete
0122420C  add         esp,4 
分析:從上面彙編執行過程我們就可以看出,delete在複雜型別情況下執行與new相反,先呼叫解構函式,再執行operator delete 歸還記憶體。
總結:delete複雜資料型別先呼叫解構函式再呼叫operator delete。

3、new和delete的執行過程

1)new的執行過程:new -> operator new -> malloc,這是new的基本部分,如果記憶體分配成功,那麼operator new就會直接返回。而記憶體分配出錯,也就是malloc返回指標為空:
malloc出錯 -> 呼叫new_handler -> 若new_handler返回為0 -> 丟擲異常
          |
             v
      若new_handler返回非0 -> 繼續呼叫malloc
若是簡單型別那麼new到這裡基本就結束了,但要是複雜型別,new還要繼續呼叫建構函式。
這下我們就明白了new和malloc的區別了,new會呼叫malloc進行記憶體分配的操作。但他和malloc不用的是,他分配失敗時會呼叫new_handler,而new_handler返回0的情況丟擲異常,而malloc只會返回一個空指標。

2)delete的執行過程

相對於new運算子delete的執行過程可以說相當簡明:delete -> 解構函式(如果有) -> operator delete -> RTCCALLBACK空巨集定義 -> free

4、過載new和delete運算子

1)過載operator new

前面說過大家稱呼new關鍵字為“運算子”,而我們知道在C++中運算子是可以過載的,那麼是否意味著我們可以為我們自己的類定製一個new運算子呢?答案是肯定的!

#include<iostream>
#include<string.h>
using namespace std;
class MyClass
{
public:
    MyClass()
    {
        _val = 1;
    }
    void * operator new(size_t size)
    {
        std::cout << "MyClass operator new!" << std::endl;
        return ::operator new(size);
    }
private:
    int _val;
};

int main(int argc,char* argv[])
{
    MyClass *m = new MyClass();
    return 0;
}

上面的例子演示了一個過載operator new的例子。上面的例子中在呼叫全域性的operator new之前,我們加入了自己的特殊處理,不過要注意返回值是 void *。

2)過載operator delete

和operator new一樣,我們也可以過載operator delete。

#include<iostream>
#include<string.h>
using namespace std;
class MyObject
{
public:
    int a;
    MyObject()
    {
        a = 1;
    }
    ~MyObject()
    {
        a = 0;
    }
    void operator delete(void *p)
    {
        std::cout << "delete MyObject" << std::endl;
        return ::operator delete(p);
    }
};
int main(int argc,char* argv[])
{
	MyObject* o = new MyObject();
	delete o;
    return 0;
}


上面的例子演示了一個過載operator delete的例子。上面的例子中在呼叫全域性的operator delete之前,我們加入了自己的特殊處理。

5、new[]和delete[]

1)new[]

1.1)簡單資料型別(包括基本資料型別和不需要解構函式的型別)。

簡單型別new[]中,首先new[]呼叫了operator new[]。計算出陣列總大小之後呼叫operator new

void *__CRTDECL operator new[](size_t count) _THROW1(std::bad_alloc)
    {    // try to allocate count bytes for an array
    return (operator new(count));
    }
總結:針對簡單型別,new[]計算好大小後呼叫operator new。

1.2)複雜資料型別(需要由解構函式銷燬物件)

複雜型別中執行完operator new[]後還會利用一個vector constructor iterator來記錄new所需的建構函式的地址等資訊。那麼編譯器是如何知道要new多少個元素呢?原來在new[]時編譯器會在陣列的頭部也就是陣列指標所指向的位置加上陣列的長度,也就是一個四位元組的_DWORD 。也正是這四個位元組導致我們使用new[]建立複雜型別陣列之後,無法使用delete來釋放而只能使用delete[]來釋放。

class A
{
public:
	A()
	{
		_val = 1;
	}

	~A()
	{
		cout << "destroy A" << endl;
	}
private:
	int _val;
};

void main()
{
	A* pAa = new A[3];
}

分析:從這個圖中我們可以看到申請時在陣列物件的上面還多分配了 4 個位元組用來儲存陣列的大小,但是最終返回的是物件陣列的指標,而不是所有分配空間的起始地址。這樣的話,釋放就很簡單了:delete [] pAa;
總結:針對複雜型別,new[]會額外儲存陣列大小。

2)delete[]

2.1)簡單資料型別(包括基本資料型別和不需要解構函式的型別)

delete和delete[]效果一樣,比如下面的程式碼:

int* pint = new int[32];
delete [] pint;
char* pch = new char[32];
delete pch;
分析:執行後不會有什麼問題,記憶體也能完成的被釋放。看下彙編碼就知道operator delete[]就是簡單的呼叫operator delete。
總結:針對簡單型別,delete和delete[]等同。

2.2)複雜資料型別(需要由解構函式銷燬物件)

複雜型別delete[]使用vector deleting destructor 來釋放陣列,釋放記憶體之前會先呼叫每個物件的解構函式,使用陣列頭指標儲存陣列長度,使用delete[]沒有問題,但使用delete就變成了簡單釋放頭指標指向的記憶體這會造成記憶體洩露。

分析:這裡要注意的兩點是:呼叫解構函式的次數是從陣列物件指標前面的 4 個位元組中取出;傳入 operator delete[] 函式的引數不是陣列物件的指標 pAa,而是 pAa 的值減 4。
總結:針對複雜型別,new[]出來的記憶體只能由delete[]釋放。

6、為什麼 new/delete 、new []/delete[] 要配對使用?

先看如下程式碼:

int *p = new int[10];
delete []p;
這肯定是沒問題的,但如果把 delete []p; 換成 delete p; 的話,會出問題嗎?

這就涉及到上面一節提到的問題了。上面提到了在 new [] 時多分配 4 個位元組的緣由,因為析構時需要知道陣列的大小,但如果不呼叫解構函式呢(如內建型別,這裡的 int 陣列)?我們在 new [] 時就沒必要多分配那 4 個位元組, delete [] 時直接到第二步釋放為 int 陣列分配的空間。如果這裡使用 delete p;那麼將會呼叫 operator delete 函式,傳入的引數是分配給陣列的起始地址,所做的事情就是釋放掉這塊記憶體空間。不存在問題的。這裡說的使用 new [] 用 delete 來釋放物件的提前是:物件的型別是內建型別或者是無自定義的解構函式的類型別!

我們看看如果是帶有自定義解構函式的類型別,用 new [] 來建立類物件陣列,而用 delete 來釋放會發生什麼?用下面的例子來說明:

A *p = new A[3];
delete p;
那麼 delete p; 做了兩件事:呼叫一次 p指向的物件的解構函式;呼叫 operator delete(p); 釋放記憶體。
顯然,這裡只對陣列的第一個類物件呼叫了解構函式,後面的兩個物件均沒呼叫解構函式,如果類物件中申請了大量的記憶體需要在解構函式中釋放,而你卻在銷燬陣列物件時少呼叫了解構函式,這會造成記憶體洩漏。上面的問題你如果說沒關係的話,那麼第二點就是致命的了!直接釋放 p 指向的記憶體空間,這個總是會造成嚴重的段錯誤,程式必然會奔潰!因為分配的空間的起始地址是 p 指向的地方減去 4 個位元組的地方。你應該將傳入引數設為那個地址!
同理,你可以分析如果使用 new 來分配,用 delete [] 來釋放會出現什麼問題?是不是總會導致程式錯誤?總的來說,記住一點即可:new/delete、new[]/delete[] 要配套使用總是沒錯的!

7、placement new

1)placement new的含義:placement new可以實現不分配記憶體,只調用建構函式。

void *operator new( size_t, void *p ) throw()     { return p; }
placement new的執行忽略了size_t引數,只返還第二個引數。其結果是允許使用者把一個物件放到一個特定的地方,達到呼叫建構函式的效果。用法如下:
#include<iostream>
#include<string.h>
 // 必須include 這個,才能使用 "placement new" 
#include <new>
using namespace std;

class Test
{
public:
    Test()
    {
        std::cout << "Constructor" << std::endl;
    };
    ~Test()
    {
        std::cout << "Destructor" << std::endl;
    }
private:
    char mA;
    char mB;
};

char* gMemoryCache = new char[sizeof(Test)];

int main(int argc,char* argv[])
{
	{
        Test* test = new(gMemoryCache) Test();
    }
    {
        Test* test = new(gMemoryCache) Test();
        test->~Test();
    }
    return 0;
}

和其他普通的new不同的是,它在括號裡多了另外一個引數。比如:
Widget * p = new Widget; 					 //ordinary new 
pi = new (ptr) int; pi = new (ptr) int;     //placement new
括號裡的引數ptr是一個指標,它指向一個記憶體緩衝器,placement new將在這個緩衝器上分配一個物件。Placement new的返回值是這個被構造物件的地址(比如括號中的傳遞引數)。placement new主要適用於:在對時間要求非常高的應用程式中,因為這些程式分配的時間是確定 的;長時間執行而不被打斷的程式;以及執行一個垃圾收集器 (garbage collector)。

2)new 、operator new 和 placement new 區別

1.1)new :不能被過載,其行為總是一致的。它先呼叫operator new分配記憶體,然後呼叫建構函式初始化那段記憶體。
1.2)operator new:要實現不同的記憶體分配行為,應該過載operator new,而不是new。
1.3)placement new:只是operator new過載的一個版本。它並不分配記憶體,只是返回指向已經分配好的某段記憶體的一個指標。因此不能刪除它,但需要呼叫物件的解構函式。placement new允許你在一個已經分配好的記憶體中(棧或者堆中)構造一個新的物件。原型中void*p實際上就是指向一個已經分配 好的記憶體緩衝區的的首地址。

3)placement new 存在的理由

1.1)用Placement new 解決buffer的問題:用new分配的陣列緩衝時,由於呼叫了預設建構函式,因此執行效率上不佳。若沒有預設建構函式則會發生編譯時錯誤。如果你想在預分配的記憶體上建立物件,用預設的new操作符是行不通的。要解決這個問題,你可以用placement new構造。它允許你構造一個新物件到預分配的記憶體上。
1.2)增大時空效率的問題:使用new操作符分配記憶體需要在堆中查詢足夠大的剩餘空間,顯然這個操作速度是很慢的,而且有可能出現無法分配記憶體的異常(空間不夠)。
placement new 就可以解決這個問題。我們構造物件都是在一個預先準備好了的記憶體緩衝區中進行,不需要查詢記憶體,記憶體分配的時間是常數;而且不會出現在程式執行中途出現內 存不足的異常。所以,placement new非常適合那些對時間要求比較高,長時間執行不希望被打斷的應用程式。

4)placement new的使用步驟

在很多情況下,placement new的使用方法和其他普通的new有所不同。這裡提供了它的使用步驟。
1.1)第一步 快取提前分配
有三種方式:
1.為了保證通過placement new使用的快取區的memory alignmen(記憶體佇列)正確準備,使用普通的new來分配它:在堆上進行分配

class Task ;
char * buff = new [sizeof(Task)]; //分配記憶體
(請注意auto或者static記憶體並非都正確地為每一個物件型別排列,所以,你將不能以placement new使用它們。)
2.在棧上進行分配
class Task ;
char buf[N*sizeof(Task)]; //分配記憶體
3.還有一種方式,就是直接通過地址來使用。(必須是有意義的地址)
void* buf = reinterpret_cast<void*> (0xF00F);
1.2)第二步:物件的分配
在剛才已分配的快取區呼叫placement new來構造一個物件。
Task *ptask = new (buf) Task
1.3)第三步:使用
按照普通方式使用分配的物件:
ptask->memberfunction();
ptask-> member;
//...
1.4)第四步:物件的析構
一旦你使用完這個物件,你必須呼叫它的解構函式來毀滅它。按照下面的方式呼叫解構函式:
ptask->~Task(); //呼叫外在的解構函式
1.5)第五步:釋放
你可以反覆利用快取並給它分配一個新的物件(重複步驟2,3,4)如果你不打算再次使用這個快取,你可以象這樣釋放它:
delete [] buf;
跳過任何步驟就可能導致執行時間的崩潰,記憶體洩露,以及其它的意想不到的情況。如果你確實需要使用placement new,請認真遵循以上的步驟。

5)效能對比:採用placement new和new的方式建立和刪除物件10000000次,統計時間,單位是us。

#include<iostream>
#include<string.h>
#include <sys/time.h> 
#include <time.h> 
#include<stdio.h>
#include <new>
using namespace std;

long GetCurrentTimeInMicroSeconds()
{
	struct timeval t_start,t_end; 
	gettimeofday(&t_start, NULL); 
	return ((long)t_start.tv_sec)*1000+(long)t_start.tv_usec/1000; 
}

class Test
{
public:
    Test()
    {
        //std::cout << "Constructor" << std::endl;
    };
    ~Test()
    {
        //std::cout << "Destructor" << std::endl;
    }
private:
    char mA;
    char mB;
};

char* gMemoryCache = new char[sizeof(Test)];

int main(int argc,char* argv[])
{
	{
        long start = GetCurrentTimeInMicroSeconds();
        for (int i = 0; i < 10000000; ++i)
        {
            Test* test = new(gMemoryCache) Test();
            test->~Test();
        }
        std::cout << "placement new:" << GetCurrentTimeInMicroSeconds() - start << std::endl;
    }
    {
        long start = GetCurrentTimeInMicroSeconds();
        for (int i = 0; i < 10000000; ++i)
        {
            Test* test = new Test();
            delete test;
        }
        std::cout << "new:"<<GetCurrentTimeInMicroSeconds() - start << std::endl;
    }
    return 0;
}

結論:在頻繁構造和析構物件的場景中,placement new對效能有5倍的提升。