1. 程式人生 > >C++動態記憶體:(二)過載new和delete

C++動態記憶體:(二)過載new和delete

一、過載的原因

    用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)統計使用動態記憶體的資訊。

(5)為了降低預設記憶體管理器帶來的空間額外開銷。

(6)為了彌補預設分配器中的非最佳齊位。

(7)為了將相關物件成簇集中。例如,為了將特定的某個資料結構在儀器使用,並且使用時缺頁中斷頻率降至最低。new和delete的palcement版本有可能完成這樣的集簇行為。

(8)獲得非傳統的行為。例如:分配和歸還共享記憶體。

 二、過載全域性的new和delete

 1、過載了全域性的new和delete後將使預設的new和delete不能載被訪問,甚至在這個重新定義裡也不能呼叫它們。

 2、過載的new必須有一個size_t引數。這個引數由編譯器產生並傳遞給我們,它是要分配的物件的長度。函式返回值為一個大於等於這個物件長度的指標。

 3、operator new()的返回值是一個void*,而不是指向任何特定型別的指標。所做的是分配記憶體,而不是完成一個物件建立——直到建構函式呼叫了才完成了物件的建立,它是編譯器確保的動作,不在我們的控制範圍之內。

 4、operator delete()的引數是一個指向由operator new()分配的記憶體的void*,而不是指向任何特定型別的指標。引數是一個void*是因為它是在呼叫解構函式後得到的指標。解構函式從儲存單元裡移去物件。operator delete()的返回型別是void。

5、功能示例:


#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。

說明:程式只是示範最簡單的使用方法,具體的過載還有很多細節需要考慮,具體參考第四節“注意事項”


6、全域性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();
}
7、全域性delete實現偽碼
void operator delete(void *mem)throw()
{
    if(mem==0) return;
    歸還mem所指記憶體;
}
三、對類過載new和delete

1、為一個過載new和delete,儘管不必顯式地使用static,但實際上仍在建立static成員函式。

2、當編譯器看到使用new建立自定義的類的物件時,它選擇成員版本的operator new()而不是全域性版本的new()。但如果要建立這個類的一個物件陣列時,全域性的operator new()就會被立即呼叫,用來為這個陣列分配記憶體。當然可以通過為這個類過載運算子的陣列版本,即operator new[]和operator delete[]來控制物件陣列的記憶體分配。

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

4、功能示例():


#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先呼叫解構函式然後釋放記憶體。

說明:程式只是示範最簡單的使用方法,具體的過載還有很多細節需要考慮,具體參考第四節“注意事項”。完整實現的偽碼為:


6、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。


四、注意事項

1、當過載operator new()和operator delete()時,我們只是改變了原有記憶體分配方法(過載operator new()唯一需要做的就是返回一個足夠大的記憶體塊的指標)。編譯器將用過載的new代替預設版本去分配記憶體並呼叫建構函式。

2、在過載的operator news內應該包含一個迴圈,反覆呼叫某個new_handler函式。連結:C++ new_handler和set_new_handler

3、注意資料位對齊。C++要求所有的operator news返回的指標都有適當的對齊(取決於資料型別)。malloc就是在這樣的要求下工作的,因此令operator new返回一個得自malloc的指標是安全的(呼叫malloc得到分配的記憶體塊指標後,不要再對指標進行偏移)。

4、除非必須,否則不要自己過載new和delete。因為可能會漏掉可移植性、齊位、執行緒安全等細節。必須時,可以借鑑使用一些開放原始碼的標準庫(例如Boost程式庫的Pool)。

5、客戶要求0 bytes時operator new也得返回一個合法的指標。通常的處理方法是當申請0位元組是,將它視為申請1-bytes。即


if(size==0)
{
   size=1;
}
6、重寫delete時,要保證“刪除null指標永遠安全”。

7、對於某個特定類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;
}
五、placement new和placement delete

     以上所示為普通的,只帶size_t引數的過載,但是如果 想要在指定記憶體位置上放置物件;或者想要在new時志記資訊則需要帶額外引數。

1、示例:

#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)因為不是在堆上分配的記憶體,因此不能用動態記憶體機制釋放記憶體。可以顯示的呼叫解構函式(在其它情況下不要顯示的呼叫解構函式)。

2、對於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;
3、對於上述示例,需要注意避免讓類專屬的news掩蓋其它的news。例如,對於上式:Widget *pw=new Widget;將被掩蓋。同樣道理,派生類中的operator news會掩蓋global版本和繼承而得的operator new版本。可以定義一個基類,包含所有正常形式的new和delete。然後自己定義的類繼承這個基類,並且在類中中using宣告取得標準形式。

參考資料:

1、《C++程式設計思想》

2、《Effective C++》


--------------------- 
作者:Z-H-I 
來源:CSDN 
原文:https://blog.csdn.net/zxx910509/article/details/64905107 
版權宣告:本文為博主原創文章,轉載請