C++ 中的 new

C++ 中使用 new 关键字来创建一个对象,创建对象的过程会产生以下操作:

  1. 在内存中申请一块空间,大小为目标对象的大小,并获得该内存空间的地址。
  2. 将该内存空间的地址转换为对象指针。
  3. 通过对象指针调用该对象的构造函数。

对于第一步来说,C++如何获得内存空间的呢?

答案是通过调用operator new(size_t size) 函数,其参数为要申请的空间大小,内部实现依然是通过 C 语言的 malloc 函数。值得一提的是,这个函数是可以进行重载的,这就为我们的内存管理留下了可操作空间。

默认情况下,new 表达式调用的是全局的 ::operator new(size_t size) 函数来申请内存空间,当我们在类中定义了成员函数 operator new(size_t size),编译器就会调用类中的成员operator new 函数来分配内存空间,所有内存管理的操作,也可以在成员operator new 函数中来实现。

delete 与 new 同理,会先通过指针调用该对象的析构函数,再调用 operator delete 函数来释放内存空间,其中 operator delete 也分为全局函数和成员函数,也支持重载,全局 operator delete 函数内部使用 C 语言的 free 来释放内存空间。

用代码表示为:

Foo* p = new Foo(x); // new 表达式,不可改变,不可重载
/*
 * 一段其他逻辑后 
 */
delete p;

上面这一行代码会被转化为:

Foo* p = (Foo*)operator new(sizeof(Foo)); // 调用 operator new 函数申请内存空间
new(p) Foo(x); // 此处为 placement new ,在new() 小括号中传入一个指针,在该指针所指的空间内调用构造函数创建对象。
/*
 * 一段其他逻辑后 
 */
p->~Foo();        // 调用析构函数
operator delete(p); // 调用operator delete 函数释放内存空间

我们所面临的问题是,当用户需要大量的创建 Foo 对象时,需要重复的反复的调用 malloc 函数来申请内存空间,这会带来两个问题:

  • 调用 malloc 的次数太多,从而影响效率。(在 C++ 中申请和释放内存空间最终都是通过使用 C 语言的 malloc 和 free 函数来实现。其实 malloc 函数的执行速度并不慢,内存管理的目的也不是为了减少 malloc 的次数,当然,即使 malloc 函数的效率很高,减少它的调用次数也依然是好事情。)
  • 浪费空间,C++为每个对象申请到的内存空间都会使用 cookie 来标志起始和结束位置,每个cookie各占用 4 个字节,如果你的对象数据成员很少的话,这将会是很大比例的浪费。

为了解决上面那两个问题,我们就需要定义成员operator new 和operator delete 函数,让 new 和 delete 的行为有我们自己控制。

内存管理的一些例子

第一个例子取材于 C++ Primer。

这个例子通过成员operator new 函数实现一次性申请足够大的空间(只有两个 cookie),在创建对象时将申请到的空间拿出一部分来构造对象。为了维护空间内的对象,为对象附加一个指针变量,使其形成链表的形式。这就是内存池的思想。

image-20220331005041623

#include <cstddef>
#include <iostream>
using namespace std;

class Screen{
public:
    Screen(int x):i(x){};
    int get(){return i;}
    
    void* operator new(size_t);
    void operator delete(void*,size_t);
    // ...
private:
    // 为内存管理而设计的数据成员
    Screen* next;
    static Screen* freeStore;
    static const int screenChunk;
private:
    // 原本的数据成员
    int i;
}
// 类外初始化静态成员变量
Screen* Screen::freeStore = nullptr;
const int Screen::screenChunk = 6;


void* Screen::operator new(size_t size){
    Screen* p;
    if(freeStroe){
        // 内存池是空的,一次性申请 screenChunk 个 Screen 对象大小的内存空间
        size_t chunk = screenChunk * size;
        freeStore = p = reinterpret_cast<Screen*>(new char[chunk]);
        // 将一大块内存空间分割成小块,当成链表串联在一起
        for(; p != &freeStore[screenChunk-1];++p){
            p->next = p+1;
        }
        p->next = 0;
    }
    p = freeStore;
    freStore = freeStore->next;
    return p;
}

void Screen::operator delete(void* p,size_t ){
    // 将删除的对象插回 free list 的前端
    (static_cast<Screen*>(p))->next = freeStore;
    freeStore = static_cast<Screen*>(p);
}

这个版本的代码实现了基本的内存池,通过单向链表的方式维护了内存池,这种方式需要在类中专门定义一个用来维护内存池的指针,在本例中,用来维护内存池的数据成员和类原始数据成员一样大,显然一定程度上造成了空间的浪费。

下面的代码采用内嵌指针的方式解决上面代码中的问题。

