C++重寫new和delete,比想像中困難

  關於C++內存管理這話題,永遠都不過期。在我剛出道的時候,就已經在考慮怎麼檢測內存泄漏(http://www.javashuo.com/article/p-kdnxlkew-z.html)。想用一份簡單的代碼,而且不太影響執行效率去實現內存泄漏檢測,是不太現實的。當時以爲重寫new和delete是沒有太大價值的,不事後來在本身的項目中仍是重寫了,加了個計數。在程序退出時檢測下計數new的次數和delete次數是否對得上,對不上就是有問題了,再用valgrind之類的工具去檢測。這種排除不了全部狀況,但確實也解決了一些問題。畢竟每次寫新功能時發現問題立馬去解決,比你寫了成千上萬個功能,上線後出問題再查找容易得多。html

  在windows下則有另外一種方案, C Run-time Library (CRT) debug,_CrtDumpMemoryLeaks()函數,這也僅僅是發現泄漏,定位還得用另外一個工具visual leak detector。最近在解決公司程序內存泄漏過程當中,發現其實並無內存泄漏,而是程序是在atexit裏調用_CrtDumpMemoryLeaks()函數的,而static變量申請的內存,可能要在atexit回調以後釋放。由此,我突然想到我之前重寫new和delete,有些地方寫得並不對,在這裏從新整理一下。ios

  new、delete並非一個函數,它在編譯的時候會被解析成三個步驟:1.調用operator new分配內存;2.調用構造函數;3.把指針轉換成對應的類型返回。可以重寫的,是operator new函數。git

#include <cstdlib>
#include <iostream>

int g_counter  = 0;

void *operator new(size_t size)
{

    ++g_counter;
    std::cout << "new mem:" << g_counter << std::endl;

    return ::malloc(size);
}

void operator delete(void* ptr)
{
    --g_counter;
    std::cout << "delete mem:" << g_counter << std::endl;

    ::free(ptr);
}

void on_exit()
{
    std::cout << "exit,mem counter = " << g_counter << std::endl;
}

int main()
{
    atexit(on_exit);

    char *ptr = new char[8];

    return 0;
}
$ g++ main.cpp$ ./a.out 
new mem:1
exit,mem counter = 1

上面簡單地重寫了operator new和operator delete,在程序退出時能夠檢測到還有一次內存沒釋放掉。但上面的代碼存在不少問題。github

 

1. 儘可能重寫全部函數web

  C++的operator new和operator delete函數一般比你想像中的多。並且不一樣的版本會帶來不一樣的函數,1七、20版本都相應的增長了一些函數,參考https://en.cppreference.com/w/cpp/memory/new/operator_new。 若是你沒有重寫完,雖然能編譯經過,但可能並非你想要的結果。好比上面的代碼,new char[8]本應該調用operator new[]函數的,因爲沒有重寫operator new[],默認調用了libstdcxx中的operator new[],默認函數又調用了operator new。雖然這不必定有什麼問題,但在某些項目中,對內存分配作了特殊處理,或者一些特殊操做(好比一個內存池重寫了operator new[]但沒重寫operator delete[],而他們的內存是回收到不一樣地方),這就會出問題。windows

 

2. 利用atexit統計內存並不許確安全

  atexit是在程序退出時調用,對絕大多數變量來講都是OK的,但對static和global變量則不必定了。根據C++標準:https://isocpp.org/files/papers/N3690.pdf 3.6.3 Termination,atexit註冊以前就已經建立的變量,則在atexit以後釋放,這意味着你的static和global變量若是new了內存必須在atexit以後建立。但這又引出C++的另外一個問題:static initialization order fiasco。固然咱們有不少方法去處理它,好比把全部static和global放到一個cpp文件裏,或者在程序退出時手動釋放new的內存。另外,gcc連接的時候,放在最後的object文件裏的global變量會優先初始化,或者用gcc的__attribute__ ((init_priority (N)))屬性來指定初始化優先級,但這不是標準,不過這畢竟是值得注意的地方。多線程

 

3. 線程安全函數

  上面的代碼沒有加鎖,因此是不能用在多線程中的。但如今有幾個程序不用多線程的,因此仍是得把鎖加上,加鎖的代碼很簡單。工具

static pthread_mutex_t *counter_mutex()
{
    static pthread_mutex_t _mutex;
    assert( 0 == pthread_mutex_init( &_mutex,NULL ) );
    return &_mutex;
}
static pthread_mutex_t *_mem_mutex_ = counter_mutex();

int g_counter  = 0;

void *operator new(size_t size)
{

    assert( _mem_mutex_ );

    pthread_mutex_lock( _mem_mutex_ );
    ++g_counter;
    pthread_mutex_unlock( _mem_mutex_ );

    std::cout << "new mem:" << g_counter << std::endl;

    return ::malloc(size);
}

 

4. 線程安全帶來初始化問題

  在上面說atexit統計內存不許確的時候提到static initialization order fiasco的問題,在這裏變得更嚴重了。由於線程安全是用一個static pthread_mutex_t指針來實現的,那麼在其餘global變量建立時若是調用了new,那麼它多是沒有被初始化的。固然若是你已按上面的方法解決了,那就不會有這個問題了。或者,根據C++標準,Static initialization初始化必須在全部Dynamic initialization以前,咱們能夠這樣寫:

/* Static initialization */
static pthread_mutex_t *_mem_mutex_ = NULL;

class global_static
{
public:
    global_static() 
    {
        assert( !_mem_mutex_ );
        _mem_mutex_ = counter_mutex();
    }

    ~global_static() {_mem_mutex_ = NULL;}
};

/* Dynamic initialization */
const static global_static gs;

這樣雖然不能解決問題,可是因爲咱們在new裏校驗了_mem_mutex_是否爲NULL,至少能發現問題。

既然是C++,那麼還能夠Construct On First Use Idiom:在使用_mem_mutex_時去檢測是否已初始化,未初始化就初始化。而不是像上面那樣全局一次初始化,之後都不用檢測。

 

5. 可否統計到STL、BOOST、so、.a等外部代碼中的new、delete是否會被重寫

  STL和BOOST這種不少時候是是模板,也就是源碼,和你項目中的代碼同樣,固然也會被重寫。對於so動態連接庫,他和程序是分離的。當你的程序加載這個so文件時,它會優先在你的程序裏查找他須要的符號,若是找到了,就會優先使用。這和LD_PRELOAD的機制是同樣的,所以也是會被重寫的。而.a這種靜態連接庫,在gcc連接時會按你傳入的庫順序查找符號,通常來講你項目中的符號都是優先於libgcc這種標準庫的,所以也是會被重寫的。

  要明白這些,要懂得gcc是如何編譯、連接一個程序的,尤爲是對符號的管理。https://akkadia.org/drepper/dsohowto.pdf

 

6. 能夠用nm來判斷是否重寫

xzc@xzc-HP-ProBook-4446s:~/Documents/code/test$ nm -C a.out | grep new
0000000000400f60 T test_static_new()
                 U operator new[](unsigned long)@@GLIBCXX_3.4
0000000000400d34 T operator new(unsigned long)
0000000000401120 r operator new(unsigned long)::__PRETTY_FUNCTION__

T表示text,說明你已經重寫了。U表示undefine,表示沒有重寫,程序運行時,要去庫裏查找這個符號。

 

   大部分人重寫operator new和operator delete的初衷,無非就是檢測內存泄漏,或者實現本身的內存管理。對於內存泄漏,經過重寫operator new來實現的,能夠看http://wyw.dcweb.cn/leakage.htm這裏,如今還在維護的項目是https://github.com/adah1972/nvwa,我沒用過,但看下邏輯應該仍是不錯的。而對於在代碼中重寫operator new來實現內存管理,我倒沒見過。畢竟想寫一個通用的內存管理不容易,寫出來也是一個庫了,好比jemalloc這種。

相關文章
相關標籤/搜索