《Effective C++》第8章 定製new和delete-讀書筆記

QingLiXueShi發表於2015-04-18

章節回顧:

《Effective C++》第1章 讓自己習慣C++-讀書筆記

《Effective C++》第2章 構造/析構/賦值運算(1)-讀書筆記

《Effective C++》第2章 構造/析構/賦值運算(2)-讀書筆記

《Effective C++》第3章 資源管理(1)-讀書筆記

《Effective C++》第3章 資源管理(2)-讀書筆記

《Effective C++》第4章 設計與宣告(1)-讀書筆記

《Effective C++》第4章 設計與宣告(2)-讀書筆記

《Effective C++》第5章 實現-讀書筆記

《Effective C++》第8章 定製new和delete-讀書筆記


 

條款49:瞭解new-handler的行為

當operator new無法滿足某一記憶體分配需求時,它會丟擲異常,當其丟擲異常以反應一個未獲滿足的記憶體需求之前,會先呼叫一個客戶指定的錯誤處理函式,一個所謂的new-handler。為了指定這個“用以處理記憶體不足”的函式,客戶必須呼叫set_new_handler。

namespace std
{
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) throw();
}

說明:

(1)set_new_handler是宣告於<new>的一個標準庫函式。

(2)throw()是一份異常明細,表示該函式不丟擲任何異常。

(3)形參p指向operator new無法分配足夠記憶體時該被呼叫的函式,返回指標指向set_new_handler被呼叫前正在執行(但馬上就要被替換)的那個new-handler函式。

(4)set_new_handler的例子:

void OutOfMem()
{
    cerr << "Unable to satisfy request for memory" << endl;
    abort();
}
int main()
{
    set_new_handler(OutOfMem);
    int *ptr = new int[100000000L];

    return 0;
}

(5)當operator new無法滿足記憶體申請時,它會不斷呼叫new-handler函式,直到找到足夠記憶體。

 

1、一個設計良好的new-handler函式必須做以下事情:

(1)讓更多記憶體被使用。

這可能使得operator new中下一次記憶體分配的嘗試成功。實現這一策略的一個方法是在程式啟動時分配一大塊記憶體,然後在new-handler第一次被呼叫時釋放它供程式使用。

(2)安裝另一個new-handler。

如果當前的new-handler不能做到使更多的記憶體可用,或許它知道有一個不同的 new-handler 可以做到。

(3)解除安裝new-handler。

即將空指標傳給set_new_handler,當記憶體分配不成功時,operator new將丟擲一個異常。

(4)丟擲bad_alloc(或派生自bad_alloc)的異常。

這樣的異常不會被operator new捕獲,因此會被傳播到記憶體請求的地方。

(5)不返回。

通常呼叫abort或exit。

 

2、以不同方式處理分配記憶體失敗

只要讓每一個class提供set_new_handler和operator new的自己的版本即可。operator new無法分配足夠記憶體時應丟擲bad_alloc異常,但也可能返回null(傳統形式仍然保留),這種傳統形式稱為“nothrow”。

class Widget { ... };
Widget *pw1 = new Widget;            // throws bad_alloc if allocation fails
if (pw1 == 0) ...                    // this test must fail
Widget *pw2 =new (std::nothrow)  Widget;    // returns 0 if allocation for the Widget fails
if (pw2 == 0) ...                            // this test may succeed

請記住:

(1)set_new_handler允許客戶指定一個函式,在記憶體分配無法獲得滿足時被呼叫。

(2)nothrow new是一個侷限的工具,因為它只適用於記憶體分配,後繼的建構函式呼叫還是可能丟擲異常。

———————————————————————————————————————————————————————

 

條款50:瞭解new和delete的合理替換時機

為什麼有些人想要替換編譯器提供的operator new或operator delete 版本呢?原因有:

(1)為了檢測運用錯誤;(2)為了收集動態分配記憶體之使用統計資訊;(3)為了增加分配和歸還的速度;(4)為了降低預設記憶體管理器帶來的空間額外開銷;(5)為了彌補預設分配器中的非最佳對齊;(6)為了將相關物件成簇集中;(7)為了獲得非傳統的行為。

———————————————————————————————————————————————————————

 

條款51:編寫new和delete時需固守常規

1、operator new

實現一致性operator new需要考慮:

(1)必須返回正確的值,記憶體不足時呼叫new-handling函式。

(2)必須有對付0記憶體需求的準備。

(3)避免不慎掩蓋正常形式的new。

下面分別說明:

