陳碩 (giantchen_AT_gmail)
Blog.csdn.net/Solstice
本文只考慮 Linux x86 平台,服務端開發(不考慮 Windows 的跨 DLL 內存分配釋放問題)。本文假定讀者知道 ::operator new() 和 ::operator delete() 是干什麼的,與通常用的 new/delete 表達式有和區別和聯系,這方面的知識可參考侯捷先生的文章《池內春秋》[1],或者這篇文章。
C++ 的內存管理是個老生常談的話題,我在《當析構函數遇到多線程》第 7 節“插曲:系統地避免各種指針錯誤”中簡單回顧了一些常見的問題以及在現代 C++ 中的解決辦法。基本上,按現代 C++ 的手法(RAII)來管理內存,你很難遇到什麼內存方面的錯誤。“沒有錯誤”是基本要求,不代表“足夠好”。我們常常會設法優化性能,如果 profiling 表明 hot spot 在內存分配和釋放上,重載全局的 ::operator new() 和 ::operator delete() 似乎是一個一勞永逸好辦法(以下簡寫為“重載 ::operator new()”),本文試圖說明這個辦法往往行不通。
內存管理的基本要求
如果只考慮分配和釋放,內存管理基本要求是“不重不漏”:既不重復 delete,也不漏掉 delete。也就說我們常說的 new/delete 要配對,“配對”不僅是個數相等,還隱含了 new 和 delete 的調用本身要匹配,不要“東家借的東西西家還”。例如:
用系統默認的 malloc() 分配的內存要交給系統默認的 free() 去釋放;
用系統默認的 new 表達式創建的對象要交給系統默認的 delete 表達式去析構並釋放;
用系統默認的 new[] 表達式創建的對象要交給系統默認的 delete[] 表達式去析構並釋放;
用系統默認的 ::operator new() 分配的的內存要交給系統默認的 ::operator delete() 去釋放;
用 placement new 創建的對象要用 placement delete (為了表述方便,姑且這麼說吧)去析構(其實就是直接調用析構函數);
從某個內存池 A 分配的內存要還給這個內存池。
如果定制 new/delete,那麼要按規矩來。見 Effective C++ 相關條款。
做到以上這些不難,是每個 C++ 開發人員的基本功。不過,如果你想重載全局的 ::operator new(),事情就麻煩了。
重載 ::operator new() 的理由
Effective C++ 第三版第 50 條列舉了定制 new/delete 的幾點理由:
檢測代碼中的內存錯誤
優化性能
獲得內存使用的統計數據
這些都是正當的需求,文末我們將會看到,不重載 ::operator new() 也能達到同樣的目的。
::operator new() 的兩種重載方式
1. 不改變其簽名,無縫直接替換系統原有的版本,例如:
#include
void* operator new(size_t size);
void operator delete(void* p);
用這種方式的重載,使用方不需要包含任何特殊的頭文件,也就是說不需要看見這兩個函數聲明。“性能優化”通常用這種方式。
2. 增加新的參數,調用時也提供這些額外的參數,例如:
void* operator new(size_t size, const char* file, int line); // 其返回的指針必須能被普通的 ::operator delete(void*) 釋放
void operator delete(void* p, const char* file, int line); // 這個函數只在析構函數拋異常的情況下才會被調用
然後用的時候是
Foo* p = new (__FILE, __LINE__) Foo; // 這樣能跟蹤是哪個文件哪一行代碼分配的內存
我們也可以用宏替換 new 來節省打字。用這第二種方式重載,使用方需要看到這兩個函數聲明,也就是說要主動包含你提供的頭文件。“檢測內存錯誤”和“統計內存使用情況”通常會用這種方式重載。當然,這不是絕對的。
在學習 C++ 的階段,每個人都可以寫個一兩百行的程序來驗證教科書上的說法,重載 ::operator new() 在這樣的玩具程序裡邊不會造成什麼麻煩。
不過,我認為在現實的產品開發中,重載 ::operator new() 乃是下策,我們有更簡單安全的辦法來到達以上目標。
現實的開發環境
作為 C++ 應用程序的開發人員,在編寫稍具規模的程序時,我們通常會用到一些 library。我們可以根據 library 的提供方把它們大致分為這麼幾大類:
C 語言的標准庫,也包括 Linux 編程環境提供的 Posix 系列函數。
第三方的 C 語言庫,例如 OpenSSL。
C++ 語言的標准庫,主要是 STL。(我想沒有人在產品中使用 IOStream 吧?)
第三方的通用 C++ 庫,例如 Boost.Regex,或者某款 XML 庫。
公司其他團隊的人開發的內部基礎 C++ 庫,比如網絡通信和日志等基礎設施。
本項目組的同事自己開發的針對本應用的基礎庫,比如某三維模型的仿射變換模塊。
在使用這些 library 的時候,不可避免地要在各個 library 之間交換數據。比方說 library A 的輸出作為 library B 的輸入,而 library A 的輸出本身常常會用到動態分配的內存(比如 std::vector)。
如果所有的 C++ library 都用同一套內存分配器(就是系統默認的 new/delete ),那麼內存的釋放就很方便,直接交給 delete 去釋放就行。如果不是這樣,那就得時時刻刻記住“這一塊內存是屬於哪個分配器,是系統默認的還是我們定制的,釋放的時候不要還錯了地方”。
(由於 C 語言不像 C++ 一樣提過了那麼多的定制性,C library 通常都會默認直接用 malloc/free 來分配和釋放內存,不存在上面提到的“內存還錯地方”問題。或者有的考慮更全面的 C library 會讓你注冊兩個函數,用於它內部分配和釋放內存,這就就能完全掌控該 library 的內存使用。這種依賴注入的方式在 C++ 裡變得花哨而無用,見陳碩寫的《C++ 標准庫中的allocator是多余的》。)
但是,如果重載了 ::operator new(),事情恐怕就沒有這麼簡單了。
重載 ::operator new() 的困境
首先,重載 ::operator new() 不會給 C 語言的庫帶來任何麻煩,當然,重載它得到的三點好處也無法讓 C 語言的庫享受到。
以下僅考慮 C++ library 和 C++ 主程序。
規則 1:絕對不能在 library 裡重載 ::operator new()
如果你是某個 library 的作者,你的 library 要提供給別人使用,那麼你無權重載全局 ::operator new(size_t) (注意這是上面提到的第一種重載方式),因為這非常具有侵略性:任何用到你的 library 的程序都被迫使用了你重載的 ::operator new(),而別人很可能不願意這麼做。另外,如果有兩個 library 都試圖重載 ::operator new(size_t),那麼它們會打架,我估計會發生 duplicated symbol link error。干脆,作為 library 的編寫者,大家都不要重載 ::operator new(size_t) 好了。
那麼第二種重載方式呢?首先,::operator new(size_t size, const char* file, int line) 這種方式得到的 void* 指針必須同時能被 ::operator delete(void*) 和 ::operator delete(void* p, const char* file, int line) 這兩個函數釋放。這時候你需要決定,你的 ::operator new(size_t size, const char* file, int line) 返回的指針是不是兼容系統默認的 ::operator delete(void*)。
如果不兼容(也就是說不能用系統默認的 ::operator delete(void*) 來釋放內存),那麼你得重載 ::operator delete(void*),讓它的行為與你的 operator new(size_t size, const char* file, int line) 匹配。一旦你決定重載 ::operator delete(void*),那麼你必須重載 ::operator new(size_t),這就回到了情況 1:你無權重載全局 ::operator new(size_t)。
如果選擇兼容系統默認的 ::operator delete(void*),那麼你在 operator new(size_t size, const char* file, int line) 裡能做的事情非常有限,比方說你不能額外動態分配內存來做 house keeping 或保存統計數據(無論顯示還是隱式),因為系統默認的 ::operator delete(void*) 不會釋放你額外分配的內存。(這裡隱式分配內存指的是往 std::map<> 這樣的容器裡添加元素。)
看到這裡估計很多人已經暈了,但這還沒完。
其次,在 library 裡重載 operator new(size_t size, const char* file, int line) 還涉及到你的重載要不要暴露給 library 的使用者(其他 library 或主程序)。這裡“暴露”有兩層意思:1) 包含你的頭文件的代碼會不會用你重載的 ::operator new(),2) 重載之後的 ::operator new() 分配的內存能不能在你的 library 之外被安全地釋放。如果不行,那麼你是不是要暴露某個接口函數來讓使用者安全地釋放內存?或者返回 shared_ptr ,利用其“捕獲”deleter 的特性?聽上去好像挺復雜?這裡就不一一展開討論了,總之,作為 library 的作者,絕對不要動“重載 operator new()”的念頭。
事實 2:在主程序裡重載 ::operator new() 作用不大
這不是一條規則,而是我試圖說明這麼做沒有多大意義。
如果用第一種方式重載全局 ::operator new(size_t),會影響本程序用到的所有 C++ library,這麼做或許不會有什麼問題,不過我建議你使用下一節介紹的更簡單的“替代辦法”。
如果用第二種方式重載 ::operator new(size_t size, const char* file, int line),那麼你的行為是否惠及本程序用到的其他 C++ library 呢?比方說你要不要統計 C++ library 中的內存使用情況?如果某個 library 會返回它自己用 new 分配的內存和對象,讓你用完之後自己釋放,那麼是否打算對錯誤釋放內存做檢查?
C++ library 從代碼組織上有兩種形式:1) 以頭文件方式提供(如以 STL 和 Boost 為代表的模板庫);2) 以頭文件+二進制庫文件方式提供(大多數非模板庫以此方式發布)。
對於純以頭文件方式實現的 library,那麼你可以在你的程序的每個 .cpp 文件的第一行包含重載 ::operator new 的頭文件,這樣程序裡用到的其他 C++ l