侯捷老师C++课程:内存管理

内存管理

第一讲:primitives

c++应用程序

侯捷老师C++课程:内存管理_第1张图片
c++内存的基本工具
侯捷老师C++课程:内存管理_第2张图片

测试程序:

#include 
using namespace std;
#include 
#include 

int main() {
    // 三种使用方法
    void* p1 = malloc(512);  // 512 bytes
    cout << p1 << endl;
    free(p1);

    complex* p2 = new complex;
    cout << p2 << endl;
    delete p2;

    void* p3 = ::operator new(512);  // 512 bytes
    cout << p3 << endl;
    ::operator delete(p3);

// 以下使用 C++ 标准库提供的 allocators。
// 虽然接口都有标准规格,但是调用方式略有区别
#ifdef _MSC_VER
    // 以下兩函數都是 non-static,定要通過 object 調用。以下分配 3 個 ints.
    int* p4 = allocator().allocate(3, (int*)0);
    allocator().deallocate(p4, 3);
#endif

#ifdef __BORLANDC__
    // 以下兩函數都是 non-static,定要通過 object 調用。以下分配 5 個 ints.
    int* p4 = allocator().allocate(5);
    allocator().deallocate(p4, 5);
#endif

//调用这一个
#ifdef __GNUC__
    // 以下兩函數都是 static,可通過全名調用之。以下分配 512 bytes.
    // void* p4 = alloc::allocate(512);
    // alloc::deallocate(p4, 512);

    // 以下兩函數都是 non-static,定要通過 object 調用。以下分配 7 個 ints.
    void* p4 = allocator().allocate(7);
    cout << p4 << endl;
    allocator().deallocate((int*)p4, 7);

    // 以下兩函數都是 non-static,定要通過 object 調用。以下分配 9 個 ints.
    void* p5 = __gnu_cxx::__pool_alloc().allocate(9);
    cout << p5 << endl;
    __gnu_cxx::__pool_alloc().deallocate((int*)p5, 9);
#endif

    return 0;
}

new expression

使用new关键字之后编译器会把这串代码翻译为如下:

new关键字使用之后重要的就执行了两步,第一步是分配内存,第二步是调用构造函数

delete expression

与new相对应的就有delete关键字

delete关键字使用的时候执行了两步,第一步是调用析构函数,第二步是释放内存

侯捷老师C++课程:内存管理_第3张图片

上面两副图片当中,通过指针,构造函数不能被直接调用,而析构函数可以被直接调用

如果非要调用的话,可以用 placement new (现在不理解什么意思)

new(p) Complex(1,2);

以下是一个测试程序:

#include 
using namespace std;
#include 

class A {
public:
    A() = default;
    A(int id) : _id(id) { cout << "ctor. this = " << this << " id = " << id << endl; }
    ~A() { cout << "dtor. this = " << this << endl; }

    int _id;
};

int main() {
    string* pstr = new string;
    cout << "str= " << *pstr << endl;

    // pstr->string::string("hello");  // ‘class std::__cxx11::basic_string’ has no member named ‘string’
    // pstr->~string();//crash

    cout << "str= " << *pstr << endl;

    A* pA = new A(1);
    cout << pA->_id << endl;  // 1

    // pA->A::A(3);//cannot call constructor ‘A::A’ directly
    // A::A(5);

    cout << pA->_id << endl;

    delete pA;

    return 0;
}

array new,array delete

注意:array new 一定要搭配 array delete,否则就极容易发生内存泄漏

这个内存泄露对于尤其是class with pointers,通常带有影响

因为对于没有指针的类,只需要释放这个类对象的指针就可以了,因此调用一次和三次的dtor没有明显的区别,换句话说就是这个类的dtor是trivial(不重要的),但是带有指针的类就不一样了

比如下面string那个例子,只换起一次dtor,那么三个string指向的东西只被释放了一个,然后整体就被释放了,剩余的两块内存怎么办呢?因此会导致内存泄漏

侯捷老师C++课程:内存管理_第4张图片

#include 
using namespace std;
#define size 3

class A {
public:
    A() : _id(0) { cout << "default ctor. this = " << this << " id = " << _id << endl; }
    A(int id) : _id(id) { cout << "ctor. this = " << this << " id = " << _id << endl; }
    ~A() { cout << "dtor. this = " << this << " id = " << _id << endl; }

public:
    int _id;
};

int main() {
    A* buf = new A[size];  // A必须有默认构造函数,否则会报错
    A* tmp = buf;

    cout << "buf= " << buf << " tmp= " << tmp << endl;

    for (int i = 0; i < size; ++i)
        new (tmp++) A(i);  // placement new , ctor 三次

    cout << "buf= " << buf << " tmp= " << tmp << endl;

    delete[] buf;  // dtor 3次,次序反过来 3 2 1

    return 0;
}

执行结果

侯捷老师C++课程:内存管理_第5张图片
内存分布

内存的底层开辟和释放都是调用的malloc和free,那么调用了malloc之后会给我们的内存分布就如下所示:

可以浅谈一下这个内存是怎么分配的(后面都会进行深入的探究,以及每一块的作用)

Demo对象:3个int,占据12个字节,3个总共36个字节;

由于他带有指针,所以需要额外记录这个数组对象包含Demo的个数,4个字节;

这个真正有效的数据区域上下(黄色的部分),分别占据32 + 4 个字节;

内存块上下的两个cookie,各自4个字节,总共8个字节;

上面一共加起来84个字节,需要调整到16个字节的倍数,也就是96个字节,多出的12个字节存放在Pad中

placement new

placement new允许我们将对象建造在已经分配好的内存当中!!

编译器翻译成为的那三个操作,在 placement new 下面,第一条由于传入了一个指针,那么会调用重载的版本,其实就是表示不用新开内存,把原来的给我就行;然后第三条编译器就调用构造函数在已有的内存上进行创建对象初始化!!!

#include 
using namespace std;

class Complex {
public:
    Complex() : _re(0), _im(0) {}
    Complex(double re, double im) : _re(re), _im(im) {}

public:
    double _re, _im;
};

int main() {
    char* buf = new char[sizeof(Complex) * 3];
    // 现在想把一个Complex对象动态开辟在buf的一个Complex单元,调用placement new
    Complex* pc = new (buf) Complex(1, 2);

    delete[]buf;
    
    return 0;
}
重载

侯捷老师C++课程:内存管理_第6张图片

重载比较多的就是在类中去重载 operator new和 operator delete,这样编译器在调用new或者delete关键字解析到那两步的时候就会优先调用我们重载的版本,在我们重载的版本当中可以设计一些专用于这个类的设计,这样或许能够提高效率和节省开销

在类里面重载

侯捷老师C++课程:内存管理_第7张图片

侯捷老师C++课程:内存管理_第8张图片

delete中的第二参数是optional的,可以写也可以不写

重载示例

在类当中进行简单的重载,和全局的输出做对比

#include 
using namespace std;
#include 

class Foo
{
public:
    int _id;

public:
    Foo() : _id(0) { cout << "default ctor.this = " << this << " id = " << _id << endl; }
    Foo(int id) : _id(id) { cout << "ctor.this = " << this << " id = " << _id << endl; }

    // virtual
    virtual ~Foo() { cout << "dtor.this = " << this << " id = " << _id << endl; }

    static void *operator new(size_t size);
    static void operator delete(void *ptr, size_t size);
    static void *operator new[](size_t size);
    static void operator delete[](void *ptr, size_t size);
};

void *Foo::operator new(size_t size)
{
    Foo *p = static_cast(malloc(size));
    cout << "Foo::operator new(), size = " << size << "\treturn : " << p << endl;
    return p;
}

void Foo::operator delete(void *ptr, size_t size)
{
    cout << "Foo::operator delete(), ptr = " << ptr << "\tsize = " << size << endl;
    free(ptr);
}

void *Foo::operator new[](size_t size)
{
    Foo *p = static_cast(malloc(size));
    cout << "Foo::operator new[](), size = " << size << "\treturn : " << p << endl;
    return p;
}

void Foo::operator delete[](void *ptr, size_t size)
{
    cout << "Foo::operator delete[](), ptr = " << ptr << "\tsize = " << size << endl;
    free(ptr);
}

