天天看點

More Effective C++之 Item M8:了解各種不同含義的new和delete

人們有時好像喜歡故意使C++語言的術語難以了解。比如說new操作符(new operator)和new操作(operator new)的差別。

    當你寫這樣的代碼:

string *ps = new string("Memory Management");

你使用的new是new操作符。這個操作符就象sizeof一樣是語言内置的,你不能改變它的含義,它的功能總是一樣的。它要完成的功能分成兩部分。第一部分是配置設定足夠的記憶體以便容納所需類型的對象。第二部分是它調用構造函數初始化記憶體中的對象。new操作符總是做這兩件事情,你不能以任何方式改變它的行為。

    你所能改變的是如何為對象配置設定記憶體。new操作符調用一個函數來完成必需的記憶體配置設定,你能夠重寫或重載這個函數來改變它的行為。new操作符為配置設定記憶體所調用函數的名字是operator new。

    函數operator new 通常這樣聲明:

void * operator new(size_t size);

    傳回值類型是void*,因為這個函數傳回一個未經處理(raw)的指針,未初始化的記憶體。(如果你喜歡,你能寫一種operator new函數,在傳回一個指針之前能夠初始化記憶體以存儲一些數值,但是一般不這麼做。)參數size_t确定配置設定多少記憶體。你能增加額外的參數重載函數operator new,但是第一個參數類型必須是size_t。(有關operator new更多的資訊參見Effective C++ 條款8至條款10。)

    你一般不會直接調用operator new,但是一旦這麼做,你可以象調用其它函數一樣調用它:

void *rawMemory = operator new(sizeof(string));

    操作符operator new将傳回一個指針,指向一塊足夠容納一個string類型對象的記憶體。

    就象malloc一樣,operator new的職責隻是配置設定記憶體。它對構造函數一無所知。operator new所了解的是記憶體配置設定。把operator new 傳回的未經處理的指針傳遞給一個對象是new操作符的工作。當你的編譯器遇見這樣的語句:

string *ps = new string("Memory Management");

它生成的代碼或多或少與下面的代碼相似(更多的細節見Effective C++條款8和條款10,還有我的文章Counting object裡的注釋。):

void *memory =                              // 得到未經處理的記憶體

  operator new(sizeof(string));             // 為String對象

call string::string("Memory Management")    //初始化

on *memory;                                 // 記憶體中

                                            // 的對象

string *ps =                                // 是ps指針指向

  static_cast<string*>(memory);             // 新的對象

    注意第二步包含了構造函數的調用,你做為一個程式員被禁止這樣去做。你的編譯器則沒有這個限制,它可以做它想做的一切。是以如果你想建立一個堆對象就必須用new操作符,不能直接調用構造函數來初始化對象。

l         placement new

    有時你确實想直接調用構造函數。在一個已存在的對象上調用構造函數是沒有意義的,因為構造函數用來初始化對象,而一個對象僅僅能在給它初值時被初始化一次。但是有時你有一些已經被配置設定但是尚未處理的(raw)記憶體,你需要在這些記憶體中構造一個對象。你可以使用一個特殊的operator new ,它被稱為placement new。

    下面的例子是placement new如何使用,考慮一下:

class Widget {

public:

  Widget(int widgetSize);

  ...

};

Widget * constructWidgetInBuffer(void *buffer,

                                 int widgetSize)

{

  return new (buffer) Widget(widgetSize);

}

    這個函數傳回一個指針,指向一個Widget對象,對象在轉遞給函數的buffer裡配置設定。當程式使用共享記憶體或memory-mapped I/O時這個函數可能有用,因為在這樣程式裡對象必須被放置在一個确定位址上或一塊被例程配置設定的記憶體裡。(參見條款M4,一個如何使用placement new的一個不同例子。)

    在constructWidgetInBuffer裡面,傳回的表達式是:

new (buffer) Widget(widgetSize)

這初看上去有些陌生,但是它是new操作符的一個用法,需要使用一個額外的變量(buffer),當new操作符隐含調用operator new函數時,把這個變量傳遞給它。被調用的operator new函數除了待有強制的參數size_t外,還必須接受void*指針參數,指向構造對象占用的記憶體空間。這個operator new就是placement new,它看上去象這樣:

void * operator new(size_t, void *location)

{

  return location;

}

    這可能比你期望的要簡單,但是這就是placement new需要做的事情。畢竟operator new的目的是為對象配置設定記憶體然後傳回指向該記憶體的指針。在使用placement new的情況下,調用者已經獲得了指向記憶體的指針,因為調用者知道對象應該放在哪裡。placement new必須做的就是傳回轉遞給它的指針。(沒有用的(但是強制的)參數size_t沒有名字,以防止編譯器發出警告說它沒有被使用;見條款M6。) placement new是标準C++庫的一部分(見Effective C++ 條款49)。為了使用placement new,你必須使用語句#include <new>(或者如果你的編譯器還不支援這新風格的頭檔案名(再參見Effective C++ 條款49),<new.h>)。

    讓我們從placement new回來片刻,看看new操作符(new operator)與operator new的關系,你想在堆上建立一個對象,應該用new操作符。它既配置設定記憶體又為對象調用構造函數。如果你僅僅想配置設定記憶體,就應該調用operator new函數;它不會調用構造函數。如果你想定制自己的在堆對象被建立時的記憶體配置設定過程,你應該寫你自己的operator new函數,然後使用new操作符,new操作符會調用你定制的operator new。如果你想在一塊已經獲得指針的記憶體裡建立一個對象,應該用placement new。

    (有關更多的不同的new與delete的觀點參見Effective C++ 條款7和我的文章Counting objects。)

