C++動態內存:(二)重載new和delete

1、重載的緣由ios

    用new建立動態對象時會發生兩件事:(1)使用operatoe new()爲對象分配內存(常常是調用malloc)(2)調用構造函數來初始化內存。相對應的調用delete運算符會(1)首先調用析構函數(2)調用operator delete()釋放內存(常常是調用free)。咱們沒法控制構造函數和析構函數的調用,是由編譯器調用的。但能夠改變內存分配函數operator new()和operator delete()。連接:C++中的new/new[],delete/delete[]編程

    使用了new和delete的內存分配系統是爲了通用目的而設計的,可是在特殊的情形下並不能知足須要。最多見的改變分配系統的緣由經常是出於效率考慮:數組

(1)增長分配和歸還的速度。建立和銷燬一個特定的類的很是多的對象,以致於這個運算成了速度的瓶頸。安全

(2)堆碎片。分配不一樣大小的內存會在堆上產生不少碎片,以致於雖然內存可能還有,但因爲都是碎片,找不到足夠大的內存塊來知足須要。經過爲特定的類建立本身的內存分配器,能夠確保這種狀況不會發生。例如在嵌入式和實時系統裏,程序可能必須在有限資源狀況下運行很長時間,這樣的系統就要求內存花費相同的時間且不容許出現堆內存耗盡或者出現不少碎片。數據結構

(3)檢測運用上的錯誤。例如:new所得的內存,delete時卻失敗了,致使內存泄漏;在new所得內存上屢次delete,致使不肯定行爲;數據"overruns」(寫入點在分配內存區塊尾端以後)或「underruns」(寫入點在分配區塊以前)。能夠超額分配,而後在額外空間放置特定的byte patterns(簽名)來進行檢測。函數

(4)統計使用動態內存的信息。this

(5)爲了下降缺省內存管理器帶來的空間額外開銷。spa

(6)爲了彌補缺省分配器中的非最佳齊位。.net

(7)爲了將相關對象成簇集中。例如,爲了將特定的某個數據結構在儀器使用,而且使用時缺頁中斷頻率降至最低。new和delete的palcement版本有可能完成這樣的集簇行爲。線程

(8)得到非傳統的行爲。例如:分配和歸還共享內存。

 2、重載全局的new和delete

 一、重載了全局的new和delete後將使默認的new和delete不能載被訪問,甚至在這個從新定義裏也不能調用它們。

 二、重載的new必須有一個size_t參數。這個參數由編譯器產生並傳遞給咱們,它是要分配的對象的長度。函數返回值爲一個大於等於這個對象長度的指針。

 三、operator new()的返回值是一個void*,而不是指向任何特定類型的指針。所作的是分配內存,而不是完成一個對象創建——直到構造函數調用了才完成了對象的建立,它是編譯器確保的動做,不在咱們的控制範圍以內。

 四、operator delete()的參數是一個指向由operator new()分配的內存的void*,而不是指向任何特定類型的指針。參數是一個void*是由於它是在調用析構函數後獲得的指針。析構函數從存儲單元裏移去對象。operator delete()的返回類型是void。

五、功能示例:


#include <stdio.h>
#include<stdlib.h>
void *operator new(size_t sz)
{
    printf("operator new:%d Bytes\n",sz);
    void *m=malloc(sz);
    if(!m)
        puts("out of memory");
    return m;
}
void operator delete(void *m)
{
    puts("operator delete");
    free(m);
}
class S
{
    int i[100];
public:
    S(){puts("S:S()");}
    ~S(){puts("~S::S()");}
};
int main( )
{
    puts("(1)Creating & Destroying an int");
    int *p=new int(47);
    delete p;
    puts("(2)Creating & Destroying an S");
    S *s=new S;
    delete s;
    puts("(3)Creating & Destroying S[3]");
    S *sa=new S[3];
    delete []sa;
    return 0;
}
輸出:


 函數說明:

(1)使用printf()和puts()而不是iostream,是由於建立一個iostream對象時(像全局的cin/cout/cerr),它們調用new去分配內存。用printf()不會進入死鎖狀態,由於它不會調用new來初始化自身。

(2)數組的輸出爲1204,而不是1200,說明額外的內存被分配用於存放它所包含對象的數量信息。

(3)輸出狀況說明都使用了由全局重載版本的new和delete。

說明:程序只是示範最簡單的使用方法,具體的重載還有不少細節須要考慮,具體參考第四節「注意事項」