int main()
{
    cout << "sizeof(Foo) = " << sizeof(Foo) << endl
         << endl;

    cout << "Foo------------------------------------------------------------" << endl;

    Foo *p = new Foo;
    delete p;

    cout << endl;

    Foo *pArray = new Foo[5]{1, 2, 3, 4, 5};
    delete[] pArray;

    cout << endl
         << "Global------------------------------------------------------------" << endl;

    Foo *p2 = ::new Foo;
    ::delete p2;

    cout << endl;

    Foo *pArray2 = ::new Foo[5]{1, 2, 3, 4, 5};
    ::delete[] pArray2;

    return 0;
}

输出结果:

侯捷老师C++课程:内存管理_第9张图片

注意:

我们可以重载class member operator new(),可以写出多个版本,前提是每一个版本都必须声明独特的参数列,并且第一个参数是size_t,其余参数以new指定的placement arguments为初值,出现在new(…)当中的就是所谓的placement arguments.

这样就可以写出很多的placement new.

例如:

void * Foo::operator new(size_t size,long extra,char ch);
//这么用
Foo* pf=new(300,'c') Foo;

测试代码:

#include 
using namespace std;
#include 

class Bad
{
};

class Foo
{
public:
    Foo() { cout << "Foo::Foo()" << endl; }
    Foo(int)
    {
        cout << "Foo::Foo(int)" << endl;
        throw Bad(); // 故意在这里抛出异常,测试调用placement operator delete
    }

    //(1) 這個就是一般的 operator new() 的重載
    void *operator new(size_t size)
    {
        cout << "operator new(size_t size), size= " << size << endl;
        return malloc(size);
    }

    //(2) 這個就是標準庫已經提供的 placement new() 的重載 (形式)
    //    (所以我也模擬 standard placement new 的動作, just return ptr)
    void *operator new(size_t size, void *start)
    {
        cout << "operator new(size_t size, void* start), size= " << size << "  start= " << start << endl;
        return start;
    }

    //(3) 這個才是嶄新的 placement new
    void *operator new(size_t size, long extra)
    {
        cout << "operator new(size_t size, long extra)  " << size << ' ' << extra << endl;
        return malloc(size + extra);
    }

    //(4) 這又是一個 placement new
    void *operator new(size_t size, long extra, char init)
    {
        cout << "operator new(size_t size, long extra, char init)  " << size << ' ' << extra << ' ' << init << endl;
        return malloc(size + extra);
    }

    //(5) 這又是一個 placement new, 但故意寫錯第一參數的 type (它必須是 size_t 以滿足正常的 operator new)
    //!  	void* operator new(long extra, char init) { //[Error] 'operator new' takes type 'size_t' ('unsigned int') as first parameter [-fpermissive]
    //!	  	cout << "op-new(long,char)" << endl;
    //!    	return malloc(extra);
    //!  	}

    // 以下是搭配上述 placement new 的各個 called placement delete.
    // 當 ctor 發出異常,這兒對應的 operator (placement) delete 就會被喚起.
    // 應該是要負責釋放其搭檔兄弟 (placement new) 分配所得的 memory.
    //(1) 這個就是一般的 operator delete() 的重載
    void operator delete(void *, size_t)
    {
        cout << "operator delete(void*,size_t)  " << endl;
    }

    //(2) 這是對應上述的 (2)
    void operator delete(void *, void *)
    {
        cout << "operator delete(void*,void*)  " << endl;
    }

    //(3) 這是對應上述的 (3)
    void operator delete(void *, long)
    {
        cout << "operator delete(void*,long)  " << endl;
    }

    //(4) 這是對應上述的 (4)
    // 如果沒有一一對應, 也不會有任何編譯報錯
    void operator delete(void *, long, char)
    {
        cout << "operator delete(void*,long,char)  " << endl;
    }

private:
    int m_i;
};

//-------------
void test_overload_placement_new()
{
    cout << "test_overload_placement_new().........." << endl;

    Foo start; // Foo::Foo

    Foo *p1 = new Foo;            // op-new(size_t)
    Foo *p2 = new (&start) Foo;   // op-new(size_t,void*)
    Foo *p3 = new (100) Foo;      // op-new(size_t,long)
    Foo *p4 = new (100, 'a') Foo; // op-new(size_t,long,char)

    Foo *p5 = new (100) Foo(1);      // op-new(size_t,long)  op-del(void*,long)
    //这里故意调用int版本的构造函数,在构造函数当中会抛出异常
    //为什么会抛出异常呢?因为我们担心这个placement operator new 我们已经分配出来的空间用在构造函数上面不够
    //不够的时候怎么办呢?内存都已经分配出来了,那就只能释放掉,调用相应的placement operator delete
    Foo *p6 = new (100, 'a') Foo(1); //
    Foo *p7 = new (&start) Foo(1);   //
    Foo *p8 = new Foo(1);            //
                                     // VC6 warning C4291: 'void *__cdecl Foo::operator new(unsigned int)'
                                     // no matching operator delete found; memory will not be freed if
                                     // initialization throws an exception
}

int main()
{
    test_overload_placement_new();

    return 0;
}

注意这里:

Foo *p5 = new (100) Foo(1);
//这里故意调用int版本的构造函数,在构造函数当中会抛出异常
//为什么会抛出异常呢?因为我们担心这个placement operator new 我们已经分配出来的空间用在构造函数上面不够
//不够的时候怎么办呢?内存都已经分配出来了,那就只能释放掉,调用相应的placement operator delete

只有这种情况下,ctor中抛出异常,对应的operator delete才会被调用起来;如果不写,那就是放心这个构造函数并且不去处理这个异常

basic_string使用new(extra)申请扩充量

per-class allocator 版本1 (重点看)

设计一个小型的内存池,小型的内存分配器,目前是第一版本

#include 
using namespace std;

class Screen
{
public:
    Screen() = default;
    Screen(int x) : _i(x){};
    int get() const { return _i; }

    inline void *operator new(size_t);
    inline void operator delete(void *, size_t);

private:
    Screen *next; // 这种设计会引发一个疑问,就是多消耗了一个指针的内存空间,但是可以抹除数组元素之间的cookie,只在数组头尾放cookie
    static Screen *freeStore;
    static const int screenChunk;

private:
    int _i;
};
Screen *Screen::freeStore = nullptr;
const int Screen::screenChunk = 24;

void *Screen::operator new(size_t size)
{
    Screen *p;
    if (!freeStore)
    {
        // linked list 是空的,所以攫取一大塊 memory
        // 以下呼叫的是 global operator new
        size_t chunk = screenChunk * size; // 这是乘法,计算需要的字节数
        freeStore = p =
            reinterpret_cast(new char[chunk]);
        // 將分配得來的一大塊 memory 當做 linked list 般小塊小塊串接起來
        for (; p != &freeStore[screenChunk - 1]; ++p)
            p->next = p + 1;
        p->next = 0;
    }
    p = freeStore;
    freeStore = freeStore->next;
    return p;
}

//! void Screen::operator delete(void *p)		//(1)
void Screen::operator delete(void *p, size_t) //(2)二擇一
{
    // 將 deleted object 收回插入 free list 前端
    (static_cast(p))->next = freeStore;
    freeStore = static_cast(p);
}

//-------------
void test_per_class_allocator_1()
{
    cout << "test_per_class_allocator_1().......... \n";

    cout << sizeof(Screen) << endl; // 8

    size_t const N = 100;
    Screen *p[N];

    for (int i = 0; i < N; ++i)
        p[i] = new Screen(i);

    // 輸出前 10 個 pointers, 用以比較其間隔
    for (int i = 0; i < 10; ++i)
        cout << p[i] << endl;

    for (int i = 0; i < N; ++i)
        delete p[i];
}

void test_global_allocator()
{
    cout << "test_global_allocator().......... \n";

    cout << sizeof(Screen) << endl; // 8

    size_t const N = 100;
    Screen *p[N];

    for (int i = 0; i < N; ++i)
        p[i] = ::new Screen(i);

    // 輸出前 10 個 pointers, 用以比較其間隔
    for (int i = 0; i < 10; ++i)
        cout << p[i] << endl;

    for (int i = 0; i < N; ++i)
        ::delete p[i];
}

int main()
{
    test_per_class_allocator_1();
    cout << endl
         << endl;
    test_global_allocator();

    return 0;
}

执行结果:

image-20230528145254800

可以看出,设计之后的内存之间没有了cookie,节省了空间,这就是我们自己的一个小型内存池

per-class allocator2 版本2

和前面的思路基本一样:就是要一大块内存,当数组形式要进来分配内存的时候,如果这一大块内存还有空间,就链在后面就行;如果没有了,就要再要一大块空间进行同样的操作就可以了,最后在前后加上cookie就可以了。而这一切的发生都必须依赖于静态变量static headOfFreeList!!!他在整个程序中只有一份,当然可以标识。

