C++内存分配之new与delete

C++中在堆内存中创建和销毁对象需要借助关键字new和delete来完成,new和delete既是C++中的关键字也是一种特殊的运算符。

内存分配

C++中有多种内存分配、释放方式:

分配释放类属可否重载
malloc()free()C函数不可
newdeleteC++表达式不可
::operator new()::operator delete()C++函数
allocator::allocate()allocator::deallocate()C++标准库可自由设计并与容器搭配

C++内存分配调用层次

GUN下示例:

void* p1=malloc(512);
free(p1);

Complex<int>* p2=new Complex<int>; // one object
delete p2;

void *p3=::operator new(512);  // 512 byte
::operator delete(p3);

// gun2.9,为静态函数
// void* p4=alloc::allocate(512);
// alloc::deallocate(p4,512);

// gun4.9;分配9个int
void* p4=allocactor<int>().allocate(9);
allocactor<int>().deallocate((int*)p4,9);

// 相当于2.9的alloc::allocate;但是都要通过对象调用
void* p5=__gnu_cxx::__pool_alloc<int>().allocate(9);
__gnu_cxx::__pool_alloc<int>().deallocate((int*)p5, 9);

分配途径

C++分配一个对象(new)时,会有一个固定的调用途径,其中有些函数是可以重载的,从而修改内存分配与释放方式:
C++内存分配途径

new可以有多个重载版本:

  • 只有size_t一个参数的,为标准class operator new:重载后,默认new对象时会最终调用此函数来分配内存;
  • 两个参数(size_t + void*)的,为标准placement new:重载后,可在已有内存上构建对象;
class Bar{
public:
    Bar(){
        cout<<"Bar::Bar()"<<endl;
    }
    Bar(int){
        cout<<"Bar::Bar(int)"<<endl;
        throw "Error";
    }
    ~Bar(){
        cout<<"Bar::~Bar()"<<endl;
    }

    // 一般operator new重载
    void* operator new(std::size_t size){
        cout<<"operator new(std::size_t)"<<endl;
        return malloc(size);
    }
    void operator delete(void*, std::size_t){
        cout<<"operator delete(void*, std::size_t)"<<endl;
    }

    // 标准placement new重载
    void* operator new(std::size_t size, void* start){
        cout<<"placement new(std::size_t, start)"<<endl;
        return start;
    }
    void operator delete(void*, void*){
        cout<<"operator delete(void*, void*)"<<endl;
    }

    // 新的(自定义)placement new重载
    void* operator new(std::size_t size, long extra){
        cout<<"placement new(std::size_t, extra)"<<endl;
        return malloc(size+extra);
    }
    void operator delete(void*, long){
        cout<<"operator delete(void*, long)"<<endl;
    }

    void* operator new(std::size_t size, long extra, char init){
        cout<<"placement new(std::size_t, extra, init)"<<endl;
        return malloc(size+extra);
    }
    void operator delete(void*, long, char){
        cout<<"operator delete(void*, long, char)"<<endl;
    }
};

void testBarAlloc() {
    Bar *p1 = new Bar();
//    Bar *p2 = new Bar[3]; // 因没重载operator new[],会用系统默认的
    char *buf = new char[sizeof(Bar)];
    Bar *p3 = new(buf)Bar();
    Bar *p4 = new(2)Bar();
    Bar *p5 = new(2, 'a')Bar();
//    Bar *p6 = new(buf)Bar(1); // 会直接terminate
    cout << "p1=" << p1 << ", p3=" << p3 << ", p4=" << p4 << ", p5=" << p5 << endl;

    // 所有delete都会调用operator delete;不会去调用placement delete
    delete p5;
    delete p4;
    delete p3;
    delete p1;
}

// operator new(std::size_t)
// Bar::Bar()
// placement new(std::size_t, start)
// Bar::Bar()
// placement new(std::size_t, extra)
// Bar::Bar()
// placement new(std::size_t, extra, init)
// Bar::Bar()
// p1=0x7fffba5cc8c0, p3=0x7fffba5cc8e0, p4=0x7fffba5cc900, p5=0x7fffba5cc920
// 
// Bar::~Bar()
// operator delete(void*, std::size_t)
// Bar::~Bar()
// operator delete(void*, std::size_t)
// Bar::~Bar()
// operator delete(void*, std::size_t)
// Bar::~Bar()
// operator delete(void*, std::size_t)

array new