(1)如果operator new有能力供應客戶申請的記憶體,就返回一個指標指向那塊記憶體,如果沒有就丟擲一個bad_alloc異常。

注意:只有當指向new-handling函式的指標是null,operator new才會丟擲異常。

(2)即使客戶要求0bytes,operator new也要返回一個合法指標,這種行為是為了簡化語言其他部分。下面是個operator new偽碼:

void * operator new(std::size_t size) throw(std::bad_alloc)
{                                            // your operator new might
    using namespace std;                    // take additional params
    if (size == 0) {                        // handle 0-byte requests
        size = 1;                            // by treating them as 1-byte requests
    }
    while (true) {
        attempt to allocate size bytes;
        if (the allocation was successful)
            return (a pointer to the memory);
        // allocation was unsuccessful; find out what the
        // current new-handling function is (see below)
        new_handler globalHandler = set_new_handler(0);
        set_new_handler(globalHandler);
        if (globalHandler) (*globalHandler)();
        else throw std::bad_alloc();
    }
}

說明:

1)把0bytes申請量視為1byte申請量,畢竟客戶多久才會發出一個0bytes申請。

2)將new-handling函式指標設為null而後又立即恢復原樣,是因為沒有辦法可以直接取得new-handling函式指標,所以必須呼叫set_new_handler找出它來。這種做法在單執行緒環境下有效,多執行緒環境下或許需要某種鎖以便安全處置new-handling函式背後的資料結構。

下面考慮operator new成員函式被繼承會發生什麼情況:

如果Base class專屬的operator new並非被設計用來處理上述情況。最佳做法是採用標準operator new:

void * Base::operator new(std::size_t size) throw(std::bad_alloc)
{
    if (size != sizeof(Base))  // if size is "wrong,"
        return ::operator new(size) ; // have standard operator
    // new handle the request
    ... // otherwise handle
        // the request here
}

 

2、operator delete

C++保證“刪除null指標永遠安全”,所以你必須兌現這項保證。