#include 
using namespace std;

class Airplane {  // 支援 customized memory management
private:
    struct AirplaneRep {
        unsigned long miles;
        char type;
    };

private:
    union {
        AirplaneRep rep;  // 此針對 used object
        Airplane *next;   // 此針對 free list
    };

public:
    unsigned long getMiles() { return rep.miles; }
    char getType() { return rep.type; }
    void set(unsigned long m, char t) {
        rep.miles = m;
        rep.type = t;
    }

public:
    static void *operator new(size_t size);
    static void operator delete(void *deadObject, size_t size);

private:
    static const int BLOCK_SIZE;
    static Airplane *headOfFreeList;
};

Airplane *Airplane::headOfFreeList;
const int Airplane::BLOCK_SIZE = 512;

void *Airplane::operator new(size_t size) {
    // 如果大小錯誤,轉交給 ::operator new()
    if (size != sizeof(Airplane))
        return ::operator new(size);

    Airplane *p = headOfFreeList;

    // 如果 p 有效,就把list頭部移往下一個元素
    if (p)
        headOfFreeList = p->next;
    else {
        // free list 已空。配置一塊夠大記憶體,
        // 令足夠容納 BLOCK_SIZE 個 Airplanes
        Airplane *newBlock = static_cast(::operator new(BLOCK_SIZE * sizeof(Airplane)));
        // 組成一個新的 free list:將小區塊串在一起,但跳過
        // #0 元素,因為要將它傳回給呼叫者。
        for (int i = 1; i < BLOCK_SIZE - 1; ++i)
            newBlock[i].next = &newBlock[i + 1];
        newBlock[BLOCK_SIZE - 1].next = 0;  // 以null結束

        // 將 p 設至頭部,將 headOfFreeList 設至
        // 下一個可被運用的小區塊。
        p = newBlock;
        headOfFreeList = &newBlock[1];
    }
    return p;
}

// operator delete 接獲一塊記憶體。
// 如果它的大小正確,就把它加到 free list 的前端
void Airplane::operator delete(void *deadObject,
                               size_t size) {
    if (deadObject == 0)
        return;
    if (size != sizeof(Airplane)) {
        ::operator delete(deadObject);
        return;
    }

    Airplane *carcass =
        static_cast(deadObject);

    carcass->next = headOfFreeList;
    headOfFreeList = carcass;
}

//-------------
void test_per_class_allocator_2() {
    cout << "test_per_class_allocator_2().......... \n";

    cout << sizeof(Airplane) << endl;  // 8

    size_t const N = 100;
    Airplane *p[N];

    for (int i = 0; i < N; ++i)
        p[i] = new Airplane;

    // 隨機測試 object 正常否
    p[1]->set(1000, 'A');
    p[5]->set(2000, 'B');
    p[9]->set(500000, 'C');
    cout << p[1] << ' ' << p[1]->getType() << ' ' << p[1]->getMiles() << endl;
    cout << p[5] << ' ' << p[5]->getType() << ' ' << p[5]->getMiles() << endl;
    cout << p[9] << ' ' << p[9]->getType() << ' ' << p[9]->getMiles() << endl;

    // 輸出前 10 個 pointers, 用以比較其間隔
    for (int i = 0; i < 10; ++i)
        cout << p[i] << endl;

    for (int i = 0; i < N; ++i)
        delete p[i];
}

void test_global_allocator() {
    cout << "test_global_allocator().......... \n";

    cout << sizeof(Airplane) << endl;  // 8

    size_t const N = 100;
    Airplane *p[N];

    for (int i = 0; i < N; ++i)
        p[i] = ::new Airplane;

    // 隨機測試 object 正常否
    p[1]->set(1000, 'A');
    p[5]->set(2000, 'B');
    p[9]->set(500000, 'C');
    cout << p[1] << ' ' << p[1]->getType() << ' ' << p[1]->getMiles() << endl;
    cout << p[5] << ' ' << p[5]->getType() << ' ' << p[5]->getMiles() << endl;
    cout << p[9] << ' ' << p[9]->getType() << ' ' << p[9]->getMiles() << endl;

    // 輸出前 10 個 pointers, 用以比較其間隔
    for (int i = 0; i < 10; ++i)
        cout << p[i] << endl;

    for (int i = 0; i < N; ++i)
        ::delete p[i];
}

int main() {
    test_per_class_allocator_2();
    cout << endl
         << endl;
    test_global_allocator();

    return 0;
}

执行结果:

侯捷老师C++课程:内存管理_第10张图片

但是这个设计有一个问题,就是你一次性拿了很多的内存,假如剩下的空白内存还很多的话,在释放的时候理应将他们还给内存,但是在上面的operator delete当中并没有将其归还给操作系统,这样不能说好也不能说不好。首先就是归还这个技术操作太难了,其次就是虽然我没有归还,但是我也没有发生内存泄漏啊,这一段内存还是在我的手上,只是被归入了freeList当中而已。

static allocator 版本3

上面的内存分配的设计对于某个指定的类是非常有效果的,但是我们不可能对于每一个类都这么干吧,所以我们需要找到一个普遍的设计方法来解决这个问题。

所以我们把上面的操作(挖一大块内存…)封装成为一个类,这个类就叫做allocator

static allocator具体可以在类里面就这么用,内存管理复杂的方面就交给这个类去管理了,不用我们对每一个类都特殊处理

注意一点就是,static变量需要在类外初始化或者定义,如图就是类外的定义。

那么allocator里面具体干什么呢?回顾一下

测试代码:

#include 
#include 
using namespace std;
#include "__allocator.h"

// macro 宏
#define DECLARE_POOL_ALLOC()                                           \
public:                                                                \
    void *operator new(size_t size) { return myAlloc.allocate(size); } \
    void operator delete(void *p) { myAlloc.deallocate(p, 0); }        \
                                                                       \
protected:                                                             \
    static __allocator myAlloc;

#define IMPLEMENT_POOL_ALLOC(class_name) \
    __allocator class_name::myAlloc;

class Foo {
    DECLARE_POOL_ALLOC()
public:
    long L;
    string str;

public:
    Foo(long l) : L(l) {}
};
// in class implementation file
IMPLEMENT_POOL_ALLOC(Foo)

//  in class definition file
class Goo {
    DECLARE_POOL_ALLOC()
public:
    complex c;
    string str;

public:
    Goo(const complex &x) : c(x) {}
};
// in class implementation file
IMPLEMENT_POOL_ALLOC(Goo)

//-------------
void test_static_allocator() {
    cout << "test_static_allocator().......... \n";

    {
        cout << endl;
        Foo *p[100];

        cout << "sizeof(Foo)= " << sizeof(Foo) << endl;
        for (int i = 0; i < 23; ++i) {  // 23,任意數, 隨意看看結果
            p[i] = new Foo(i);
            cout << p[i] << ' ' << p[i]->L << endl;
        }
        // Foo::myAlloc.check();

        for (int i = 0; i < 23; ++i) {
            delete p[i];
        }
        // Foo::myAlloc.check();

        cout << endl;
    }

    {
        cout << endl;
        Goo *p[100];

        cout << "sizeof(Goo)= " << sizeof(Goo) << endl;
        for (int i = 0; i < 17; ++i) {  // 17,任意數, 隨意看看結果
            p[i] = new Goo(complex(i, i));
            cout << p[i] << ' ' << p[i]->c << endl;
        }
        // Goo::myAlloc.check();

        for (int i = 0; i < 17; ++i) {
            delete p[i];
        }
        // Goo::myAlloc.check();

        cout << endl;
    }
}

int main() {
    test_static_allocator();

    return 0;
}

执行结果:

侯捷老师C++课程:内存管理_第11张图片
macro for static allocator 版本4 (偷懒)

因为上面的static allocator的格式写的非常固定,所以我们可以想办法给他简化一下,偷偷懒

用C++中的宏来替代,可以得出很多有趣的东西

改进后的代码:

// macro 宏
#define DECLARE_POOL_ALLOC()                                           \
public:                                                                \
    void *operator new(size_t size) { return myAlloc.allocate(size); } \
    void operator delete(void *p) { myAlloc.deallocate(p, 0); }        \
                                                                       \
protected:                                                             \
    static __allocator myAlloc;

#define IMPLEMENT_POOL_ALLOC(class_name) \
    __allocator class_name::myAlloc;