class Airplane{
private:
    struct AirplaneRep{         // 类的原始数据成员
        unsigned long miles;
        char type;
    };
private:
    union{ // 嵌入式指针
        AirplaneRep rep; // 
        Airplane* next;  //
    };
public:
    unsigned long getMiles(){
        return rep.miles;
    }
    char getType(){return rep.type;}
    void set(unsigned long m,chat 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){
    if(size != sizeof(Airplane)){  // 继承的情况下会出现不相等的情况
        return ::operator new(size);
    }
    
    Airplane* p = headOfFreeList;
    if(p){
        // 如果 p 有效,把list 的头部下移一个单元
        headOfFreeList = p -> next;
    }
    else{
        // free list 已空,申请一大块内存
        Airplane* newBlock = static_cast<Airplane*>
            (::operator new(BLOCK_SIZE*sizeof(Airplane)));
        
        // 将小块连成一个 freelist
        // 跳过 第 0 块,因为它是本次 new 的结果
        for(int i = 1;i < BLOCK_SIZE-1;i++){
            newBlock[i].next = &newBlock[i+1];
        }
        newBlock[BLOCK_SIZE-1].next = 0;
        p = newBlock;
        headOfFreeList = &newBlock[1];
    }
    return p;
}

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<Airplane*>(deadObject);
    carcass->next = headOfFreeList;
    headOfFreeList = carcass;
}

在上面例子中,使用嵌入式指针的方式避免了额外的空间开销。

想象这样一种情况,使用者 new 了 10000 个对象,然后 delete 了 5000 个,又 new 了 6000 个,又 delete 10000 个…

这种情况下,每次释放的空间会呗插入到链表的头部,反复这样操作会出现一个链表长度的峰值,即使实际对象数量小于链表的长度,使得链表中有很多空闲空间,这样的情况也不算是内存泄漏

当然,如果这些空闲的空间能想办法还给操作系统就更好了。

上面版本的内存管理操作已经很好了,但是对于不同的需要内存管理 class 都需要写一个这样的函数,为了避免过多的重复性的代码,我们把上述内存管理操作封装成类 allocator ,这样在需要内存管理的类中包含一个 allocator 对象即可。

class allocator{
private:
    struct obj{
        struct obj* next; // 内嵌指针
    };
public:
    void* allocate(size_t);
    void deallocate(void*,size_t);
private:
    obj* freeStore = nullptr;
    const int CHUNK = 5;
}

void allocator::deallocate(void* p,size_t){
    ((obj*)p)->next = freeStore;
    freeStore = (obj*)p;
}

void* allocator::allocate(size_t size){
    obj* p;
    if(!freeStore){
        freeStore = p = (obj*)malloc(size*CHUNK);
        
        for(int i=0;i<(CHUNK-1);++i){
            p->next = (obj*)((char*)p + size);
            p = p->next;
        }
        p->next = nullptr;
    }
    p = freeStore;
    freeStore = freeStore->next;
    return p;
}


// 对于其他需要内存管理的类来说 只需要在类中使用 allocator 成员即可。
class Foo{
public:
    long L;
    string str;
    static allocator myAlloc; // 用于内存管理
public:
    Foo(long l):L(l){}
    // 重载这两个函数
    static void* operator new(size_t size){
        return myAlloc.allocate(size);
    }
    static void operator delete(void* p,size_t size){
        return myAlloc.deallocate(p,size);
    }
}
allocator Foo::myAlloc;

上面的设计比前两种的设计都干净多了,不再需要在分配内存的时候进行指针的类型的各种转型。

进一步可以发现,每个需要内存管理的类需要添加的内容依然很有规律性,它需要添加下面这部分代码:

public:
	static void* operator new(size_t size){
        return myAlloc.allocate(size);
    }
	static void operator delete(void* p,size_t size){
        return myAlloc.deallocate(p,size);
    }
protected:
	static allocator myAlloc;

// 在类外添加一行静态成员的定义
allocator class_name::myAlloc;

于是为了更加的简洁,我们可以把上述代码写成宏的方式。

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

#define IMPLEMENT_POOL_ALLOC(calss_name) \
allocator class_name::myAlloc;

// 于是,我们的 Foo 类就可以写成
class Foo{
    DECLARE_POOL_ALLOC()
public:
    long L;
    string str;
public:
    Foo(long l):L(l){}
};
IMPLEMENT_POOL_ALLOC(Foo)

上面所有的例子中利用一条链表来维护内存空间,将其发展为 16 条链表,即下图的形式,并且不再以 class 内的 static 成员出现,而是一个全局的 allocator,这就是 std::alloc 的雏形。

image-20220331022752091