l         Deletion and Memory Deallocation

    為了避免記憶體洩漏,每個動态記憶體配置設定必須與一個等同相反的deallocation對應。函數operator delete與delete操作符的關系與operator new與new操作符的關系一樣。當你看到這些代碼:

string *ps;

...

delete ps;                          // 使用delete 操作符

你的編譯器會生成代碼來析構對象并釋放對象占有的記憶體。

    Operator delete用來釋放記憶體,它被這樣聲明:

void operator delete(void *memoryToBeDeallocated);

    是以,

delete ps;

    導緻編譯器生成類似于這樣的代碼:

ps->~string();                      // call the object's dtor

operator delete(ps);                // deallocate the memory

                                    // the object occupied

    這有一個隐含的意思是如果你隻想處理未被初始化的記憶體,你應該繞過new和delete操作符,而調用operator new 獲得記憶體和operator delete釋放記憶體給系統:

void *buffer =                      // 配置設定足夠的

  operator new(50*sizeof(char));      // 記憶體以容納50個char

                                 //沒有調用構造函數

...

operator delete(buffer);              // 釋放記憶體

                                 // 沒有調用析構函數

這與在C中調用malloc和free等同。

    如果你用placement new在記憶體中建立對象,你應該避免在該記憶體中用delete操作符。因為delete操作符調用operator delete來釋放記憶體,但是包含對象的記憶體最初不是被operator new配置設定的,placement new隻是傳回轉遞給它的指針。誰知道這個指針來自何方?而你應該顯式調用對象的析構函數來解除構造函數的影響:

// 在共享記憶體中配置設定和釋放記憶體的函數

void * mallocShared(size_t size);

void freeShared(void *memory);

void *sharedMemory = mallocShared(sizeof(Widget));

Widget *pw =                                   // 如上所示,

  constructWidgetInBuffer(sharedMemory, 10);   // 使用

                                               // placement new

...

delete pw;            // 結果不确定! 共享記憶體來自

                      // mallocShared, 而不是operator new

pw->~Widget();        // 正确。 析構 pw指向的Widget,

                      // 但是沒有釋放

                      //包含Widget的記憶體

freeShared(pw);       // 正确。 釋放pw指向的共享記憶體

                      // 但是沒有調用析構函數

    如上例所示,如果傳遞給placement new的raw記憶體是自己動态配置設定的(通過一些不常用的方法),如果你希望避免記憶體洩漏,你必須釋放它。(參見我的文章Counting objects裡面關于placement delete的注釋。)

l         Arrays

    到目前為止一切順利,但是還得接着走。到目前為止我們所測試的都是一次建立一個對象。怎樣配置設定數組?會發生什麼?

string *ps = new string[10];          // allocate an array of

                                // objects

    被使用的new仍然是new操作符,但是建立數組時new操作符的行為與單個對象建立有少許不同。第一是記憶體不再用operator new配置設定,代替以等同的數組配置設定函數,叫做operator new[](經常被稱為array new)。它與operator new一樣能被重載。這就允許你控制數組的記憶體配置設定,就象你能控制單個對象記憶體配置設定一樣(但是有一些限制性說明,參見Effective C++ 條款8)。

    (operator new[]對于C++來說是一個比較新的東西,是以你的編譯器可能不支援它。如果它不支援,無論在數組中的對象類型是什麼,全局operator new将被用來給每個數組配置設定記憶體。在這樣的編譯器下定制數組記憶體配置設定是困難的,因為它需要重寫全局operator new。這可不是一個能輕易接受的任務。預設情況下,全局operator new處理程式中所有的動态記憶體配置設定,是以它行為的任何改變都将有深入和普遍的影響。而且全局operator new有一個正常的簽名(normal signature)(也就單一的參數size_t,參見Effective C++條款9),是以如果你決定用自己的方法聲明它,你立刻使你的程式與其它庫不相容(參見條款M27)基于這些考慮,在缺乏operator new[]支援的編譯器裡為數組定制記憶體管理不是一個合理的設計。)

    第二個不同是new操作符調用構造函數的數量。對于數組,在數組裡的每一個對象的構造函數都必須被調用:

string *ps =               // 調用operator new[]為10個

  new string[10];          // string對象配置設定記憶體,

                           // 然後對每個數組元素調用

                           // string對象的預設構造函數。

    同樣當delete操作符用于數組時,它為每個數組元素調用析構函數,然後調用operator delete來釋放記憶體。

    就象你能替換或重載operator delete一樣,你也替換或重載operator delete[]。在它們重載的方法上有一些限制。請參考優秀的C++教材。(有關優秀的C++教材的資訊,參見本書285頁的推薦)

    new和delete操作符是内置的,其行為不受你的控制,凡是它們調用的記憶體配置設定和釋放函數則可以控制。當你想定制new和delete操作符的行為時,請記住你不能真的做到這一點。你隻能改變它們為完成它們的功能所采取的方法,而它們所完成的功能則被語言固定下來,不能改變。(You can modify how they do what they do, but what they do is fixed by the language)