class Foo {
    DECLARE_POOL_ALLOC()
public:
    long L;
    string str;

public:
    Foo(long l) : L(l) {}
};
// in class implementation file
IMPLEMENT_POOL_ALLOC(Foo)

//  in class definition file
class Goo {
    DECLARE_POOL_ALLOC()
public:
    complex c;
    string str;

public:
    Goo(const complex &x) : c(x) {}
};
// in class implementation file
IMPLEMENT_POOL_ALLOC(Goo)

global allocator 标准库的那个非常棒的alloc

侯捷老师C++课程:内存管理_第12张图片

new handler

当operator new没有能力为我们分配成功我们所申请的memory的时候,会抛出异常 std::bad_alloc,我们应该要采取一些措施来应对这个

如果想要编译器一定要返回0而不是抛出异常的话可以这么做:

new(nothrow) Foo;

当然标准库在抛出异常之前会调用依次可以由用户指定的handler,如何设计如下所示:

这样就可以在抛出异常之前自定义一些处理操作,例如Abort()或者exit()等等

设计良好的new handler有两个作用:

  • 让更多的内存可用
  • 调用abort()或者exit()

注意:new handler必须return void,然后没有参数

#include 
using namespace std;
#include 

// new handler必须return void,然后没有参数
void noMoreMemory() {
    cerr << "out of memory\n";
    abort();
}

void test_set_new_handler() {
    cout << "test_set_new_handler().......... \n";

    set_new_handler(noMoreMemory);

    int* p = new int[100000000000000];  // well, so BIG!
    assert(p);

    p = new int[100000000000000];  //[Warning] integer constant is too large for its type
    assert(p);
}

int main() {
    test_set_new_handler();

    return 0;
}

在这个程序当中,如果不调用abort()函数,那么程序就会卡在这一行,

 int* p = new int[100000000000000];  // well, so BIG!

会一直输出自定义的错误信息 out of memory

=default,=delete

注意:

=default 只能用default只能用在big three中,即default ctor(默认构造),copy/move asgn(拷贝/移动赋值),copy/move ctor(拷贝/移动构造),dtor(析构函数)当中,因为其他的函数编译器没有提供默认的版本

=delete 则不限

#include 
using namespace std;

class Foo {
public:
    long _x;

public:
    Foo(long x = 0) : _x(x) {}

    // 这两个东西不能default,因为编译器没有默认的版本,default只能用在big three当中,即default ctor,copy/move asgn,copy/move ctor,dtor当中
    // static void *operator new(size_t size) = default;                 //[Error] cannot be defaulted
    // static void operator delete(void *pdead, size_t size) = default;  //[Error] cannot be defaulted
    static void *operator new[](size_t size) = delete;
    static void operator delete[](void *pdead, size_t size) = delete;
};

class Goo {
public:
    long _x;

public:
    Goo(long x = 0) : _x(x) {}

    static void *operator new(size_t size) = delete;
    static void operator delete(void *pdead, size_t size) = delete;
};

void test_delete_and_default_for_new() {
    cout << "test_delete_and_default_for_new().......... \n";

    Foo *p1 = new Foo(5);
    delete p1;
    // Foo* pF = new Foo[10];	//[Error] use of deleted function 'static void* Foo::operator new [](size_t)'
    // delete [] pF;			//[Error] use of deleted function 'static void Foo::operator delete [](void*, size_t)'

    // Goo* p2 = new Goo(7);	//[Error] use of deleted function 'static void* Goo::operator new(size_t)'
    // delete p2;				//[Error] use of deleted function 'static void Goo::operator delete(void*, size_t)'
    Goo *pG = new Goo[10];
    delete[] pG;
}

int main() {
    test_delete_and_default_for_new();

    return 0;
}

第二讲:std::allocator

malloc

侯捷老师C++课程:内存管理_第13张图片

当我们调用malloc函数的时候,图当中block size的部分是真实的存放我们的数据的地方,除此之外还会有其他的东西,在上下会有两包东西分别叫debug header和debug tail(这个是什么现在不去管),在整个部分的上下会有固定两个大小的cookie,记录这一段区块的大小(只有区块大小相同才可以去除cookie),也就是类似于前面per-class allocator的设计,VC6是上下各四个字节共八个字节;然后他要求这个内存块必须要满足字节数是16的倍数(不同的设计可能不同),需要有一个pad块来进行调整,整个就是malloc分配给我们的内存

不同版本allocator的实现

不同的编译器对于分配器allocator的实现都是不一样的,下面将举几个版本的例子:

VC6

VC6的版本里面没有做特殊设计,就是调用operator new/delete,进而调用malloc,free,没有对内存进行特殊管理

BC5

同样BC5的版本也没有做特殊设计

Gc2.9

Gc2.9的分配器allocator也没有做特殊设计

但是Gc2.9的容器使用的分配器却不是这个allocator,而是一个设计的非常好的alloc

下面将会介绍

pool alloc(Gc4.9) 非常棒的版本

以下是Gc2.9和Gc4.9对这个的实现

Gc4.9有很多扩充的alloctors,其中 __gnu_cxx::__pool_alloc<> 就是这个非常好的分配器

调用这个非常好的分配器的时候还要引入头文件

#include 

注意:Gc4.9当中标准库使用的分配器并不是这个很好的alloc,而是上面提到的allocator!!!

Gc4.9标准分配器allocator(不是alloc)的实现

这个分配器就是一般的调用malloc和free,不做特殊设计

那么使用alloc不适用allocator的好处是什么呢?

答案是去除了cookie,比如放入一百万个元素,使用cookie就节省了八百万个字节的空间,这样减少了内存开销

Gc2.9 std::alloc(很好的分配器)的实现

这个东西的基本原理和我们设计的per-class allocator是一样的,但是现在他设计了16条free_list,分别管理不同大小的内存,大小从小到大,8个字节,16个字节等等等;如果用户需要的大小不是8的倍数会被调整到8的倍数,然后进入对应的链表中;在该链表中一次性去取一大块的内存,在图中的设计中是20为标准量,比如可以取20*32字节的内存,这样相邻的之间就没有cookie,新的需求进来之后如果还有空间就移动指针存储就好了,没有就继续挖一大块,这样就形成了去除cookie,也是一个非常好的内存池的设计;现在如果需要的内存大小超过这个链表可以维护的最大大小,这个分配器就不用这么精妙的设计去做了,因为数据块的大小比cookie大多了,浪费是可以接受的,这个时候就调用一般的malloc就可以了

关于挖内存,这个设计还有一些细节:

当挖内存的时候,比如就32字节的那块,如果设计者设计的是20*32,但是实际上挖出来的是2个20块,每一块32个字节;其中前20块就给32字节的区块去用,后20块作为备用区块(战备池),暂时不处理,可以给负责其他大小的区块取用以此节省空间。比如这时候要64个字节,按理来说应该7号链表去挖,但是他观察到3号链表的后备区块有空间,他的指针就指向这一块,所以图中这两块是连续的,也就是说这个后备区块可以存放10个64字节的空间

所以每次要的时候都是要两倍的空间,留相同大小(这里是20个)的空间作为战备池(memory pool)

embedded pointers 内嵌入式指针

关于free_list的指针:

这个设计非常巧妙,由于分配的次序和归还的次序在实际操作的过程中可能并不是完全符合逆向,所以对于free_list他们的内存可能不是完全连续的,这很正常

但是呢,free_list之间是用指针传递的,本身是一个链表,也就不存在连续不连续的问题了

因此,free_list指向的每一个区块都是提前挖出来的内存块,为了存放下一个free块的指针,这里借用了这一个区块的前4个字节(64位电脑是8个字节)作为指针,指向下一块free内存,当这一块内存被分配给用户的时候,数据值会覆盖掉这个指针,但是没有关系,这个时候我的free_list已经通过这个指针指向下一块free内存了,这一块内存也就不属于free_list的范畴了,属于用户持有的内存了

那么为什么要这么设计呢?如果额外拿出4或者8个字节来分配给指针,一个cookie上下加起来才8个字节,那不是相当于消除了原来的cookie增添了新的指针负担吗?所以需要使用embedded pointers

那么这里考虑到一个问题,就是如果客户需要的空间本来就小于4或者8个即一个指针的大小,那这个时候指针是不是就不能借用了呢?这是正确的,但是对于工业级别的绝大多数情况,客户需要的大小肯定都是大于一根指针的大小的,所以不太需要担心这个问题

Gc2.9 std::alloc 运行一瞥(一个非常好的设计) 1-13

01

02

注意:这些链表都是指向的是free_list,而不是用户分配到的内存块

在申请内存的时候,比如申请32字节,free_list上没有,首先去找战备池有没有合适的,没有的话就在#3(对应32字节)下面申请 32 * 20 * 2 + RoundUp(0>>4) = 1280的空间

注意:

1.在实作的时候,总是优先先把分配好的内存放到战备池当中,然后再分配出去内存,比如给一块给用户,剩余19块挂在free_list[3]上面,不这么实现其实问题也不大,但是标准库这么实现了代码会漂亮很多

2.RoundUp(0>>4)是一个函数,表示一个追加量,是实现这个的这个公司设计的,具体原因不清楚,表示把一个数字调整到一定的边界,后面再说,一开始(目前)是0

然后对于cookie,在我们这样的设计之后,这一整块是用malloc拿到的,所以这一整块上下会有两个cookie

03

接上,现在我申请64字节,free_list上没有,肯定是优先查看战备池的空间,这里够用,所以把战备池当中的一块分给用户,剩余9块挂在free_list[7]上面,注意的是这两块空间在内存上是连续的!这时战备池用光了

规定:在后面在战备池上面取出空间去划分的时候,一次性划分的个数不能超过20个!!!

04

现在继续申请96字节,free_list上没有,战备池为空,需要重新申请内存,申请96 * 20 * 2 + RoundUp(1280>>4) 的内存大小,其中一块给用户,19块放到free_list[11]上,剩余的2000就是战备池

注意:关于追加量的计算

RoundUp(x>>4):用目前的累计申请量(例如现在没申请前是1280)右移4位,即除以16,然后调到8的边界

可以看出这个追加量会越来越大,随着内存的开辟

05

现在申请88字节,即#10,free_list上没有,先看战备池,最多可以划出20块,20 * 88 < 2000 ,则划分20块出去,战备池剩余2000 - 88 * 20 = 240个字节的大小

划分出去的空间一块给用户,剩余19块传到free_list[10]上面去

06

连续申请三次88字节,由于free_list[10]上有空间,直接分配给用户即可,将free_list[10]指针后移

07

接着申请8字节,free_list上没有,先看战备池,由于8 * 20 < 240 ,分配出去,战备池空间还剩80,划分出一块给用户,剩余的挂在free_list[0]上面,战备池剩余80

08

碎片处理:

这时候申请104字节大小,free_list上没有,上一次的战备池剩余80,连一个都没有办法满足;这个时候会把这个80挂载到#9号的free_list[9]上面,这个时候战备池就为空了,然后重新用malloc申请内存,各项参数如上所示

09

申请112个字节,free_list上没有,先找战备池,由于112 * 20 = 2240 < 2408,所以分配出去,现在战备池剩余168

10

申请48个字节,free_list上没有,找战备池,168 / 48 = 3,分配3个出去,一个给用户,剩下两个挂在free_list[5]上,战备池剩余24

11

现在申请72,free_list上没有,先找战备池,24满足不了,那么会申请内存,但是现在为了观察系统边界,手动将系统堆的大小设置小,现在如果在索取内存就超出边界了,显然不行,所以满足不了这次申请,那么就找距离72最近的free_list,在这里就是80,即9号,上面有一个空白的区块,好,把他切成72 + 8 的形式,72分配给用户,8就是战备池,这个时候 #8 和 #9 都没有free_list,他们的链表都是空的!!!

12

再申请72,没有free_list,,战备池不够,同时好的又malloc失败了,这个时候只有去找 #10 的空白区块了,**先处理原来的战备池,将其挂到#0号free_list的首部,即如图所示,**然后把 #10 号的第一个空白区块分成72和16,72给用户,16作为战备池

13 山穷水尽

申请120,#14 没有free_list,战备池空间不够,malloc好的不出意外又失败了,这个时候就去找#15,哦豁没有,找不到,那么就g啦!!!战备池归0

针对目前的这个现状,可以做一些思考:

侯捷老师C++课程:内存管理_第14张图片

  • 图中还有很多空白的区块未分配给用户,那么可不可以把白色的小区块合并成为大区块给用户呢?(难度太高了)
  • system heap还剩余 10000 - 9688 = 312,可不可以把剩下的312继续用光呢?

Gc2.9 std::alloc的源码剖析

第二级分配器:第二级分配器就是上面提到的alloc,当这个分配器分配不出内存的时候,实际上不会立即山穷水尽,会调用第一级分配器调用new_handler来对分配不出内存进行处理

模板参数 bool threads和int inst,在我们目前所研究的范围当中都没有派上用场

Round_Up():计算追加量

embedded pointers:嵌入式指针

在这个类里面最先定义这个指针成员,也就达到了我们需要的借用头部作为指针,后面容器进来之后覆盖并且移动指针到下一个位置就可以了

union obj{
	union obj* free_list_link;
};

free_list[]:静态,全局只有一份,代表那16个链表

FREELIST_INDEX():计算数组下标,计算出由第几号链表提供服务

战备池相关

  • start_free:指向pool首部
  • end_free:指向pool尾部

head_size:统计累计分配量

还有两个函数目前尚不清楚怎么实现:

refill():从内存池中申请空间并构建free list,然后从free list中分配空间给用户

chunk_alloc():从内存池中分配空间

然后就是最重要的allocate()和deallocate()函数

allocate函数中:如果需要的空间太大超过范围就调用第一级分配器;然后去查询free_list当中是否可以分配,如果可以分配那么就分配就好了;如果没有就调用refill()函数,从内存池中申请空间并且构建free_list,然后分配一块给用户,至于是战备池还是战备池不够处理碎片然后malloc申请,或者是malloc失败去找后面的空白区块,这就是refill的事情了,现在尚不清楚

deallocate函数中:如果空间太大,调用第一级,与allocate配套;否则把free_list[]指针前移

那么问题来了,为什么这里不调用free()释放还给操作系统呢?

前面知道,由于各种原因,free_list[]的指向并不一定是连续的,但是他们之间是用链表实现的,给我们的感官是这样的;不连续的话贸然去free()就可能会出问题,所以他不还给操作系统(个人感觉这里不是很合理)

另一个问题就是没有对这个p指针进行检查,如果他不是这个分配器给出来的指针,他指向空间的大小可能就不是8的倍数,这样如果执行这段代码可能就会造成不可逆转的结果了

refill()函数:

这个函数的作用就是在free_list没有空间的时候,内部调用chunk_alloc()申请内存池并且分配给用户和free_list,然后把申请到的free_list给串起来

然后拿一大块内存的事情就交给chunk_alloc()函数去实现

chunk_alloc()函数:

这里就是去要一大块内存,先去看战备池,他这里是先看能不能满足最大的需求,就是规定的战备池最多提供20块,不能的话看能切出几块,然后修改指针;

如果无法满足那么就代表战备池无法满足,那么就把这个碎片进行处理(给他放到对应的free_list的首部),然后准备计算接下来需要malloc拿到的空间,然后尝试去拿取;malloc拿到的空间前面提到是2 * 20 * 32 比如,先全部放到战备池当中,然后切出一半来给用户和free_list分配;失败了说明系统heap空间不够了,那么就尝试去这个大小的链表后面去找可用的空间,将其一块分为用户和战备池,如果这还找不到就山穷水尽,g!

然后就剩下一些类外的初始化和typedef的操作了

整合了一下代码:

//第一级分配器
#ifndef _STD_ALLOC_1ST_H_
#define _STD_ALLOC_1ST_H_

#define __THROW_BAD_ALLOC            \
    cerr << "out of memory" << endl; \
    exit(1)
//----------------------------------------------
// 第1級配置器。
//----------------------------------------------
template 
class __malloc_alloc_template {
private:
    static void *oom_malloc(size_t);
    static void *oom_realloc(void *, size_t);
    static void (*__malloc_alloc_oom_handler)();

public:
    static void *allocate(size_t n) {
        void *result = malloc(n);  // 直接使用 malloc()
        if (0 == result)
            result = oom_malloc(n);
        return result;
    }
    static void deallocate(void *p, size_t /* n */) {
        free(p);  // 直接使用 free()
    }
    static void *reallocate(void *p, size_t /* old_sz */, size_t new_sz) {
        void *result = realloc(p, new_sz);  // 直接使用 realloc()
        if (0 == result)
            result = oom_realloc(p, new_sz);
        return result;
    }
    static void (*set_malloc_handler(void (*f)()))() {  // 類似 C++ 的 set_new_handler().
        void (*old)() = __malloc_alloc_oom_handler;
        __malloc_alloc_oom_handler = f;
        return (old);
    }
};
//----------------------------------------------
template 
void (*__malloc_alloc_template::__malloc_alloc_oom_handler)() = 0;

template 
void *__malloc_alloc_template::oom_malloc(size_t n) {
    void (*my_malloc_handler)();
    void *result;

    for (;;) {  // 不斷嘗試釋放、配置、再釋放、再配置…
        my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == my_malloc_handler) {
            __THROW_BAD_ALLOC;
        }
        (*my_malloc_handler)();  // 呼叫處理常式,企圖釋放記憶體
        result = malloc(n);      // 再次嘗試配置記憶體
        if (result)
            return (result);
    }
}

template 
void *__malloc_alloc_template::oom_realloc(void *p, size_t n) {
    void (*my_malloc_handler)();
    void *result;

    for (;;) {  // 不斷嘗試釋放、配置、再釋放、再配置…
        my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == my_malloc_handler) {
            __THROW_BAD_ALLOC;
        }
        (*my_malloc_handler)();  // 呼叫處理常式,企圖釋放記憶體。
        result = realloc(p, n);  // 再次嘗試配置記憶體。
        if (result)
            return (result);
    }
}
//----------------------------------------------

typedef __malloc_alloc_template<0> malloc_alloc;

template 
class simple_alloc {
public:
    static T *allocate(size_t n) {
        return 0 == n ? 0 : (T *)Alloc::allocate(n * sizeof(T));
    }
    static T *allocate(void) {
        return (T *)Alloc::allocate(sizeof(T));
    }
    static void deallocate(T *p, size_t n) {
        if (0 != n)
            Alloc::deallocate(p, n * sizeof(T));
    }
    static void deallocate(T *p) {
        Alloc::deallocate(p, sizeof(T));
    }
};

#endif
//第二级分配器
#ifndef _STD_ALLOC_2ND_H_
#define _STD_ALLOC_2ND_H_

#include "std_alloc_1st.h"
using namespace std;
#include 

// 第二級配置器
//----------------------------------------------
enum {
    __ALIGN = 8
};  // 小區塊的上調邊界
enum {
    __MAX_BYTES = 128
};  // 小區塊的上限
enum {
    __NFREELISTS = __MAX_BYTES / __ALIGN
};  // free-lists 個數

// 本例中兩個 template 參數完全沒有派上用場
template 
class __default_alloc_template {
private:
    // 實際上應使用 static const int x = N
    // 取代 enum { x = N }, 但目前支援該性質的編譯器不多

    static size_t ROUND_UP(size_t bytes) {
        return (((bytes) + __ALIGN - 1) & ~(__ALIGN - 1));
    }

private:
    union obj {
        union obj *free_list_link;
    };

private:
    static obj *volatile free_list[__NFREELISTS];
    static size_t FREELIST_INDEX(size_t bytes) {
        return (((bytes) + __ALIGN - 1) / __ALIGN - 1);
    }

    // Returns an object of size n, and optionally adds to size n free list.
    static void *refill(size_t n);

    // Allocates a chunk for nobjs of size "size".  nobjs may be reduced
    // if it is inconvenient to allocate the requested number.
    static char *chunk_alloc(size_t size, int &nobjs);

    // Chunk allocation state.
    static char *start_free;
    static char *end_free;
    static size_t heap_size;

public:
    static void *allocate(size_t n)  // n must be > 0
    {
        obj *volatile *my_free_list;  // obj** my_free_list;
        obj *result;

        if (n > (size_t)__MAX_BYTES) {
            return (malloc_alloc::allocate(n));
        }

        my_free_list = free_list + FREELIST_INDEX(n);
        result = *my_free_list;
        if (result == 0) {
            void *r = refill(ROUND_UP(n));
            return r;
        }

        *my_free_list = result->free_list_link;
        return (result);
    }

    static void deallocate(void *p, size_t n)  // p may not be 0
    {
        obj *q = (obj *)p;
        obj *volatile *my_free_list;  // obj** my_free_list;

        if (n > (size_t)__MAX_BYTES) {
            malloc_alloc::deallocate(p, n);
            return;
        }
        my_free_list = free_list + FREELIST_INDEX(n);
        q->free_list_link = *my_free_list;
        *my_free_list = q;
    }

    static void *reallocate(void *p, size_t old_sz, size_t new_sz);
};
//----------------------------------------------
// We allocate memory in large chunks in order to
// avoid fragmentingthe malloc heap too much.
// We assume that size is properly aligned.
// We hold the allocation lock.
//----------------------------------------------
template 
char *
__default_alloc_template::
    chunk_alloc(size_t size, int &nobjs) {
    char *result;
    size_t total_bytes = size * nobjs;
    size_t bytes_left = end_free - start_free;

    if (bytes_left >= total_bytes) {
        result = start_free;
        start_free += total_bytes;
        return (result);
    } else if (bytes_left >= size) {
        nobjs = bytes_left / size;
        total_bytes = size * nobjs;
        result = start_free;
        start_free += total_bytes;
        return (result);
    } else {
        size_t bytes_to_get =
            2 * total_bytes + ROUND_UP(heap_size >> 4);
        // Try to make use of the left-over piece.
        if (bytes_left > 0) {
            obj *volatile *my_free_list =
                free_list + FREELIST_INDEX(bytes_left);

            ((obj *)start_free)->free_list_link = *my_free_list;
            *my_free_list = (obj *)start_free;
        }
        start_free = (char *)malloc(bytes_to_get);
        if (0 == start_free) {
            int i;
            obj *volatile *my_free_list, *p;

            // Try to make do with what we have. That can't
            // hurt. We do not try smaller requests, since that tends
            // to result in disaster on multi-process machines.
            for (i = size; i <= __MAX_BYTES; i += __ALIGN) {
                my_free_list = free_list + FREELIST_INDEX(i);
                p = *my_free_list;
                if (0 != p) {
                    *my_free_list = p->free_list_link;
                    start_free = (char *)p;
                    end_free = start_free + i;
                    return (chunk_alloc(size, nobjs));
                    // Any leftover piece will eventually make it to the
                    // right free list.
                }
            }
            end_free = 0;  // In case of exception.
            start_free = (char *)malloc_alloc::allocate(bytes_to_get);
            // This should either throw an exception or
            // remedy the situation. Thus we assume it
            // succeeded.
        }
        heap_size += bytes_to_get;
        end_free = start_free + bytes_to_get;
        return (chunk_alloc(size, nobjs));
    }
}
//----------------------------------------------
// Returns an object of size n, and optionally adds
// to size n free list.We assume that n is properly aligned.
// We hold the allocation lock.
//----------------------------------------------
template 
void *__default_alloc_template::
    refill(size_t n) {
    int nobjs = 20;
    char *chunk = chunk_alloc(n, nobjs);
    obj *volatile *my_free_list;  // obj** my_free_list;
    obj *result;
    obj *current_obj;
    obj *next_obj;
    int i;

    if (1 == nobjs)
        return (chunk);
    my_free_list = free_list + FREELIST_INDEX(n);

    // Build free list in chunk
    result = (obj *)chunk;
    *my_free_list = next_obj = (obj *)(chunk + n);
    for (i = 1;; ++i) {
        current_obj = next_obj;
        next_obj = (obj *)((char *)next_obj + n);
        if (nobjs - 1 == i) {
            current_obj->free_list_link = 0;
            break;
        } else {
            current_obj->free_list_link = next_obj;
        }
    }
    return (result);
}
//----------------------------------------------
template 
char *__default_alloc_template::start_free = 0;

template 
char *__default_alloc_template::end_free = 0;

template 
size_t __default_alloc_template::heap_size = 0;

template 
typename __default_alloc_template::obj *volatile __default_alloc_template::free_list[__NFREELISTS] = {
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
};

//----------------------------------------------
// 令第2級配置器的名稱為 alloc
using alloc = __default_alloc_template;

#endif
//测试程序
#include 
using namespace std;
#include "std_alloc_2nd.h"

void test_G29_alloc() {
    cout << "test_global_allocator_with_16_freelist().......... \n";

    void *p1 = alloc::allocate(120);
    void *p2 = alloc::allocate(72);
    void *p3 = alloc::allocate(60);  // 不是 8 倍數

    cout << p1 << endl
         << p2 << endl
         << p3 << endl;

    alloc::deallocate(p1, 120);
    alloc::deallocate(p2, 72);
    alloc::deallocate(p3, 60);

    // 以下, 不能搭配容器來測試, 因為新版 G++ 對於 allocator 有更多要求 (詢問更多 typedef 而 alloc 都無法回答)
    // 它其實就是 G4.9 __pool_alloc,所以讓 G4.9容器使用 __pool_alloc 也就等同於這裡所要的測試
    /*
        vector> v;
        for(int i=0; i< 1000; ++i)
            v.push_back(i);
        for(int i=700; i< 720; ++i)
            cout << v[i] << ' ';
    */
}

int main() {
    test_G29_alloc();

    return 0;
}

执行结果:

image-20230531113514416

Gc2.9 std::alloc观念大整理

假设这里list<>使用的分配器是std::alloc,list除了本身的Foo之外,还带有list的两根指针,如果sizeof(Foo) + 2 * 指针大小 < 128,那么可以调用alloc分配器;

第二行list<>容器调用push_back()插入操作,Foo(1)是临时对象,放在栈区,然后调用copy ctor,把他放到alloc创造的空间当中,这一段空间不带有cookie;

第二个操作把一个由malloc动态开辟出来带有cookie的空间copy到alloc创造的空间当中,同样也不带有cookie

注意等号判断的写法,这里推荐把右值放在等号左边,为什么呢?

这样的话,如果我不小心把 == 号写成了 = 号,那么 0 = start_free是没办法通过的,会报错,如果交换顺序则会通过,造成的后果是非常严重的,所以强烈建议判断 == 的时候把右值放在等号左边!!

侯捷老师C++课程:内存管理_第15张图片

当没有内存可以分配的时候,分配器会去找已有的内存,也就是上面往后找一块free_list下的内存块划分给用户和战备池,如果这都没有的话就g啦。但是当时我们提到过,可以把剩余的内存(比如9688到10000还有312),可以把剩余的内存减半再减半,直到可以被分配出来,这样不是可以更好的利用内存吗?想法是肯定的,但是上面说在多进程的机器上会带来大灾难,这里的理解不一了,一种理解是这不是更好的利用内存了嘛?另一种是你把内存尽可能的使用了,其他人怎么办呢?Gc2.9选择了后者,如果想实现前者,也不是不可以,只是难度比较大

另外一个问题就是他的deallocate()函数不进行内存的归还,这也是受限制于这个设计先天的缺陷吧,链表指来指去,内存不一定连续,这样就没有把握free内存

测试代码:

#include 
using namespace std;
#include 
#include 
#include 

template 
using listPool = list>;

static long long countNew = 0;
static long long countDel = 0;
static long long countArrayNew = 0;
static long long countArrayDel = 0;
static long long timesNew = 0;

// 两个函数
void* myAlloc(size_t size) {
    return malloc(size);
}

void myFree(void* ptr) {
    free(ptr);
}

// 重载全局operator new/delete
inline void* operator new(size_t size) {
    // cout << "global new(), \t" << size << "\t";
    countNew += size;
    ++timesNew;

    return myAlloc(size);
}

inline void* operator new[](size_t size) {
    // cout << "global new[](), \t" << size << "\t";
    countArrayNew += size;

    void* p = myAlloc(size);
    cout << p << endl;
    return p;

    return myAlloc(size);
}

// 天啊, 以下(1)(2)可以並存並由(2)抓住流程 (但它對我這兒的測試無用).
// 當只存在 (1) 時, 抓不住流程.
// 在 class members 中二者只能擇一 (任一均可)
//(1)
inline void operator delete(void* ptr, size_t size) {
    // cout << "global delete(ptr,size), \t" << ptr << "\t" << size << endl;
    countDel += size;
    myFree(ptr);
}
//(2)
inline void operator delete(void* ptr) {
    // cout << "global delete(ptr), \t" << ptr << endl;
    myFree(ptr);
}

//(1)
inline void operator delete[](void* ptr, size_t size) {
    // cout << "global delete[](ptr,size), \t" << ptr << "\t" << size << endl;
    countArrayDel += size;
    myFree(ptr);
}
//(2)
inline void operator delete[](void* ptr) {
    // cout << "global delete[](ptr), \t" << ptr << endl;
    myFree(ptr);
}

void test_overload_global_new() {
    cout << "test_overload_global_new().......... \n"
         << endl;

    //***** 測試時, main() 中的其他測試全都 remark, 獨留本測試 *****
    {
        cout << "::countNew= " << ::countNew << endl;  // 0
        cout << "::countDel= " << ::countDel << endl;  // 0
        cout << "::timesNew= " << ::timesNew << endl;  // 0

        string* p = new string("My name is Ace");  // jjhou global new(), 4 	(註:這是 string size)
                                                   // jjhou global new(), 27	(註:這是 sizeof(Rep)+extra)
        delete p;                                  // jjhou global delete(ptr), 0x3e3e48
                                                   // jjhou global delete(ptr), 0x3e3e38

        cout << "::countNew= " << ::countNew << endl;  // 31 ==> 4+27
        cout << "::timesNew= " << ::timesNew << endl;  // 2
        cout << "::countDel= " << ::countDel << endl;  // 0 <== 本測試顯然我永遠觀察不到我所要觀察的
                                                       //       因為進不去 operator delete(ptr,size) 版

        p = new string[3];  // jjhou global new[](), 16 (註:其中內含 arraySize field: 4 bytes,
                            // 所以 16-4 = 12 ==> 4*3, 也就是 3 個 string 每個佔 4 bytes)
        // jjhou global new(), 13  	//Nil string
        // jjhou global new(), 13	//Nil string
        // jjhou global new(), 13	//Nil string

        delete[] p;  // jjhou global delete(ptr),   0x3e3e88
                     // jjhou global delete(ptr),   0x3e3e70
                     // jjhou global delete(ptr),   0x3e39c8
                     // jjhou global delete[](ptr), 0x3e3978

        cout << "::countNew= " << ::countNew << endl;            // 70 ==> 4+27+13+13+13
        cout << "::timesNew= " << ::timesNew << endl;            // 5
        cout << "::countArrayNew= " << ::countArrayNew << endl;  // 16 (這個數值其實對我而言無意義)

        // 測試: global operator new 也會帶容器帶來影響
        vector vec(10);  // jjhou global new(), 	40  	0x3e3ea0  (註:10 ints)
                              // 註:vector object 本身不是 dynamic allocated.
        vec.push_back(1);
        // jjhou global new(), 	80		0x3e3ed0
        // jjhou global delete(ptr), 	0x3e3ea0
        vec.push_back(1);
        vec.push_back(1);

        cout << "::countNew= " << ::countNew << endl;  // 190 ==> 70+40+80
        cout << "::timesNew= " << ::timesNew << endl;  // 7

        list lst;                                 // 註:list object 本身不是 dynamic allocated.
        lst.push_back(1);                              // jjhou global new(), 	12	(註:每個 node是 12 bytes)
        lst.push_back(1);                              // jjhou global new(), 	12
        lst.push_back(1);                              // jjhou global new(), 	12
        cout << "::countNew= " << ::countNew << endl;  // 226 ==> 190+12+12+12
        cout << "::timesNew= " << ::timesNew << endl;  // 10

        // jjhou global delete(ptr), 	0x3e3978
        // jjhou global delete(ptr), 	0x3e39c8
        // jjhou global delete(ptr), 	0x3e3e70
        // jjhou global delete(ptr), 	0x3e3ed0
    }

    cout << endl
         << endl;

    {
        // reset countNew
        countNew = 0;
        timesNew = 0;

        // list> lst;
        // 上一行改用 C++/11 alias template 來寫 :
        listPool lst;

        for (int i = 0; i < 1000000; ++i)
            lst.push_back(i);
        cout << "::countNew= " << ::countNew << endl;  // 16752832 (注意, node 都不帶 cookie)
        cout << "::timesNew= " << ::timesNew << endl;  // 122
    }

    cout << endl
         << endl;

    {
        // reset countNew
        countNew = 0;
        timesNew = 0;
        list lst;
        for (int i = 0; i < 1000000; ++i)
            lst.push_back(i);
        cout << "::countNew= " << ::countNew << endl;  // 16000000 (注意, node 都帶 cookie)
        cout << "::timesNew= " << ::timesNew << endl;  // 1000000
    }
}