六、全局new實現僞碼

void* operator new(std::size_t size)throw(std::bad_alloc)
{
    using namespce std;
    if(size==0) 
    {
        size=1;
    };
    while(true)
        嘗試分配 size bytes;
    if(分配成功)
        return (一個指針指向分配得來的內存);
    //分配失敗:找出new_handling函數
    new_handler globalHandler=set_new_handler(0);
    set_new_handler(globalHandler);
 
    if(globalHandler)(*globalHandler)();
    else
        throw std::bad_alloc();
}
七、全局delete實現僞碼
void operator delete(void *mem)throw()
{
    if(mem==0) return;
    歸還mem所指內存;
}
3、對類重載new和delete

一、爲一個重載new和delete,儘管沒必要顯式地使用static,但實際上仍在建立static成員函數。

二、當編譯器看到使用new建立自定義的類的對象時,它選擇成員版本的operator new()而不是全局版本的new()。但若是要建立這個類的一個對象數組時,全局的operator new()就會被當即調用,用來爲這個數組分配內存。固然能夠經過爲這個類重載運算符的數組版本,即operator new[]和operator delete[]來控制對象數組的內存分配。

三、使用繼承時,重載了的類的new和delete不能自動繼承使用。

四、功能示例():


#include<iostream>
using namespace std;
class Widget
{
    int i[10];
public:
    Widget(){cout<<"*";}
    ~Widget(){cout<<"~";}
    void* operator new(size_t sz)
    {
        cout<<"Widget::new: "<<sz<<"bytes"<<endl;
        return ::new char[sz];
    }
    void operator delete(void* p)
    {
        cout<<"Widget::delete "<<endl;
        ::delete []p;
    }
    void *operator new[](size_t sz)
    {
        cout<<"Widget::new[]: "<<sz<<"bytes"<<endl;
        return ::new char[sz];
    }
    void operator delete[](void *p)
    {
        cout<<"Widget::delete[] "<<endl;
        ::delete []p;
    }
};
int main( )
{
    cout<<"(1-1) new Widget"<<endl;
    Widget *w=new Widget;
    cout<<"\n(1-2) delete Widget"<<endl;
    delete w;
    cout<<"\n(2-1)new Widget[25]"<<endl;
    Widget *const wa=new Widget[25];
    cout<<"\n(2-2)delete []Widget"<<endl;
    delete []wa;
    return 0;
}
結果:

程序分析:

(1)由於沒有重載全局版本的operator new()和operator delete(),所以可使用cout

(2)語法上除了多一對括號外數組版本的new與delete與單個對象版本的是同樣的。無論是哪一種狀況,咱們都要決定要分配的內存的大小。數組版本的大小指的是整個數組的大小。

(3)從結果能夠看出:都是調用的重載版本的,new先分配內存再調用構造函數,delete先調用析構函數而後釋放內存。

說明:程序只是示範最簡單的使用方法,具體的重載還有不少細節須要考慮,具體參考第四節「注意事項」。完整實現的僞碼爲:


六、member版本operator new和operator delete實現僞碼

class Base
{
public:
    static void *operator new(std::size_t size)throw(std::bad_alloc);
    static void operator delete(void *rawMemory,std::size_t size)throw();
    ...
};
void *Base::operator new(std::size_t size)throw(std::bad_alloc)
{
    if(size!=sizeof(Base))//sizeof(空類)爲1,所以不須要判斷size==0的狀況
        return ::operator new(size);
    ...
}
void Base::operator delete(void *rawMemory, std::size_t size)throw()
{
    if(rawMemory==0)return;
    if(size!=sizeof(Base))
    {
        ::operator delete(rawMemory);
        return;
    }
    歸還rawMemory所指的內存;
    return;
}
動態建立時,只需寫Base *pBase=new Base;size的大小編譯器會計算並傳遞給new。


4、注意事項

一、當重載operator new()和operator delete()時,咱們只是改變了原有內存分配方法(重載operator new()惟一須要作的就是返回一個足夠大的內存塊的指針)。編譯器將用重載的new代替默認版本去分配內存並調用構造函數。

二、在重載的operator news內應該包含一個循環,反覆調用某個new_handler函數。連接:C++ new_handler和set_new_handler

