文章目录
C++中在堆内存中创建和销毁对象需要借助关键字new和delete来完成,new和delete既是C++中的关键字也是一种特殊的运算符。
内存分配
C++中有多种内存分配、释放方式:
分配 | 释放 | 类属 | 可否重载 |
---|---|---|---|
malloc() | free() | C函数 | 不可 |
new | delete | C++表达式 | 不可 |
::operator new() | ::operator delete() | C++函数 | 可 |
allocator::allocate() | allocator::deallocate() | 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)时,会有一个固定的调用途径,其中有些函数是可以重载的,从而修改内存分配与释放方式:
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),从而获取数组数量,来调用对应数量的析构函数。
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(但不会归还给操作系统)。
allocate流程(设n为申请内存大小,MAX为链表允许最大内存(128)):
- 若n>MAX,则直接向系统申请内存(即最终调用malloc);
- 查找n在链表中对应的索引,查看是否有可用空节点;
- 若为节点分配内存时失败(很少发生),则尝试查找右侧(比当前节点大的节点)是否有空节点;若有则借用(分割后挂载到当前节点下)。
向系统申请内存:每次分配内存时,除所需内存外,还会多分配一部分(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(其包含的子块数会翻倍)。
super block为分配块,包括:
- 块的总大小;
- 可用块的数量;
- bitmap:标识可用块;
- 可分配的内存块;
当某个super block使用完时(所有子块都回收后),会回收到临时记录区(以大小排序);当超过64个可回收super block时,会把最大的super block归还给OS。
malloc内存布局
现在操作系统中malloc分配内存并不慢(已做了各种优化),但是会增加Cookie以及各种header,因此会浪费内存(额外的头以及为对齐而填充的数据)。
以下以VC6为了说明malloc的内存布局。
page结构
malloc在申请内存时,会增加cookie以便后续free以及校验使用:
- Cookie为对应分配内存的大小(包括debug头等信息):分配时最低位设为1;
- 释放时Cookie最低位改回0:然后就可以根据Cookie判断上下相邻块是否可合并。
每次从操作系统中申请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);
VC程序调用栈
以下是VC6程序启动时调用栈,在新版(VC10及以后)中__sbh**
函数都 已不存在了(直接调用系统的内存分配)。
sbh:Small block heap(小内存块堆),用于优化管理小内存分配;在新版中已不需要了(操作系统已做了对应优化)。