class Base { // same as before, but now
public: // operator delete is declared
    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 delete(void *rawMemory, std::size_t size) throw()
{
    if (rawMemory == 0) return; // check for null pointer
    if (size != sizeof(Base)) {  // if size is "wrong,"
        ::operator delete(rawMemory);  // have standard operator
        return;  // delete handle the request
    }
    deallocate the memory pointed to by rawMemory;
    return;
}

請記住:

(1)operator new應該內含一個無窮迴圈,並在其中嘗試分配記憶體,如果它無法滿足記憶體需求,就該呼叫new-handler。它也應該有能力處理0bytes申請。class專屬版本還應該處理“比正確大小更大的(錯誤)申請”。

(2)operator delete應該在收到null指標時不做任何事。class專屬版本還應該處理“比正確大小更大的(錯誤)申請”。

———————————————————————————————————————————————————————

 

條款52:寫了placement new也要寫placement delete

當你寫一個new表示式:

Widget *pw = new Widget;

共有兩個函式被呼叫:第一個是operator new用於分配記憶體,第二個是Widget的default 建構函式。

假設第一個呼叫成功,第二個呼叫導致丟擲一個異常。那麼在第1步中完成的記憶體分配必須被撤銷,否則記憶體洩漏。但客戶不可能回收這些記憶體,因為如果Widget的建構函式丟擲一個異常,pw根本就沒有被賦值。對於客戶來說無法得到指向應該被回收的記憶體指標。所以撤銷第1步的職責必然落在了C++ 執行時系統身上。C++ 執行時系統會呼叫第1步operator new相應的operator delete,但只有在它知道哪一個operator delete(可能有許多個)該被呼叫。

(1)對於正常的operator new,執行時系統可以找到對應的operator delete。常規的operator new

void* operator new(std::size_t) throw(std::bad_alloc);

對應常規的operator delete:

void operator delete(void *rawMemory) throw();                        // normal signature at global scope
void operator delete(void *rawMemory, std::size_t size) throw();    // typical normal signature at class scope 

(2)當宣告operator new的非常規形式(帶有額外引數)的時候,問題就出現了。假設編寫了一個類專用的operator new,但編寫了一個常規的operator delete:

class Widget {
public:
    ...
        static void* operator new(std::size_t size, // non-normal
        std::ostream& logStream)                    // form of new
        throw(std::bad_alloc);
    static void operator delete(void *pMemory    // normal class-
        std::size_t size) throw();                // specific form
                                                // of delete
    ...
};

說明:如果operator new接受的引數除了size_t之外還有其他,稱為placement new。比較有用的一個placement new版本是“接受一個指標指向物件該被構造之處”。

void* operator new(std::size_t, void *pMemory) throw();

 

考慮以下程式碼:

Widget *pw = new (std::cerr) Widget; // call operator new, passing cerr as
                                    // the ostream; this leaks memory
                                    // if the Widget constructor throws

如果記憶體分配成功,建構函式丟擲異常。執行時系統尋找“引數個數和型別都與operator new相同”的某個operator delete,如果找到則呼叫。顯然,這裡沒有找到,所以執行時系統無法取消分配的記憶體。對應於placement new的placement delete版本類似於:

void operator delete(void *, std::ostream&) throw();

然而如果建構函式沒有丟擲異常,客戶程式碼為delete pw,呼叫的是正常形式的operator delete,而非placement版本。

注意:只有在呼叫一個與placement new相關聯的建構函式丟擲異常,placement delete才會被呼叫。所以如果要處理所有與placement new相關的記憶體洩露,必須同時提供正常的operator delete和placement版本。

 

另外需要注意的是,成員函式名稱會掩蓋其外圍作用域中的相同名稱,所以要避免讓class專屬的news遮蓋客戶期望的其他news(包括正常版本)。

class Base {
public:
    ...
        static void* operator new(std::size_t size, // this new hides the normal global forms
        std::ostream& logStream) 
        throw(std::bad_alloc); 
    ...
};
Base *pb = new Base;                // error! the normal form of
                                    // operator new is hidden
Base *pb = new (std::cerr) Base;    // fine, calls Base's
                                    // placement new

同樣道理,derived classes中的operator news會遮蓋globle版本和繼承而得到的operator new。

class Derived: public Base {                        // inherits from Base above
public:
    ...
        static void* operator new(std::size_t size) // redeclares the normal
        throw(std::bad_alloc);                        // form of new
    ...
};
Derived *pd = new (std::clog) Derived;    // error! Base's placement
                                        // new is hidden
Derived *pd = new Derived;                // fine, calls Derived's
                                        // operator new

 

對於撰寫記憶體分配函式,需要注意的是,預設情況下C++在globle作用域內提供的operator news形式:

void* operator new(std::size_t) throw(std::bad_alloc); // normal new
void* operator new(std::size_t, void*) throw(); // placement new
void* operator new(std::size_t,                // nothrow new — see Item 49
    const std::nothrow_t&) throw();            

如果你在class內宣告任何operator news都會遮掩上述標準形式,除非你就是故意阻止客戶使用這些形式。如果你希望這些函式可用,只要令你的class專屬版本呼叫globle版本即可。

class StandardNewDeleteForms {
public:
    // normal new/delete
    static void* operator new(std::size_t size) throw(std::bad_alloc)
    { return ::operator new(size); }
    static void operator delete(void *pMemory) throw()
    { ::operator delete(pMemory); }
    // placement new/delete
    static void* operator new(std::size_t size, void *ptr) throw()
    { return ::operator new(size, ptr); }
    static void operator delete(void *pMemory, void *ptr) throw()
    { return ::operator delete(pMemory, ptr); }
    // nothrow new/delete
    static void* operator new(std::size_t size, const std::nothrow_t& nt) throw()
    { return ::operator new(size, nt); }
    static void operator delete(void *pMemory, const std::nothrow_t&) throw()
    { ::operator delete(pMemory); }
};

class Widget: public StandardNewDeleteForms {            // inherit std forms
public:
    using StandardNewDeleteForms::operator new;            // make those
    using StandardNewDeleteForms::operator delete;        // forms visible
    static void* operator new(std::size_t size,             // add a custom
        std::ostream& logStream)                        // placement new
        throw(std::bad_alloc);
    static void operator delete(void *pMemory,            // add the corresponding placement delete
        std::ostream& logStream)                        
        throw(); 
    ...
};

請記住:

(1)當你寫operator new的placement版本時,確保同時編寫operator delete相應的placement版本。否則,你的程式可能會發生微妙的,斷續的記憶體洩漏。

(2)當你宣告new和delete的placement版本時,確保不會無意中覆蓋這些函式的常規版本。

———————————————————————————————————————————————————————

 

我感覺寫的有點流水賬。但其實每次拜讀這本書感受都有點不同,比如有的問題,自己知道這樣做肯定行,但是就是想不明白為什麼這麼麻煩著做。可能是沒有經驗吧。這篇總結,先記錄到這裡,我還要回顧修改的。看起來有點亂糟糟的。

相關文章