撥開自訂operator new與operator delete的迷霧

來源:互聯網
上載者:User

    C++裡允許使用者通過自訂operator new的方式來更改new運算式的行為,這給了程式員定製記憶體管理方案的自由。但是享受這種自由的時候必須遵守一定的規範,具體可以參見《Effective C++ 2nd》的相關條款。本文補充解釋一些特別容易引起誤解的問題。

    operator new和operator delete都有其正規形式(normal signature):

void* operator new(size_t size);
void operator delete(void *p);
void operator delete(void *p,size_t size);

    普通的new與delete運算式在分配與釋放記憶體時調用的就是它們。一般來說operator delete(void*)的優先順序比operator delete(void*,size_t)要高,這意味著如果在同一空間(scope)定義了這兩種形式的delete,擁有單一參數者優先被編譯器選擇。這一點在VC7.1中得到驗證,不知其它編譯器如何? 

    除了上面的正規形式外,我們還可以定義擁有更多參數的operator new和operator delete,只要保證前者的傳回值和第一個參數分別是void*和size_t類型,而後者的分別是void和void*就行了。比如:

void* operator new(size_t size,const char* szFile,int nLine);
void operator delete(void *p,const char*,int);

    運算式new("xxx",20) SomeClass實際上就是告訴編譯器調用上面的多參數operator new來分配記憶體。但是不要依此類推出 delete("xxx",20) pObj,這是非法的。那麼怎麼才能調用到這個多參數的operator delete呢?實話告訴你,你沒有這個權利。呵呵,別吃驚,容我慢慢解釋。當兩個operator new和operator delete有相等的參數個數,並且除了第一個參數之外其餘參數的類型依次完全相同之時,我們稱它們為一對匹配的operator new和operator delete。按照這個標準,上面兩位就是匹配的一對了。在我們使用 SomeClass *pObj = new("xxx",20) SomeClass 於堆中構建一個對象的過程中,如果在執行SomeClass的建構函式時發生了異常,並且這個異常被捕獲了,那麼C++的異常處理機制就會自動用與被使用的operator new匹配的operator delete來釋放記憶體(補充一點:在operator new中拋出異常不會導致這樣的動作,因為系統認為這標誌著記憶體配置失敗)。編譯期間編譯器按照以下順序尋找匹配者:首先在被構建對象類的類域中尋找,然後到父類域中,最後到全域域,此過程中一旦找到即停止搜尋並用它來產生正確的記憶體釋放代碼,如果沒有找到,當發生上述異常情況時將不會有代碼用來釋放分配的記憶體,這就造成記憶體流失了。而如果一切正常,delete pObj 則總是會去調用operator delete的正規形式。現在明白了吧,多參數的operator delete不是給我們而是給系統調用的,它平常默默無聞,但在最危急的關頭卻能挺身而出,保證程式的健壯性。為了有個感性的認識,讓我們看看下面的代碼(實驗環境是VC7.1):

#include <malloc.h>

struct Base
{
    Base()
    {
        throw int(3);
    }

    ~Base() {}

    void* operator new( size_t nSize, const char*,int)
    {
        void* p = malloc( nSize );

        return p;
    } 

    void operator delete( void *p)
    {
        free(p);
    }

    void operator delete( void* p,const char*,int)
    {
        free( p );
    }
};

#define NULL 0
#define new new(__FILE__, __LINE__)

int main( void )
{
    Base* p = NULL;

    try
    {
        p = new Base;

        delete p;
    }
    catch(...)
    {
    }

    return 0;
}

    跟蹤執行會發現:程式在 p = new Base 處拋出一個異常後馬上跳去執行operator delete(void*,const char*,int)。注釋掉Base建構函式中的throw int(3)重來一遍,則new成功,然後執行delete p,這時實際調用的是Base::operator delete(void*)。以上實驗結果符合我們的預期。注意,operator new和operator delete是可以被繼承和重定義的,那接下來就看看它們在繼承體系中的表現。引進一個Base的衍生類別(代碼加在#define NULL 0的前面):

struct Son : public Base
{
    Son()
    {
    }

    void* operator new( size_t nSize, const char*,int)
    {
        // class Son

        void* p = malloc( nSize );
        return p;
    }

    void operator delete( void *p)
    {
        // class Son
        free(p);
    }

    void operator delete( void* p,const char*,int)
    {
        // class Son
        free( p );
    }
};
    然後將main函數中的p = new Base改成p = new Son並且取消對Base()中的throw int(3)的注釋,跟蹤執行,發現這回new運算式調用的是Son重定義的operator new,拋出異常後也迅速進入了正確的operator delete,即Son重定義的多參數版本。一切都如所料,是嗎?呵呵,別急著下結論,讓我們把拋異常的語句注釋掉再跑一次吧。很明顯,有些不對勁。這次delete p沒有如我們所願去調用Son::operator delete(void*),而是找到了在Base中定義的版本。怎麼回事?我願意留一分鐘讓好奇的你仔細想想。

    找到答案了嗎?沒錯,罪魁禍首就是那愚蠢的Base解構函式聲明。作為一個領導著衍生類別的基類,解構函式竟然不聲明成virtual函數,這簡直就是瀆職。趕緊糾正,在~Base()前加上一個神聖的virtual,rebuild and run.....。謝天謝地,世界終於完美了。

    可能你會疑惑,在沒有給基類解構函式加virtual之前,當發生異常時C++為什麼知道正確地調用衍生類別定義的多參數operator delete,而不是基類的?其實很簡單,new一個對象時必須提供此對象的確切類型,所以編譯器能夠在編譯期確定new運算式拋出異常後應該調用哪個類定義的operator delete。對於正常的delete p來說,如果p被聲明為非基類類型的指標,編譯器就會在編譯時間決定調用這種宣告類型定義的operator delete(靜態繫結),而如果p是某種基類類型指標,編譯器就會聰明地把到底調用哪個類定義的operator delete留待運行期決定(動態綁定)。那麼編譯器如何判斷p是否是基類指標呢?實際上它的根據就是p的宣告類型中定義的解構函式,只有在解構函式是虛擬情況下p才被看成基類指標。這就可以解釋上面碰到的問題。當時p被聲明為Base*,程式中它實際指向一個Son對象,但我們並沒有把~Base()聲明為虛擬,所以編譯器大膽地幫我們做了靜態繫結,也即產生調用Base::operator delete(void*)的代碼。不過千萬不要以為所有編譯器都會這樣做,以上分析僅僅是基於VC7.1在本次實驗中的表現。事實上在C++標準中,經由一個基類指標刪除一個衍生類別對象,而此基類卻有一個非虛擬解構函式,結果未定義。明白了吧老兄,編譯器在這種情況下沒有產生引爆你電腦的代碼已經算是相當客氣與負責了。現在你該能夠體會Scott Meyers勸你"總是讓base class擁有virtual detructor"時的苦心吧。

    至於數組版本的operator new[]和opeator delete[],情況一樣。朋友們可以自己做實驗確認一下。

    最後要指出的是,實驗代碼中對operator new和operator delete的實現相當不規範,負責任的做法仍然請大家參考Scott Meyers的著作。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.