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


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

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

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

#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,在程序退出時可以檢測到還有一次內存沒釋放掉。但上面的代碼存在很多問題。

 

1. 盡量重寫所有函數

  C++的operator new和operator delete函數通常比你想像中的多。而且不同的版本會帶來不同的函數,17、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[],而他們的內存是回收到不同地方),這就會出問題。

 

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這種。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM