內(nèi)存01

上古程序員

640K

  • 在 <font color=red>90年代以前</font>, 操作系統(tǒng)被 <font color=red>M-DOS</font>統(tǒng)治, 在當時的硬件環(huán)境下OS只能看到 <font color=red>1MB</font>的內(nèi)存, 但對于程序員來說, 能利用的空間只有 <font color=red>前面的640kb</font>, 雖然可用的內(nèi)存很少, 但也涌現(xiàn)了很多優(yōu)秀的軟件


錙銖必較

  • 所以當時的程序員對內(nèi)存的重視計較到1個字節(jié), 隨著工業(yè)技術(shù)和軟件技術(shù)的進步, 如今的內(nèi)存已經(jīng)是海量的,這就出現(xiàn)一個新的問題--還有必要管理內(nèi)存嗎?


內(nèi)存管理的層面

  • 嚴格來說, 內(nèi)存被os管理, 而且已經(jīng)管理的很嚴謹高效, 作為上層的程序員了解內(nèi)存的管理流程對自己的技術(shù)會有一個巨大的提升


memory primitives

內(nèi)存的層次

20.png
從圖中可以看到, c++程序員處于的位置是最上層的 applications

用的最多的是new, new[]
    如果用容器, 則內(nèi)存基本不用管理

其次也可以調(diào)用malloc

至于最底層的os的api, 沒有可移植性


它之間的調(diào)用關(guān)系如上圖的箭頭所示


函數(shù)屬性

分配 釋放 所屬 重載
malloc free c函數(shù) no
new delete c++表達式(==new操作符==) no
::operator new() ::operator delete() c++函數(shù) ==yes==
allocator<T>::allocate() allocator<T>::deallocate() STL分配器 可以自己設(shè)計搭配容器
auto p_1 = malloc(sizeof(int));
free(p_1);

stringstream* p_b = new stringstream();
delete p_b;

void* p_c = ::operator new(sizeof(int));
::operator delete(p_c);


/** 
    STL的分配器必須創(chuàng)建對象調(diào)用成員方法
    但實際管理的內(nèi)存是統(tǒng)一的
    所以 allocate和deallocate時候, 雖然對象不是同一個
    但沒有問題

    而且 allocate的時候, 第2個參數(shù)(VC6中必傳)void*可以不傳
    deallocate的時候必須告訴分配器還什么指針(p_d)以及還多大(1)
        第2個參數(shù)要和allocate時第1個參數(shù)一致
*/
int* p_d = allocator<int>().allocate(1); //1個int
allocator<int>().deallocate(p_d,1);

new的初步探究

new的工作流程

  • c++的程序員基本都會用 <font color=red>new</font>來為對象分配一個 <font color=red>堆內(nèi)存</font>, 并且 <font color=red>new會調(diào)用對應(yīng)的構(gòu)造函數(shù)</font>, 構(gòu)造函數(shù)是用來初始化對象的, 所以總結(jié)出new的功能是:
    1. 在堆中分配一塊內(nèi)存
    2. 調(diào)用對應(yīng)的構(gòu)造函數(shù)
調(diào)用形式:
    Object* obj = new Object(v1, v2)



new實際的工作(偽代碼):
    try{
        void* obj = operator::new(sizeof(Object));

        Object* obj_p = static_cast<Object*>(obj);

        obj_p->Object(v1,v2); // 只有編譯器可以這樣調(diào)用(但vc中可以)

    }cache(std::bad_alloc){
        // 分配內(nèi)存失敗
    }


根據(jù)上面的內(nèi)存函數(shù)的調(diào)用流程, 所以 operator::new會調(diào)用malloc, 下面用MSVC測試一下:


測試new的調(diào)用流程

21.png

22.png

23.png

24.png

struct A{
    int a;
    int b;
    A(){}
    A(int _a, int _b): a(_a), b(_b){}
};
int main(int arg, char** args){
    A* tmp_a = new A(2, 8);
    delete tmp_a;
}

/** 
    上面的4張圖是在MSVC中反匯編的運行時代碼

    從第1張圖可以看出, new 調(diào)用了operator new

    從第3張圖可以看出, operator new內(nèi)部調(diào)用了malloc

    operator new的函數(shù)從VC的匯編大致可以得出長這樣:
        void* __CRTDECL operator new(size_t const size){
            for(;;){
                if(void* const block = malloc(size))
                    ...
            }
        }


    ps: 實際上編譯器是在 new的地方調(diào)用了對應(yīng)的構(gòu)造函數(shù), 并不是在new的內(nèi)部
        new只是編譯器識別的一個標識符, 并不是函數(shù), 編譯器看到new后會malloc, 然后調(diào)用構(gòu)造函數(shù)


    VS2019可以看到 operator new的源碼:
*/
_CRT_SECURITYCRITICAL_ATTRIBUTE
void* __CRTDECL operator new(size_t const size)
{
    for (;;)
    {
        if (void* const block = malloc(size))
        {
            return block;
        }

        if (_callnewh(size) == 0)
        {
            if (size == SIZE_MAX)
            {
                __scrt_throw_std_bad_array_new_length();
            }
            else
            {
                __scrt_throw_std_bad_alloc();
            }
        }

        // The new handler was successful; try to allocate again...
    }
}
/** 
    上面的 operator new的作用是 調(diào)用malloc分配內(nèi)存
    
    當malloc成功后直接返回
    當malloc失敗后:
        并不會再次 malloc
        而是調(diào)用 _callnewh() new_handler()
            這個函數(shù)的作用是 向自己定義的函數(shù)索取內(nèi)存
            所以 new_handler可以理解為釋放一些緩存
        調(diào)用完new_handler后, 可能釋放了內(nèi)存, 這個時候再嘗試調(diào)用malloc獲取內(nèi)存
    
*/

delete的初步探究

工作流程

25.png

26.png

27.png

28.png

29.png
using namespace std;
struct A {
    int a;
    int b;
    A() {}
    A(int _a, int _b) : a(_a), b(_b) {}
    ~A() {}
};
int main(int arg, char** args) {
    A* tmp_a = new A(2, 8);
    delete tmp_a;
}


析構(gòu)的另一種調(diào)用方式

struct A {
    int a;
    int b;
    A(int _a, int _b) : a(_a), b(_b) {}
};

int main(int arg, char** args) {
    A* tmp_a = new A(2, 8);

    /** 
        直接利用指針調(diào)用A的析構(gòu)函數(shù), 標準規(guī)格是允許的
            在 19_stl.md中的 分類對算法的影響--case3 中也這樣用到過
        
    */
    tmp_a->~A();


    /** 
        雖然調(diào)用了析構(gòu)函數(shù), 但事實上tmp_a的空間并沒有釋放 
        所以可以訪問tmp_a

        ps: 如果是delete tmp_a; 就不能會訪問
    */
    cout << tmp_a->a << endl;


    /** 
        在A中也沒有定義析構(gòu)函數(shù)函數(shù), 在上小節(jié)的 工作流程中的
        最后1張圖提出了1個問題, 如果沒有手動寫出析構(gòu)函數(shù), 那編譯器
        會不會生成默認的析構(gòu)函數(shù), 但這里測試的編譯器至少在調(diào)用時生成了

        在以前學習類的時候, 說過1個類定義了如果沒有在任何地方用到就不會
        生成默認的幾個成員函數(shù), 這里至少提高到不顯示調(diào)用就不會生成
    */
}


array new/delete

語法

struct A {
    int a;
    int b;
    A() {
        cout << "A() : this:" << this << endl;
    }
    A(int _a, int _b) : a(_a), b(_b) {
        cout << "A(int,int): this: " << this << endl;
    }
    ~A() {
        cout << "~A(): this: " << this << endl;
    }
};


int main(int arg, char** args){
    //A* tmp = new A[2](2,6);       //_code_a

    A* tmp = new A[2];              //_code_b

    //delete tmp;                   //_code_c
    
    delete[] tmp;                   //_code_d

    cout << "over\n";               //_code_e
}

/** 
    _code_a 本身想調(diào)用A的有參構(gòu)造, 但語法不成立

    _code_b 用了array new, 內(nèi)部會調(diào)用malloc分配2個A大小的空間
            然后調(diào)用2次A默認無參的ctor

    _code_c
        cl.exe(MSVC的c++編譯器, 這里用的是2019)編譯運行后runtime error, 但會調(diào)用1次tmp[0]的析構(gòu)函數(shù)
        g++編譯運行后, 沒有問題, main函數(shù)正常結(jié)束, 也只會調(diào)用1次tmp[0]的析構(gòu)函數(shù)




    _code_d會調(diào)用2次析構(gòu)函數(shù), 是正確的合理的寫法
        先調(diào)用tmp[1], 再調(diào)用tmp[0]

        new A[2] 內(nèi)部會調(diào)用malloc分配2個A大小的空間, 返回的是 A*
        new A    內(nèi)部會調(diào)用malloc分配1個A大小的空間, 返回的是 A*

        但最后用 delete tmp搭配new A[2] 時出現(xiàn)錯誤, 說明new A和new A[]
        的過程是不一樣的, 至少結(jié)構(gòu)是不一樣的, 這留到后面的malloc再說



    對于_code_c, 在g++的環(huán)境下, 可以通過, 但并不適合所有的類
    當1個類的成員變量有指針時, _code_c搭配_code_b, 就會有內(nèi)存泄露
    因為數(shù)組在釋放時, 并沒有調(diào)用所有元素的析構(gòu)函數(shù)
    并且, 這里面有1個結(jié)構(gòu)上的問題, 后面再說
    
   ps: 如果A沒有手動寫出析構(gòu)函數(shù), _code_c這一行在VC編譯后運行沒有問題, 但同樣可能有內(nèi)存泄露

*/


placement new

概念

  • 標準不允許直接用指針調(diào)用構(gòu)造函數(shù), 但 <font color=red>給出了另一種調(diào)用語法</font>, 可以 <font color=red>在現(xiàn)有的對象的空間中調(diào)用構(gòu)造函數(shù)</font>, 它不會創(chuàng)建新的空間
struct A {
    int a;
    int b;
    A() {
        cout << "A() : this:" << this << endl;
    }
    A(int _a, int _b) : a(_a), b(_b) {
        cout << "A(int,int): this: " << this << endl;
    }
    ~A() {
        cout << "~A(): this: " << this << endl;
    }
};

int main(){
    A* tmp = new A[2];

    auto _idx = tmp;

    for(int i = -1; ++i < 2;){
        /** 
            利用placement new的語法, 手動調(diào)用A的構(gòu)造函數(shù)
            但不會創(chuàng)建新對象, 就是說一個一個遍歷數(shù)組, 
            通過指針訪問對應(yīng)元素的構(gòu)造函數(shù)

            下一節(jié)的 operator new第2個參數(shù) 會說明 placement new的實現(xiàn)機制
        */
        new(_idx++)A(2,75);
    }

    delete[] tmp;
}


operator new第2個參數(shù)

int main(int arg, char** args) {
    A* a = new A();
    auto result = operator new(sizeof(A), a);

    cout << a << endl;
    cout << result << endl;
    
     
    //a和result的地址一樣, 其實這里可以找到operator new的源碼
}
#define __PLACEMENT_NEW_INLINE
_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(_Size) _Post_satisfies_(return == _Where)
inline void* __CRTDECL operator new(size_t _Size, _Writable_bytes_(_Size) void* _Where) noexcept
{
    (void)_Size;
    return _Where;  //如果有第2個參數(shù)則直接返回第2個參數(shù)
}


探究operator new

int main(int arg, char** args) {
    A* a = new A();
    new(a)A(27, 224);

    void* b = ::operator new(sizeof(A));
    cout << b << endl;  

    cout << a->a << endl;
    cout << a->b << endl;
}

/**
    對于這樣的調(diào)用, 匯編代碼如下:
*/
30.png

31.png

33.png

34.png

重載內(nèi)存函數(shù)

內(nèi)存分配的過程

35.png
從圖中可以看出, 當出現(xiàn)
    new Foo(x)
后, 編譯器會查看 Foo 這個類有沒有 實現(xiàn) operator new(size_t)的static函數(shù)
如果有就會調(diào)用Foo這個版本的operator new, 如果沒有就調(diào)用全局的::operator new
delete也是如此

所以可以為一個類單獨實現(xiàn)operator new和operator delete, 也可以重載全局的operator new/delete
但很少這么做, 因為全局的版本是照顧所有的類


重載::operator new/delete

  • 上面說了, 重載 <font color=red>全局的operator new/delete</font>影響深遠, 但可以重載, 方法是在 <font color=red>另1個==namespace==中聲明和全局版本相同的函數(shù)簽名</font>
using namespace std;
struct A {
    int a;
    int b;
    A() {
        cout << "A() : this:" << this << endl;
    }
    A(int _a, int _b) : a(_a), b(_b) {
        cout << "A(int,int): this: " << this << endl;
    }
    ~A() {
        cout << "~A(): this: " << this << endl;
    }
};

////////////////////////////
////// 以下4個函數(shù)就在當前的文件中聲明定義

inline void* operator new(size_t len) {
    return malloc(len); 
}

inline void* operator new[](size_t len) {
    return malloc(len);
}

inline void operator delete(void* ptr) {
    return free(ptr);
}

inline void operator delete[](void* ptr) {
    return free(ptr);
}
int main(int arg, char** args) {
    A* a = new A();  //調(diào)用到上面自定義的operator new

    void* b = ::operator new(sizeof(A));  // 雖然指定的是::operator new, 但還是調(diào)用到上面自定義的版本

    // 對于a是正確的釋放, 先調(diào)用a的析構(gòu)函數(shù), 再調(diào)用上面的operator delete
    delete a;

    // 對于b, 釋放的操作應(yīng)該顯示調(diào)用operator delete
    /// 這里用delete后, 并沒有調(diào)用A的析構(gòu)函數(shù), 而是直接調(diào)用了 operator delete
    ///// 可見編譯器對于 delete void* 的時候, 是直接轉(zhuǎn)換為 opreator delete
    delete b;
}


重載類本身的operator new/delete

using namespace std;
struct A {
    A() {
        cout << "A() : this:" << this << endl;
    }
    ~A() {
        cout << "~A(): this: " << this << endl;
    }


    /** 
        這個函數(shù)寫不寫static都會被編譯器做成static
    */
    static void* operator new(size_t len) {
        return malloc(len);
    }

    static void operator delete(void* ptr, size_t len) {
        return free(ptr);
    }
};

int main(int arg, char** args) {
    //調(diào)用A::operator new版本
    A* a = new A();
    
    // 調(diào)用全局的operator new, 如果想調(diào)用A的版本, 則顯示調(diào)用 A::operator new()
    void* b = operator new(sizeof(A));
    
    
    A* c = ::new A();       //調(diào)用全局的operator new
    void* d = ::new A();    //調(diào)用全局的operator new
    
    delete a;   //先調(diào)用a的析構(gòu)函數(shù),再調(diào)用a的operator delete
    delete b;   //直接調(diào)用了全局的operator delete
    ::delete c;
    ::delete d;
}


本質(zhì)就是重載new

  • new是 <font color=red>關(guān)鍵字</font>, 編譯器看到new, 會轉(zhuǎn)換成 <font color=red>operator new</font>的調(diào)用, 所以new也可以理解為操作符(==畢竟用operator修飾的==)
A* a = new A();   //轉(zhuǎn)換成 A* a = ::operator new(sizeof(A));
                  // 如果有A自己定義了 operator new, 則會轉(zhuǎn)換成 A* a = operator new(sizeof(A));
                  // 接著調(diào)用A的無參ctor
                  /// 也就是說這句代碼完整的應(yīng)該是這樣 A* a = new(sizeof(A)) A();
                  /////// 但編譯器不允許這么寫


/** 
    前面在說placement new的時候:
        new(a)A(2,2);  //A(2,2)表示A有一個2參數(shù)的ctor, new(a)中的a是一個void*
    本質(zhì)是調(diào)用了 operator new(size_t, void*)的函數(shù), 那就說明:
        可以為 operator new 重載不同參數(shù)的operator new函數(shù)
*/
struct A {
    A() {
        cout << "A() : this:" << this << endl;
    }
    ~A() {
        cout << "~A(): this: " << this << endl;
    }

    // 最原始的operator new
    void* operator new(size_t len) {
        return malloc(len);
    }
    
    // 就是placement new(前面說過placement new不會創(chuàng)建新的內(nèi)存)
    void* operator new(size_t len, void* t) {
        return t;
    }

    // 為operator new重載自己的版本
    static void* operator new(size_t len, const char* str) {
        void* mem = malloc(len);
        cout << "str" << endl;
        return mem;
    }

    static void operator delete(void* ptr, size_t len) {
        return free(ptr);
    }
};

int main(int arg, char** args) {
    // 調(diào)用operator new(size_t)
    A* a = new A; //或 new A()
    
    // 調(diào)用 operator new(size_t, void*)
    ///很可能會出問題, 因為*args并不是A*的指針
    /// 所以調(diào)用ctor時,如果ctor內(nèi)部訪問了成員變量, 就會有問題
    A* b = new((void*)*args) A; // 無參的ctor可以不用A()

    // 調(diào)用operator new(size_t, const char*)
    A* c = new("hello") A;  // 無參的ctor 可以不用 A()

    
    /** 
        重載operator new時, 第1個參數(shù)必須是size_t, 并且用new 調(diào)用時不能傳遞第1個參數(shù)
    */
}


關(guān)于多參new對應(yīng)的delete

  • 上面重載了參數(shù)不一的new(==operator new==), 那是不是要重載對應(yīng)的delete呢?


  • 理論上delete的工作 <font color=red>只是為了釋放內(nèi)存</font>, 所以只需要一個==指針==就可以了, 但c++在語法上支持 <font color=red>多參的delete重載</font>, 但delete的調(diào)用格式只有 ==delete type==, 所以關(guān)于重載的delete版本什么時候被調(diào)用, 并不是用戶手動調(diào)用, 而是對應(yīng)版本的==ctor發(fā)生異常后, 會由crt來調(diào)用對應(yīng)的delete
struct A{
    A():{}                                          //_ctor_a
    A(int):{throw 2;}                               //_ctor_b

    static operator new(size_t len){ .. }           //_new_a
    static operator new(size_t len, int e){ .. }    //_new_b
    static operator delet(void* ptr){ ... }         //_del_a
    static operator delet(void* ptr, int e){ ...}   //_del_b
};

int main(int arg, char** args){
   A* a = new A();      //_new_a, _ctor_a
   
   A* b = new(444)A;    //_new_b, _ctor_a

   A* c = new A(2);     //_new_a, _ctor_b
                        //_ctor_b有異常但卻沒有調(diào)用 _del_a

   A* d = new(42)A(75); //_new_b, _ctor_b
                        //有異常, 但卻沒有調(diào)用_del_b

   /** 
        也就是說重載的operator delete就是為了處理new的異常, 但上面卻沒有調(diào)用
        在g++早期的版本, 上面是會調(diào)用對應(yīng)的 _del_a和_del_b, 現(xiàn)在不會了, VC也不會


        事實上, new的異常處理有專門的語法格式
   */
}
class A {
public:
    A() try:a(test()) {
        cout << "A()\n";

    }catch (...) {
        // 這里可以訪問當前對象的成員, 所以在這里釋放
        if(b){
            delete b;
            b = nullptr;
        }
        cout << "A catch ...\n";
    }
    int test() {
        b = new int();
        *b = 64;
        throw 2;

        return 4;
    }
    ~A() {
        if(b){
            delete b;
            b = nullptr;
        }
        cout << "A destructor\n";
    }

private:
    int a;
    int* b;
};



int main(int arg, char** args) {
    A a;
    return 0;
}

利用operator new實現(xiàn)類似java中的satic方法

  • 在java中, 一個類最先調(diào)用的函數(shù)是 static方法(==不屬于對象==),c++中原本是沒有的, 但利用operator new可以模擬出類似的效果
public class A{
   static{
        System.out.println("fir");
    }

    static public void main(String[] args){
        System.out.println("two");
    }
}
/** 
    上面最先調(diào)用的是 static{}, 下面是c++的實現(xiàn)
*/
struct A{
    static void do_sth{
        ...
    }
    static operator new(size_t len){
        // do something
        static once_flag o;
        std::call_once(once_flag, do_sth);
        return ::operator new(len);
    }
};
/** 
    其實上述的實現(xiàn)和java中的static靜態(tài)函數(shù)并不等價,
    java中的static一定在main前面調(diào)用, 并且不存在線程競爭
    而cpp的實現(xiàn), 是程序已經(jīng)運行起來, 并且可能存在線程競爭
*/
struct A {
    friend static void call();
private:
    static void s_static_func() {
        n = new int(2);
        this_thread::sleep_for(chrono::seconds(2));
        cout << "before main\n";
    }
    static int* n;
};
int* A::n;

static void call() {
    A::s_static_func();
}

auto lam = [](void) -> int{
    call();
    return 0;
}();

int main(int arg, char** args) {
    cout << "main" << endl;
}


new handler

注冊

  • 當 <font color=red>operator new</font> 調(diào)用==malloc==后, 發(fā)現(xiàn)沒有內(nèi)存, 會調(diào)用==new handler==, 所以new_handler的作用應(yīng)該是釋放一些不必要的內(nèi)存


  • 可以注冊自己的new handler
/** 
    void(__cdecl*)(void)    __cdecl是標準的c調(diào)用約定(函數(shù)從右往左壓棧)
    using new_handler = void(__CLRCALL_PURE_OR_CDECL*)();

    /** 
        設(shè)置新的new handler, 返回舊的new handler
    */
    _CRTIMP2 new_handler __cdecl set_new_handler(_In_opt_ new_handler) noexcept;

    _NODISCARD _CRTIMP2 new_handler __cdecl get_new_handler() noexcept;
*/

void __cdecl mem_release(void){

}

int main(int arg, char** args){
    auto old_handler = set_new_handler(mem_release);
}


設(shè)計內(nèi)存管理機制[^ann_1]

為什么要自己設(shè)計

c++中new的底層調(diào)用了c的malloc, 關(guān)于malloc會在后面探究, 這里先大致提一下:
    malloc本身很復(fù)雜, 效率也很高, 它本身就是一個內(nèi)存管理器, 也可以稱為內(nèi)存池
    malloc在設(shè)計上會對給出的內(nèi)存附加額外的內(nèi)存
        當然用戶是不知道的
        細心的程序員可能想到了free
            free函數(shù)只接收1個指針, 但為什么就可以準確釋放呢?
            原因很可能是malloc的約定, 這個到后面細說
        
基于malloc的特點, 對容器來說, 特別是元素相鄰的容器(如vector)
    如果直接用malloc, 則每次分配內(nèi)存后都會附加其他內(nèi)存
    由于每次push_back, 調(diào)用malloc后元素就不是緊緊相鄰的, 這樣vector管理元素的復(fù)雜度就大大提升
    所以要實現(xiàn)一個沒有額外內(nèi)存的管理器提供給vector


思路:
    降低malloc調(diào)用次數(shù)
        目的是減少額外內(nèi)存
    有效利用手上的空間

下面開始就是一步一步從最簡單的分配器開始, 一直演進到gcc中著名的內(nèi)存池


內(nèi)存管理第0步

  • 最重要的就是 <font color=red>接手new</font>, 從前面的學習中已經(jīng)知道了, 可以為==類重載new==, 這樣 <font color=red>申請內(nèi)存的操作就到了自己的手中</font>, 下面開始為 <font color=red>1個單一的類設(shè)計內(nèi)存管理</font>


內(nèi)存管理單個類

分析:
    由于malloc有碎片, 所以為了減少malloc的調(diào)用, 但又不得不從malloc拿內(nèi)存, 所以:
        要拿就拿夠

思路:
    一次性向malloc索取一大塊內(nèi)存, 放到自己的手中
    當類new的時候, 先看手上有沒有
        有就給出去指針
        沒有就向malloc索取

    當delete時, 將內(nèi)存再回收到自己的手中

    索取的內(nèi)存用鏈表來管理
#define _MEM_ERROR_0 -1

#define _NEW_CODE_ \
    if (size <= 0)\
        throw _MEM_ERROR_0;\
    return A::alloc(size);

#define _DEL_CODE_  \
    if(ptr) \
        return dealloc(ptr);

class A {
public:


    void* operator new(size_t size) {
        _NEW_CODE_
    }

    void* operator new[](size_t size) {
        _NEW_CODE_
    }

    void operator delete(void* ptr) {
        _DEL_CODE_  
    }
    void operator delete[](void* ptr) {
        _DEL_CODE_
    }

private:
    static void make_list(A* begin, A* const end) {
        while (begin != end)
            (begin->next = begin + 1, ++begin);

        begin->next = nullptr;
    }

    static void* _malloc_chunk(size_t len) {
        size_t need_mem = A::chunk * len;

        size_t mod = need_mem / sizeof(A);

        A* need = static_cast<A*>(malloc(need_mem));
        
        // 取頭尾指針, 包含尾指針
        A::make_list(need, reinterpret_cast<A*const>(need + mod - 1));

        return need;
    }

    static void* alloc(size_t len) {
        A* result = A::free_mem_start;

        if (!result)
            result = A::free_mem_start = static_cast<A*>(A::_malloc_chunk(len));

        A::free_mem_start = A::free_mem_start->next;

        return static_cast<void*>(result);
    }

    static void dealloc(void* ptr) {
        //將 已經(jīng)調(diào)用過析構(gòu)函數(shù)的廢棄的ptr指向當前鏈表的頭部
        static_cast<A*>(ptr)->next = A::free_mem_start;

        //再將頭部的指針指向ptr
        A::free_mem_start = static_cast<A*>(ptr);

        /*
            這里無法將指針還給os(free), 因為無法確定ptr是不是malloc給出的合理指針
        */
    }
private:
    static constexpr int chunk = 4;

    static A* free_mem_start;
    
    // 相當于鏈表中的節(jié)點
    struct {
        A* next;
        int data;
    };
};
A* A::free_mem_start = nullptr;

int main(int arg, char** args) {
    {
        A* a = ::new A;
        A* b = ::new A;
        A* c = ::new A;
        cout << a << "\n";
        cout << b << "\n";
        cout << c << "\n";
    }
    cout << "***************************\n";
    
    {
        for (int i = -1; ++i < 11;) {
            A* a = new A;
            cout << a << endl;
        }
    }
}

/** 
00DEFF40
00DEF990
00DEFB18
***************************
00DEC130
00DEC138
00DEC140
00DEC148

00DF24B8
00DF24C0
00DF24C8
00DF24D0

00DF2468
00DF2470
00DF2478


    前3個直接調(diào)用的全局的operator new, 所以是malloc分配, 所以3個堆內(nèi)存是不相鄰的
    
    
    代碼中設(shè)置的chunk為4, 所以每4次分配內(nèi)存
    相鄰的4次之間是緊緊相連的(A對象大小8個字節(jié))


    存在的問題:
        為了實現(xiàn)鏈表, 在A的類型中加入了一個額外的指針next
        分析可以知道 在鏈表中的內(nèi)存就只有2種狀態(tài):
            外界在用,即沒在鏈表中
            外界沒有在用, 即在鏈表中
            而A中的data和next實際就是這2種狀態(tài), 外界在用, 則next是無效的(data有效), 外界沒有用, 則next有效(data無效)
            所以可以考慮 data, next共用同一塊內(nèi)存(union)

        修改:
            將 A中的struct 直接修改為union, 其他地方不要改, 測試如下:
*/
00DC7E28
00DC7E58
00DCBCD0
***************************
00DD1FC0
00DD1FC4
00DD1FC8
00DD1FCC

00DCBD00
00DCBD04
00DCBD08
00DCBD0C

00DC6150
00DC6154
00DC6158

/** 
    改為union后, A大小是4個字節(jié), 所以每個元素大小是4,并且是相鄰的
    
    現(xiàn)在測試代碼再改一下
*/
int main(int arg, char** args){
    for (int i = -1; ++i < 11;) {
        A* a = new A;
        cout << a << endl;
        delete a;
    }
}
/** 
01286428
01286428
01286428
01286428
01286428
01286428
01286428
01286428
01286428
01286428
01286428

    發(fā)現(xiàn)a的地址始終是同一個, 說明是不斷的從池中拿的
*/


上述實現(xiàn)的bug

問題:
    1. 只適用于 new/delete, 如果是 array new/array delete就有問題
    2. 如果A的大小 < void*, 則會有額外有多余的內(nèi)存, 但這是合理的, 雖然看起來浪費了, 但卻在靈活使用手上的內(nèi)存
    

無法寫出析構(gòu)函數(shù):
    原因是不可能找到最初的malloc的地址指針

原因:
    alloc 函數(shù)中:
        A::free_mem_start = A::free_mem_start->next   _code_a

    這句代碼是以 1個sizeof(A)為單位往外給內(nèi)存, 也就是alloc函數(shù)參數(shù)len的大小是sizeof(A)
    但如果是 new A[2], 那么傳遞到alloc時, len就是 2*sizeof(A)為8
        此時_code_a中的 free_mem_start應(yīng)該移動2次
            即: A::free_mem_strat = A::free_mem_start->next->next

    相應(yīng)的 在dealloc函數(shù)中, 如果還的是 A[2] 這個數(shù)組, 應(yīng)該先在delete[]中將數(shù)組中的2個元素串起來, 再傳給dealloc
    但可惜的是 delete[] arr_obj, 不會將arr_obj整個數(shù)組的大小傳遞過去
        

    所以如果牽扯到 new A[] 和 new A的交叉使用, 這個內(nèi)存池是不可以的, 而且無法將內(nèi)存歸還給操作系統(tǒng)

解決:
    關(guān)于第1個交叉使用的問題, 可以在內(nèi)存池內(nèi)部維護 N*sizeof(A) 的鏈表, 這個也是后面gcc中一個分配器的實現(xiàn)
    第2個歸還os的問題, 當前這種設(shè)計是不支持的, 但并不是所謂的內(nèi)存泄露, 因為所有的內(nèi)存都在自己的手上

    所以上面的版本只能對 new A()使用, 應(yīng)該刪除掉 new[]和delete[]


進化到版本2(通用到各個類)

/** 
    就是把內(nèi)存相關(guān)的部分抽取出來
*/
template<int _N>
constexpr size_t _align_min() {
    static_assert(_N > 0, "通過");
    return (size_t)_N < sizeof(void*) ? sizeof(void*) : _N;
}

template<int _N>
constexpr size_t _check_chunk() {
    static_assert(_N > 0, "通過");
    return _N;
}

template<
    int Num,
    int chunk = 4, 
    size_t _unit = _align_min<Num>(), 
    size_t _chunk = _check_chunk<chunk>()>
class _alloc_ {
public: 
    void* alloc() {
        _Node* result = this->head;

        if (!result) {
            cout << _chunk << " " << _unit << endl;
            result = this->head = static_cast<_Node*>(::operator new(_chunk * _unit));

            cout << "malloc address: " << result << endl;

            for (; result != &this->head[chunk - 1];)
                (result->next = result + 1, ++result);

            result->next = nullptr;

            result = this->head;
        }
        this->head = this->head->next;

        return reinterpret_cast<void*>(result);
    }
    void dealloc(void* ptr) {
        cout << "回收: " << ptr << endl;
        reinterpret_cast<_Node*>(ptr)->next = this->head;
        this->head = reinterpret_cast<_Node*>(ptr);
    }

public:
    template<size_t _N>
    struct _tuple_ : _tuple_<_N - 1> {
        char data;
    };

    template<>
    struct _tuple_<0> {};

    using _Data = _tuple_<_unit>;
    using _Data_p = typename add_pointer<_Data>::type;

    union _Node{
        _Data data;
        _Node* next;
    };
    
    _Node* head;
};



/** 
    交差delete測試1
*/
int main(int arg, char** args) {
    _alloc_<2> all{};

    for (int i = -1; ++i < 10;){
        auto p = all.alloc();
        cout <<"fir:" << p << endl;

        all.dealloc(p);  //只回收1次

        p = all.alloc();
        cout <<"sec:" << p << endl;
    }
}
/** 
4 4
malloc address: 013BC130
fir:013BC130
回收: 013BC130
sec:013BC130
fir:013BC134
回收: 013BC134
sec:013BC134
fir:013BC138
回收: 013BC138
sec:013BC138
fir:013BC13C
回收: 013BC13C
sec:013BC13C
4 4
malloc address: 013C1F90
fir:013C1F90
回收: 013C1F90
sec:013C1F90
fir:013C1F94
回收: 013C1F94
sec:013C1F94
fir:013C1F98
回收: 013C1F98
sec:013C1F98
fir:013C1F9C
回收: 013C1F9C
sec:013C1F9C
4 4
malloc address: 013B6428
fir:013B6428
回收: 013B6428
sec:013B6428
fir:013B642C
回收: 013B642C
sec:013B642C

    這個測試結(jié)果沒有問題
*/





/** 
    交差測試2
*/
int main(int arg, char** args) {
    
    _alloc_<2> all{};

    for (int i = -1; ++i < 10;){
        auto p = all.alloc();
        cout <<"fir:" << p << endl;

        p = all.alloc();
        cout <<"sec:" << p << endl;

        all.dealloc(p);  //只回收1次
    }
}
/** 
4 4
malloc address: 00A01FC0
fir:00A01FC0
sec:00A01FC4
回收: 00A01FC4
fir:00A01FC4
sec:00A01FC8
回收: 00A01FC8
fir:00A01FC8
sec:00A01FCC
回收: 00A01FCC
fir:00A01FCC
4 4
malloc address: 009FBD00
sec:009FBD00
回收: 009FBD00
fir:009FBD00
sec:009FBD04
回收: 009FBD04
fir:009FBD04
sec:009FBD08
回收: 009FBD08
fir:009FBD08
sec:009FBD0C
回收: 009FBD0C
fir:009FBD0C
4 4
malloc address: 009F7E28
sec:009F7E28
回收: 009F7E28
fir:009F7E28
sec:009F7E2C
回收: 009F7E2C
fir:009F7E2C
sec:009F7E30
回收: 009F7E30


    也沒有問題
*/


完善版本2

template<int _N>
constexpr size_t _align_min() {
    static_assert(_N > 0, "通過");
    return (size_t)_N < sizeof(void*) ? sizeof(void*) : _N;
}

template<int _N>
constexpr size_t _check_chunk() {
    static_assert(_N > 0, "通過");
    return _N;
}

template<
    int Num,
    int chunk = 4, 
    size_t _unit = _align_min<Num>(), 
    size_t _chunk = _check_chunk<chunk>()>