数组需要通过new[]与delete[](会调用array new与delete)来分配与释放内存;

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

private:
    int id;
};

void testArrayFooAlloc(){
    Foo* buf = new Foo[3]; // 3-time default ctor

    Foo* pTmp = buf;
    cout<<"buf="<<buf<<", tmp="<<pTmp<<endl;
    for(int i=0; i<3; ++i){
        new(pTmp++)Foo(i+1); // 通过placement new调用构造函数
    }
    cout<<"buf="<<buf<<", tmp="<<pTmp<<endl;

    delete[] buf; // 3-time dtor,且逆序;若是delete,只会调用一次dtor
}

// default ctor.this=0x7fffd91758c8, id=0
// default ctor.this=0x7fffd91758cc, id=0
// default ctor.this=0x7fffd91758d0, id=0
// buf=0x7fffd91758c8, tmp=0x7fffd91758c8
// 
// ctor.this=0x7fffd91758c8, id=1
// ctor.this=0x7fffd91758cc, id=2
// ctor.this=0x7fffd91758d0, id=3
// buf=0x7fffd91758c8, tmp=0x7fffd91758d4
// 
// dtor.this=0x7fffd91758d0, id=3
// dtor.this=0x7fffd91758cc, id=2
// dtor.this=0x7fffd91758c8, id=1

通过new[]分配的内存,会在对象前面添加一个对象数量的信息(其头包括:cookie+[debug-header]+object-count),以下以包含三个int数据的Demo类为例:

  • 返回给客户的是指向数据开始处(00481c34);
  • 客户使用delete[]删除时,会调整到(00481c30),从而获取数组数量,来调用对应数量的析构函数。

array_new_内存分配结构
Cookie中存放的是内存块的大小(最低位用于表示是否已分配:1标识已分配,0标识已回收),以上0x60的组成为:

  • 32+4:为debug头信息(细节参见3.2‘debug头’部分);
  • 4:为debug信息的下gap;
  • 36:所申请内存大小(sizeof(Demo)*3);
  • 4*2:上下Cookie大小;
  • 填充到16倍数,再增加12个填充(84->96);

operator new