int main() {
    // 为了防止刷屏我把operator new/delete里面输出的内容给注释了
    test_overload_global_new();

    return 0;
}

执行结果:

侯捷老师C++课程:内存管理_第16张图片

第四讲:loki::allocator

第三讲将malloc和free,太难了,这部分暂时没听.

Loki::allocator设计

Loki分配器和std::alloc分配器的区别,std::alloc分配器的致命伤是他要到一大块内存之后进行设计,然后分配给用户之后,用户发出释放内存的操作的时候,分配器内部的实现是不归还给操作系统的,他很霸道,官方的解释是一是实现很难,二是归还这个操作可能在多任务进程中会影响其他进程的操作,而Loki分配器就解决了这个问题

如何设计?

三层结构如上:

最下层Chunk,存放这一块的指针,这一块的索引和目前还可以供应的区块

中间层FixedAllocator,存放vector< Chunk >和两根指向Chunk的指针

最上面的层SmallObjAllocator,也就是用户面对的层次,存放vector< FixedAllocator >和两根指向FixedAllocator的指针

至于为什么要指向某两个,这两个可能还有其他的特殊作用,这个后面再说

源代码

Chunk类

这些函数都是高层的类调用底层的类的,我们用户在实际操作的时候没有必要直接调用这个

Init()函数:挖出一大块内存用于操作,单位大小和区块个数都经过了调整

Reset()函数:对这大块内存进行分配,就是标出Chunk那三块, 其中用流水线的方式表示索引,把最前面一个字节的空间占据l来当作索引,概念类似于embedded pointer(嵌入式指针),只不过这里是嵌入式索引

重头戏:allocate()和deallocate()

allocate()函数:在初始化Init()函数之后调用allocate()函数,这个时候每一个block的大小都是blocksize,firstAvaliableBlock代表了这个时候第一块空白的内存,存放的是索引,就是从0开始按顺序去数,并且优先度最高,它里面存放的区块索引是之前设计好的,就是第二高的优先度,第一块内存分配给用户之后,第二块就上来了,变成了firstAvailableBlock的内容,然后blocksAvailable减减

deallocate()函数:给一个指针,当然需要先判断这跟指针位于哪个Chunk结构当中,这就一个一个去查询(查询指针的位置)就好了,找到之后把这一块内存free掉,做allocate()函数的逆操作,把目前状态的firstAvailable的区块索引填入该区块,然后该区块索引取代他成为firstAvailableBlock,然后blocksAvailable加加,这就保证了操作的严谨和自洽

FixedAllocator类

FixedAllocator类的结构:

侯捷老师C++课程:内存管理_第17张图片

除了存放vector< Chunk >之外,还有两根指针,用来标识最近一次alloc的Chunk和dealloc的Chunk,这么做的含义是,可以从这个最近的区块当中看是否可以继续分配或者回收来提高效率

Allocate代码:

如果标识allocChunk为0(例如最开始的时候没有分配内存)或者allocChunk指向的blockAvailable为0代表没有课余空间,那么表明allocChunk不可用,需要从头重新开始查找

在查找过程中如果发现可用的,记下地址然后去分配,如果没有可用的(都用光了),这时候加一个新的chunk进去用于新的分配!!!

注意加了新的chunk之后记得更改allocChunk和deallocChunk的值,allocChunk就设置为改Chunk就可以,因为这是新生成的,可以进行分配;deallocChunk设置为头部

但是为什么要重设呢?因为vector在push_back操作中可能出现2倍扩张导致move的问题,这个时候原来的指针就失效了,显然需要重新设置!!!

deallocate代码:

侯捷老师C++课程:内存管理_第18张图片

拿到一个需要释放的指针,先找到需要释放的Chunk区块,通过VicinityFind()实现,然后通过DoDeallocate()进行释放

VicinityFind()函数:

他这种写法其实没有特别的数学依据,大致思想就是从上一个deallocChunk开始,将其分为上下两个部分,上面找一个,下面找一个,直到找到对应的区块,然后进行修改

但是如果给的p不是Chunk分配拿出的指针,那么显然是找不到的,所以会死循环

DoDeallocate()函数:

这里要注意如果回收之后这一块Chunk变为全空,需要把这一块进行回收嘛?这就不是内存分配的重点了

第五讲:other issues

new_allocator

侯捷老师C++课程:内存管理_第19张图片

这几个版本实现不同,但是其实本质上没有进行额外的设计,就是对c runtime libirary里面malloc和free的调用

array_allocator

这个分配器的目的是分出一块静态内存array(C++数组),然后分配给用户,由于是静态,所以不需要进行回收,因此按道理来说不需要deallocate()。但是分配器都需要提供这些统一的接口,所以他do nothing.

注意第二个模板参数传入的参数必须是array<>!!!

例子:

静态数组充当分配单元

侯捷老师C++课程:内存管理_第20张图片

动态开辟的空间充当分配单元

debug_allocator

用途:包裹其另一个分配器分配的空间,添加一个extra的空间用来记录分配的大小(类似于cookie,因为cookie当中也记录着整块的大小)

但是感觉没什么用,试想一下我刚好去除了cookie构造了一个不错的内存池,然后用这个debug_allocator又添加了一个类似于cookie的debug header,这不是很鸡肋嘛

Gc2.9使用的std::alloc (__pool_alloc)

太熟悉啦!

bitmap_allocator

该分配器就allocate()和deallocate()函数做了两种;

就容器需要的元素种类个数为1和以上做了区分,但是实际上绝大多数情况下容器存放的东西都是一种类型,不同的类型很少

侯捷老师C++课程:内存管理_第21张图片

下面他的设计用图示实现

allocate过程:

侯捷老师C++课程:内存管理_第22张图片

还是以内存池的形式,挖出一大块内存然后进行分配。在这里这个团队设计的是一开始挖出64个指定类型的区块,然后填上bitmap,use count和一个头部记录super block size,因此这一整块就叫做super block。

注意bitmap是怎么确定大小的,bitmap里面存的是16进制数,数组的形式,一个数组值4个字节,也就是可以放图中的4 * 16 = 64 个block的状态,1代表已存放,0代表未存放

整个super block大小的计算如上

对于整个bitmap_allocator的控制,使用的是自己设计的一个建议__mini_vector,因为标准库的vector底层还有分配器,用他的话就相当于套娃了,这里就是一个简单的版本,里面同样有三根指针,然后同样是2倍扩充,机制是一样的

当填充进数据:

侯捷老师C++课程:内存管理_第23张图片

记得修改use count;

修改bitmap数组的值,这里的顺序是反着来的,已分配设为0,未分配设为1,图中的1110代表 block的前四位 第一位0 分配出去了,所以就是E

当第一个super_block用尽,启动第二个super_block:

侯捷老师C++课程:内存管理_第24张图片

block的个数由64加倍,变为128;然后记得修改__mini_vector的值,记得vector是两倍成长的

继续:

侯捷老师C++课程:内存管理_第25张图片

当需要启动第三个super_block的时候,这个时候进行2倍扩张,就变成了4个区块

这个团队规定:如果不全回收,分配规模不断增大;如果全回收了下一次规模减半

这个vector的value_type并没有限制,因为他的每一个区块都是在自己的value_type之下构成的,我的vector只起了一个管理的作用!!!

从图中也可以看出我们的vector里面存放的只是两根指针,指向值的第一个block和最后一个block!!!

deallocate过程:

如果把第一个super block全回收了,那么第一个super block会被放入一个free_list当中(最多64个,多了会被归还给操作系统)用作下一个分配的备用空间,然后在__mini_vector当中,会把第一个元素给删除掉,做类似于erase()的操作,只不过erase()函数需要减少size的,但是这里并没有减少,只是看起来是将元素向前推了,多出的空间就是空白

侯捷老师C++课程:内存管理_第26张图片

侯捷老师C++课程:内存管理_第27张图片

整个过程大致就是这样

谈谈const

当成员函数的const和non-const版本同时存在,const对象只能调用const版本,non-const对象只能调用non-const版本

因此,const也是区分函数是否相同的标志,同一个函数加上const和不加就相当于是一个重载的版本了

#include 
using namespace std;

class Fuck {
public:
    void fuck() const {
        cout << "const version" << endl;
    }

    void fuck() {
        cout << "non-const version" << endl;
    }
};

void test() {
    const Fuck f1;
    f1.fuck();// const version

    Fuck f2;
    f2.fuck();// non-const version
}

int main() {
    test();

    return 0;
}

你可能感兴趣的:(侯捷老师C++课程,c++)