三、注意數據位對齊。C++要求全部的operator news返回的指針都有適當的對齊(取決於數據類型)。malloc就是在這樣的要求下工做的,所以令operator new返回一個得自malloc的指針是安全的(調用malloc獲得分配的內存塊指針後,不要再對指針進行偏移)。

四、除非必須,不然不要本身重載new和delete。由於可能會漏掉可移植性、齊位、線程安全等細節。必須時,能夠借鑑使用一些開放源碼的標準庫(例如Boost程序庫的Pool)。

五、客戶要求0 bytes時operator new也得返回一個合法的指針。一般的處理方法是當申請0字節是,將它視爲申請1-bytes。即


if(size==0)
{
   size=1;
}
六、重寫delete時,要保證「刪除null指針永遠安全」。

七、對於某個特定類class X設計的operator new和operator delete一般是爲大小恰好爲sizeof(X)大小的對象而設計的。所以不要在繼承類中使用該重載的new和delete。若是基類專屬的operator new並不是被設計用來對付派生的狀況,能夠將「內存申請量錯誤」的調用行爲改成標準的operator new。像下例

void *Base::operator new(std::size_t size)throw(std::bad_alloc)
{
    if(size!=sizeof(Base))
        return ::operator new(size);
    ...
}
一樣的,若類將 大小有誤的分配行爲轉交::operator new執行,則必須將大小有誤的刪除行爲轉交::operator delete執行。若是要刪除的對象派生自某個base class然後者欠缺virtual析構函數,C++傳給operator delete的size_t數值可能不正確。
void Base::operator delete(void *rawMemory, std::size_t size)throw()
{
    if(rawMemory==0)return;
    if(size!=sizeof(Base))
    {
        ::operator delete(rawMemory);
        return;
    }
    歸還rawMemory所指的內存;
    return;
}
5、placement new和placement delete

     以上所示爲普通的,只帶size_t參數的重載,可是若是 想要在指定內存位置上放置對象;或者想要在new時志記信息則須要帶額外參數。

一、示例:

#include<iostream>
using namespace std;
class X
{
    
public:
    X(int ii=0):i(ii)
    {
        cout<<"this="<<this<<endl;
    }
    ~X()
    {
        cout<<"X::~X():"<<this<<endl;
    }
    void *operator new(size_t,void *loc)
    {
        return loc;
    }
    int i;
 
};
int main( )
{    
    int arr[10];
    cout<<"arr="<<arr<<endl;
    X *xp=new(arr)X(47);//X at location arr
    cout<<xp->i<<endl;
    xp->~X();//顯示調用析構函數
    return 0;
}
輸出:


函數說明:

(1)調用時,關鍵字new後時參數表(沒有size_t參數,它由編譯器處理),參數表後面是正在建立的對象的類名。

(2)operator new()僅返回了傳遞給它的指針。所以調用者能夠決定將對象放置在哪裏,這是在該指針所指的那塊內存上,做爲new表達式一部分的構造函數被調用。

(3)由於不是在堆上分配的內存,所以不能用動態內存機制釋放內存。能夠顯示的調用析構函數(在其它狀況下不要顯示的調用析構函數)。

二、對於new,若是第一個函數(operator new)調用成功,第二個函數(構造函數)卻拋出異常,則系統會調用第一個函數對應的operator delete版本,若沒有則系統什麼都不會作,會發生內存泄露。所以若重載一個帶額外參數的operator new,那麼也要定義一個帶相同額外參數的operator delete。若是但願這些函數有着日常行爲,只要令專屬版本調用global版本便可。
     可是,若是沒有在構造函數裏拋出異常,則placement delete不會被調用。delete一個指針,調用的是正常形式(沒有額外參數)的delete。


class Widget
{
public:
    ...
    static void* operator new(std::size_t size,std::ostream &logStream)
        throw(std::bad_alloc);
    static void operator delete(void*pMem,std::ostream&logStream) throw();
    static void operator delete(void* pMem)throw();
    ...
    
};
Widget *pw=new(std::cerr)Widget;
delete pw;
三、對於上述示例,須要注意避免讓類專屬的news掩蓋其它的news。例如,對於上式:Widget *pw=new Widget;將被掩蓋。一樣道理,派生類中的operator news會掩蓋global版本和繼承而得的operator new版本。能夠定義一個基類,包含全部正常形式的new和delete。而後本身定義的類繼承這個基類,而且在類中中using聲明取得標準形式。

參考資料:

一、《C++編程思想》

二、《Effective C++》

相關文章
相關標籤/搜索