全局的与类中的operator new都可重载;类中的operator new无论是否添加static都会作为静态成员。若类中没有实现operator new,则会调用全局的new(也可通过::new Foo::delete来强制调用全局的void* ::operator new(size_t)void ::operator delete(void*)

构造函数不可直接被调用(由编译器调用;或通过placement new调用)、而析构函数可以直接调用。

class Foo{
public:
    void* operator new(size_t);
    void operator delete(void*, size_t);
};

Foo *p = new Foo;
// ...
delete p;

编译器会编译为以下形式:

// new
try{
    void *mem=operator new sizeof(Foo);
    p = static_cast<Foo*>(mem);
    p->Foo::Foo();
}

// delete
p->~Foo();
operator delete(p);

placement new

带参数的operator new即为placement new;placement new允许将object建构在已分配的内存中。

可以重载class member operator new(),并写出多个版本,只需要参数不同即可:

  • 第一个参数必须为size_t:为内存大小;
  • 第二个为指针时,为标准placement new:除第一个参数外,其他参数作为new(…)参数传递;

也可以重载class member operator delete(),并写出多个版本,但默认他们绝对不会被delete调用。只有当new调用ctor抛出异常时,才会调用这些重载版本。

char* buf = new char[sizeof(Complex)];
Complex* pc = new(buf)Complex(1,2); // 对第一个元素调用构造函数
// ...
delete[] buf;

上面的第二行代码(placement new)会转换为以下类似代码(编译器):

Complex* pc;
try{
    void* mem=operator new(sizeof(Complex));
    pc=static_cast<Complex*>(mem);
    pc->Complex::Complex(1,2);
}
catch(std::bad_alloc){
    // allocation失败,不知晓Constructor
}

new handler

当operator new无法分配出所需内存时,会抛出std::bad_alloc异常;抛出异常前会先调用(不止一次)一个handler:

typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();

设计良好的new handler只有两个选择:

  • 释放内存,让内存分配成功;
  • 调用abort()或exit();

若要让new在申请内存失败时不抛出异常,而是返回0,需要使用nothrow版本:

new (nothrow) Foo

allocator

gun2.9中自己实现了allocator;但到gun4.9后,默认的allocator只是对new的简单调用。若要使用类似2.9中的allocator(批量申请内存,再切分给每个元素使用),需要使用__gnu_cxx::__pool_alloc

vector<int, __gnu_cxx::__pool_alloc<int>> vecPool;

gun C++中内置的一些内存分配的分配器:

  • __gnu_cxx::new_allocator:直接调用operator new/delete;
  • __gnu_cxx::malloc_allocator直接调用malloc/free;
  • __gnu_cxx::bitmap_allocator:使用bit-map追踪已用与未用的内存块;
  • __gnu_cxx::pool_allocator:缓存池方式;
  • __gnu_cxx::__mt_alloc:多线程版;
  • __gnu_cxx::debug_allocator:是一个wrapper;
  • __gnu_cxx::array_allocator:分配已知且固定大小的内存块(std::arary);

pool_allocator

gun2.9中,对于大小为(8*1 ~ 8*16)范围内的内存,会以pool方式管理;需要内存时会分配一大片,然后以链表形式串联在一起;后续需要时直接从free_list中获取;当对象释放时,会返回给free_list(但不会归还给操作系统)。
gun2.9_allocator

allocate流程(设n为申请内存大小,MAX为链表允许最大内存(128)):

  • 若n>MAX,则直接向系统申请内存(即最终调用malloc);
  • 查找n在链表中对应的索引,查看是否有可用空节点;
  • 若为节点分配内存时失败(很少发生),则尝试查找右侧(比当前节点大的节点)是否有空节点;若有则借用(分割后挂载到当前节点下)。

allocate流程

向系统申请内存:每次分配内存时,除所需内存外,还会多分配一部分(size*20*2+ROUND_UP(total>>4),total为已分配内存的累积量),预留后续使用;

  • 当前大小整数倍的区块(一般是size*20):一块分配出去(返回给客户),其余用链表连接在一起;
  • 预留区块:(即start_free与end_free指向的那块)
 // __ALIGN=8
size_t ROUND_UP(size_t bytes){ // 对齐内存(必须为8的倍数)
    return (((bytes) + __ALIGN-1) & ~(__ALIGN-1));
}

size_t FREELIST_INDEX(size_t bytes){ // 获取在链表中的索引
    return (((bytes) + __ALIGN-1)/__ALIGN - 1);
}

bitmap_allocator

bitmap保存固定大小的内存块,可用块通过bitmap来表示。最开始时分配的super block包括64个可用块,当使用完后会分配新的super block(其包含的子块数会翻倍)。
gun_bitmap_allocator

super block为分配块,包括:

  • 块的总大小;
  • 可用块的数量;
  • bitmap:标识可用块;
  • 可分配的内存块;

当某个super block使用完时(所有子块都回收后),会回收到临时记录区(以大小排序);当超过64个可回收super block时,会把最大的super block归还给OS。
gun_bitmap_allocator-(dealloc)

malloc内存布局

现在操作系统中malloc分配内存并不慢(已做了各种优化),但是会增加Cookie以及各种header,因此会浪费内存(额外的头以及为对齐而填充的数据)。

以下以VC6为了说明malloc的内存布局。

page结构

malloc在申请内存时,会增加cookie以便后续free以及校验使用:

  • Cookie为对应分配内存的大小(包括debug头等信息):分配时最低位设为1;
  • 释放时Cookie最低位改回0:然后就可以根据Cookie判断上下相邻块是否可合并。

vc-malloc-page

每次从操作系统中申请1M,分为32个组(32K大小) ,每个组分为八个page(4K),即上图左侧部分(其可用内存为4K-4*2-8=4080);所有小于1016(1k-2*4)的内存申请都从page中申请(会对齐到16的倍数)。

debug头

为方便调试,在debug模式下会增加debug头信息:

  • 用于以链表方式连接已分配内存的指针(Next与Prev);
  • szFileName与nLine指示申请内存(调用malloc)代码的位置;
  • lRequest:分配标识(如上面的为第一块分配的内存,则为1);
  • gap:为保护区(已分配设定为0xFD,已回收设定为0xDD,已清理的设定为0xCD);

VC6-malloc-debug头

VC程序调用栈

以下是VC6程序启动时调用栈,在新版(VC10及以后)中__sbh**函数都 已不存在了(直接调用系统的内存分配)。
VC6-程序启动调用层次

sbh:Small block heap(小内存块堆),用于优化管理小内存分配;在新版中已不需要了(操作系统已做了对应优化)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值