class _alloc_ {
public: 
    void* alloc() {
        _Node* result = this->head;

        if (!result) {
            cout << _chunk << " " << _unit << endl;
            result = this->head = static_cast<_Node*>(::operator new(_chunk * _unit));

            cout << "malloc address: " << result << endl;

            for (; result != &this->head[chunk - 1];)
                (result->next = result + 1, ++result);

            result->next = nullptr;

            result = this->head;
        }
        this->head = this->head->next;

        return reinterpret_cast<void*>(result);
    }
    void dealloc(void* ptr) {
        cout << "回收: " << ptr << endl;
        reinterpret_cast<_Node*>(ptr)->next = this->head;
        this->head = reinterpret_cast<_Node*>(ptr);
    }
    
    template<typename T>
    static _alloc_& get() {
        static _alloc_<sizeof(T)> mem;
        cout <<"memory static alloca: " << &mem << endl;
        return mem;
    }
private:
    template<size_t _N>
    struct _tuple_ : _tuple_<_N - 1> {
        char data;
    };

    template<>
    struct _tuple_<0> {};

    using _Data = _tuple_<_unit>;
    using _Data_p = typename add_pointer<_Data>::type;

    union _Node{
        _Data data;
        _Node* next;
    };
    
    _Node* head;
};



#define _US_LB_ALLOCATOR_NEW(_Cls) \
    void* operator new(size_t size) {   \
        return _alloc_<sizeof(_Cls)>::get<_Cls>().alloc();\
    }\


#define _US_LB_ALLOCATOR_DEL(_Cls) \
    void operator delete(void* ptr) { \
        return _alloc_<sizeof(_Cls)>::get<_Cls>().dealloc(ptr);\
    }\


#define _US_LB_ALLCATOR(_Cls)\
_US_LB_ALLOCATOR_NEW(_Cls)\
_US_LB_ALLOCATOR_DEL(_Cls)

class A {
public:
    _US_LB_ALLCATOR(A)

private:
    int a;
    int b;
};


class B {
public:
    _US_LB_ALLCATOR(B)
private:
    int b;
};



int main(int arg, char** args) {

    for (int i = -1; ++i < 4;)
    {
        auto p_a = new A();
        cout <<"fir p_a" << p_a << endl;

        p_a = new A();
        cout <<"sec:" << p_a << endl;

        
        auto p_b = new B();
        cout << "fir p_b: " << p_b << endl;
        delete p_b;

        p_b = new B();
        cout << "sec p_b: " << p_b << endl;
        delete p_b;

        delete p_a;
    }
}


/** 
memory static alloca: 002F03C4
4 8
malloc address: 00DEC130
fir p_a00DEC130
memory static alloca: 002F03C4
sec:00DEC138
memory static alloca: 002F06E4
4 4
malloc address: 00DE6428
fir p_b: 00DE6428
memory static alloca: 002F06E4
回收: 00DE6428
memory static alloca: 002F06E4
sec p_b: 00DE6428
memory static alloca: 002F06E4
回收: 00DE6428
memory static alloca: 002F03C4
回收: 00DEC138
memory static alloca: 002F03C4
fir p_a00DEC138
memory static alloca: 002F03C4
sec:00DEC140
memory static alloca: 002F06E4
fir p_b: 00DE6428
memory static alloca: 002F06E4
回收: 00DE6428
memory static alloca: 002F06E4
sec p_b: 00DE6428
memory static alloca: 002F06E4
回收: 00DE6428
memory static alloca: 002F03C4
回收: 00DEC140
memory static alloca: 002F03C4
fir p_a00DEC140
memory static alloca: 002F03C4
sec:00DEC148
memory static alloca: 002F06E4
fir p_b: 00DE6428
memory static alloca: 002F06E4
回收: 00DE6428
memory static alloca: 002F06E4
sec p_b: 00DE6428
memory static alloca: 002F06E4
回收: 00DE6428
memory static alloca: 002F03C4
回收: 00DEC148
memory static alloca: 002F03C4
fir p_a00DEC148
memory static alloca: 002F03C4
4 8
malloc address: 00DF2508
sec:00DF2508
memory static alloca: 002F06E4
fir p_b: 00DE6428
memory static alloca: 002F06E4
回收: 00DE6428
memory static alloca: 002F06E4
sec p_b: 00DE6428
memory static alloca: 002F06E4
回收: 00DE6428
memory static alloca: 002F03C4
回收: 00DF2508
*/


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文轉(zhuǎn)自:https://www.cnblogs.com/lancidie/archive/2011/08/05/...
    Nrocinu閱讀 1,447評論 0 1
  • 2 內(nèi)存泄漏 2.1 C++中動態(tài)內(nèi)存分配引發(fā)問題的解決方案 假設(shè)我們要開發(fā)一個String類,它可以方便地處理字...
    Nrocinu閱讀 246評論 0 0
  • layout: posttitle: 重識newcategories: C/C++description: 重識n...
    超哥__閱讀 697評論 0 0
  • 應(yīng)用程序的設(shè)計中,我們所說的內(nèi)存管理就是將系統(tǒng)要處理的內(nèi)存分配和釋放接管過來,內(nèi)存池是常用的一種設(shè)計思路。內(nèi)存池是...
    GreatJu閱讀 432評論 0 2
  • layout: posttitle: 重識newcategories: C/C++description: 重識n...
    超哥__閱讀 264評論 0 0

友情鏈接更多精彩內(nèi)容