C++的内存管理
C++的内存管理
-
数据类型 32位 64位 取值范围(32位) char 1 1 -128~127 unsigned char(当byte使用) 1 1 0~255 short int /short 2 2 –32,768~32,767 unsigned short 2 2 0~65,535 int 4 4 -2,147,483,648~2,147,483,647 unsigned int 4 4 0~4,294,967,295 long int /long 4 8 –2,147,483,648~2,147,483,647 unsigned long 4 8 0~4,294,967,295 long long int/long long 8 8 -9,223,372,036,854,775,808~9,223,372,036,854,775,807 指针 4 8 float 4 4 3.4E +/- 38 (7 digits) double 8 8 1.7E +/- 308 (15 digits) - 注意:
- 求数组大小时,如果数组作为参数传递时,退化为指针,所以sizeof(arr)大小为该系统下指针的大小
- 对于C字符串,需要牢记C/C++中一个汉字占两个字节(Linux下3个字节)。
- 求struct 或者 class 的大小时候,除了基本的数据类型大小,特别要考虑的是字节对齐问题,如果是C++的还涉及虚函数的虚表问题,需要加上虚表指针的大小。
- 注意:
-
-
分配 释放 类型 可否重载 malloc
free
C函数 不可 new
delete
C++表达式 不可 ::operator new
::operator delete
C++函数 可 allocator<T>::allocate
allocator<int>::deallocate
C++标准库 可自由设计并以之搭配任何容器 上述函数用法:
void primitives_sample() { //CRT运行时函数 void *p1 = malloc(512); free(p1); //new complex<int>* p2 = new complex<int>; delete p2; //::operator new(全局函数,可被重载) void* p3 = ::operator new(sizeof(int)); //内部调用malloc ::operator delete(p3); //内部调用delete // allocator分配器(STL容器分配内存的方式) 7代表要7个ints而不是7个直接 void* p4 = allocator<int>().allocate(7); allocator<int>().deallocate((int*)p4, 7);//还的时候当初要的数量也得还回去 return; }
-
malloc/free分配细节:
- malloc:除了分配给定的内存之外,还会有cookie——用来记录分配数组的长度
- free:查看cookie中的长度,然后调用适当的析构函数
-
new/delete探究
-
new expression
- 分配内存(调用operator new)
- 调用构造函数
-
delete expression
- 调用析构函数
- 释放内存(调用operator delete)
-
operator new
-
调用malloc
-
operator delete
- 调用free
-
new/delete编译器自动生成的代码实现:
//调用new的时候,编译器生成的代码 primitives_new_delete* new_override() { primitives_new_delete* pc; try { void *mem = operator new(sizeof(primitives_new_delete)); //分配内存 pc = static_cast<primitives_new_delete*>(mem); //指针转换 //pc->primitives_new_delete::primitives_new_delete(1, 2); //实际上是这样,但是编译器不支持。指针不可直接调用构造函数 /* 欲直接调用ctor,则使用placement new: new(p) primitives_new_delete(1, 2); */ new(pc) primitives_new_delete(1, 2); //直接调用构造函数(placement new) return pc; }catch(std::bad_alloc) { cout << "Bad Allocation" << endl; } return nullptr; } //调用delete的时候,编译器生成的代码 void delete_override(primitives_new_delete* pc) { pc->~primitives_new_delete(); //指针可直接调用析构函数 operator delete(pc); //释放内存 }
-
Ctor & Dtor 直接调用
//工具人class class A { public: int id; A(int _id) : id(_id){ cout << "ctor.this = " << this << " id = " << endl; } ~A() { cout << "dtor.this = " << this << " id = " << endl; } }; static void TEST_Ctor_Dtor() { string* pstr = new string; cout << "str = " << *pstr << endl;//先打印一下证明真的拿到了 /* 不能直接调用构造函数 原因: typedef basic_string string //即string的真正类型是basic_string */ //pstr->string::string("jjhou"); //可以直接调用析构 pstr->~string(); A* pA = new A(1); cout << " id = " << pA->id << endl; //pA->A::A(3); //直接调用构造函数 //VC6老版本成功了,GC和新版本都GG,所以无法直接调用构造函数,要真的想直接调就用placement_new }
-
-
数组new[]/delete[]探究
-
new object[]/delete[] object(本质调用malloc/delete)
-
如果调用了new[]而释放的时候调用的是delete而不是delete[]就会发生内存泄漏,但是不是我们以为的new[]了N个内存,释放只释放了第一个,本质上new出来的对象指向的并不是内存地址而是这一组内存对应的cookie,本质上cookie记录了free本身需要释放的内存大小,所以通过delete也会根据cookie将对应的资源还给操作系统,并不会发生内存泄漏。而真正的泄露发生在调用的析构函数次数上,如果一个成员变量不带指针,没有特定的复写析构函数,那么这个析构是不重要的,调用不调用没区别则不会发生内存泄漏,如果object拥有带指针的成员变量,则意味着析构函数是重要的,需要在析构函数中将指针所引用的内存释放,而delete则会影响析构函数的调用次数,只会调用一次析构,即第一个对象的析构或者最后一个对象的析构(具体是哪个看编译器实现,各家不一样),而其他对象的析构就不会被调用,所以new[]不对应delete[]的话会影响指针指向的内存部分的泄露。
-
且上述还有一个细节,见下述代码
int* pi = new int[10];//sizeof(pi) = 4 delete pi;//能被释放,且因为没有指针对象,这块的pi指向的是第一个元素内存的地址 class Complex{ public: int id; Complex(int _id) : id(_id) { cout << "default ctor is. " << this << " id = " << id << endl; } Complex() : id(0) { } ~Complex() { cout << "dtor is. " << this << " id = " << id << endl; } }; static void TEST_ARRAY_NEW_DELETE(int size) { Complex* buf = new Complex[size]; //必须有默认构造函数 Complex* tmp = buf; cout << "buf = " << buf << " tmp = " << tmp << endl; for(int i = 0 ; i < size; i ++) { new (tmp++)Complex(i); //replacemennt new } cout << "buf = " << buf << "tmp = " << tmp << endl; /* delete pi; 当new[]的对象的析构被重新定义是有意义的时候,他在分配内存时还会在数据头加上一个值表示new出来的长度,这里就是10,pi指向的也是这个10,这时候delete就会编译报错 dtor is. 0x1304e78 id = 0(只释放的第一个) munmap_chunk(): invalid pointer 无效的指针 (就代表指向的是那个new出来的长度,这里我又做了个测试,吧析构注释了,在运行就并没有报错,佐证了上述解释) */ delete []buf; //从最后一个往上进行释放 } int main(void) { TEST_ARRAY_NEW_DELETE(3); return 0; } /* buf = 0x1835e78 tmp = 0x1835e78 default ctor is. 0x1835e78(8609400) id = 0 default ctor is. 0x1835e7c(8609404) id = 1 default ctor is. 0x1835e80(8609408) id = 2 buf = 0x1835e78tmp = 0x1835e84 dtor is. 0x1835e80 id = 2 dtor is. 0x1835e7c id = 1 dtor is. 0x1835e78 id = 0 */
-
Replacement new
-
允许我们将对象建构在已经分配好的内存上
-
没有所谓的placement delete,因为placement new根本不分配内存!
-
不会在分配内存(定点内存)
//调用placement new 的编译器生成的代码 void *mem = operator new(sizeof(Complex), buf); //重载 pc = static_cast<Complex*>(mem); //pc->Complex::Complex(1,2); //构造函数
-
具体用法上面代码块里我用了,可以看看
-
-
重载全局的::operator
- 影响无限大
-
operator_new重载
-
重载operator new(本质上就是placement new)
-
写法:
/* 重载类内operator new/delete */ class Foo{ public: int _id; long _data; string _str; public: Foo() : _id(0) { cout << "default ctor. this = " << this << "id = " << _id << endl; } Foo(int i) : _id(i) { cout << "ctor.this = " << this << "id = " << _id << endl; } ~Foo() { cout << "dtor.this = " << this << " id = " << _id << endl; } //这些函数必须是静态函数, 因为这是编译器在创建对象时调用的,这时候对象都没有,不可能调用成员函数,所以必须是静态的,也正是因为必须是静态的,如果这些方法没加静态编译器也会自动加上,所以写的时候有些地方没加static,但是最后编译生成的肯定是静态的方法 static void *operator new(size_t size){ Foo *p = (Foo*) malloc(size); cout << "Foo new()" << endl; return p; } static void operator delete(void* pdead, size_t) { cout << "Foo delete()" << endl; free(pdead); } static void* operator new[](size_t size){ Foo* p = (Foo*)malloc(size); cout << "Foo new()" << endl; return p; } void operator delete[] (void * pdead, size_t) { cout << "Foo delete()" << endl; free(pdead); } };
-
-
重载new()/delete()
- 可以重载class member operator new(), 写出很多版本,前提是每天一版本的额声明, 都必须具有独特的参数列,其中第一参数必须是size_t, 其余参数以new指定的placement arguments为初值。出现于new()小括号内的便是所谓的placement arguments
- 可以重载class member operator delete(), 但它们绝不会被delete调用。只有当new所谓的ctor抛出exception,才会调用这些重载版本的operator delete()。经验证:Clang9.0.0并不会调用operator delete
-
operator new和placement new的区别
-
两个都是重载operator new()这个函数签名
-
但是operator new的参数是operator new(size_t size)
-
而placement new的参数是operator new(size_t size, void* ptr);
-
new(p)就是placement new,被称为定点new, 而功能是并不分配内存
class Foo{ public: Foo() { cout << "Foo:::Foo()" << endl; } Foo(int ) { cout << "Foo::Foo(int)" << endl; throw Bad(); } //一般的operator new()的重载 void* operator new(size_t size) { cout << "operator new(size_t)" << endl; return malloc(size); } //标准库已提供的placement new()的重载 //当ctor发出异常,下面对应的operator delete就会被调用(其作用就是释放相应的内存) void* operator new(size_t size, void * start) { cout << "operator nen(size_t, void*)" << endl; return start; } void* operator new(size_t size, long extra) { cout << "operator new(size_t, long)" << endl; return malloc(size + extra); } void* operator new(size_t size, long extra, char init) { cout << "operator new(size_t, long, char)" << endl; return malloc(size + extra); } //相应的delete重载函数 void operator delete(void*, size_t size) { cout << "operator delete(void*, size_t)" << endl; } void operator delete(void*, void*) { cout << "operator delete(void*, void*)" << endl; } void operator delete(void*, long) { cout << "operator delete(void*, long)" << endl; } void operator delete(void*, long, char) { cout << "operator delete(void*, long, char)" << endl; } private: int m_i; };
-
-
-
per_class_allocator
-
为什么要为类写内存分配器?
- 提高分配速度
- 降低空间浪费率(cookie)
-
allocator1.0:
#include <iostream> #include <algorithm> #include <cstring> using namespace std; /* 通过Screen和Screen_new两个类来测试重载operator new对cookie的影响 结论: Clang两个版本都不带cookie,大小和间隔都是16 GCC 分配区别明显 注意: 在写程序的时候,一定要注意virtual机制带来的内存浪费 优点: 内存池,减少malloc调用次数 缺点: 引入next指针,没有调用free,如果对象是一个int,加上next指针导致内存膨胀率变成了百分百。且不释放资源就会导致这块内存一直被占用 */ class Screen{ public: Screen(long x) : i(x){} ~Screen() {} int get() {return i;} private: Screen* next;//32 4 ,64 8 static Screen* freeStore; static const int screenChunk; private: long i; //8 }; class Screen_override { public: Screen_override (int x) : i(x) {} ~Screen_override () {} int get() {return i;} //重载 static void* operator new(size_t); static void operator delete(void*, size_t); private: Screen_override* next;//32 4字节 64 8字节 static Screen_override* freeStore; static const int screenChunk; private: int i; //4字节 //64位电脑上 4+8加上内存对其到16位 所以size是16 }; Screen_override* Screen_override::freeStore = 0; //最开始的头结点 const int Screen_override::screenChunk = 5; //这里写了5 所以5个以后不够就要继续去挖 /* @operator new 逻辑是当freeStore空的时候分配5个 当freeStore不空的时候,从Chunk中取next */ void *Screen_override::operator new(size_t size) { //size为元素大小,一次分配5个 Screen_override* p; if(!freeStore) { //linked list 是空的,申请一大块 size_t chunk = screenChunk* size; freeStore = p = reinterpret_cast<Screen_override*>(new char[chunk]); //将这一块分割成片,当做linked list串接起来 for(; p != &freeStore[screenChunk - 1]; ++p) { p->next = p + 1; } p->next = 0; } p = freeStore; freeStore = freeStore->next; return p; } /* 将deleted object 插回到free list前端 */ void Screen_override::operator delete(void* p, size_t) { (static_cast<Screen_override*>(p))->next = freeStore; freeStore = static_cast<Screen_override*>(p); } //测试不重载operator new,分配内存带cookie void TEST_WITH_COOKIE(){ cout << endl << "TEST_WITH_COOKIE" << endl; //32位系统下,16字节,4(Alignment) +8(Screen*) +4(Alignment) ; static 成员变量并不算在对象中 //64位系统下,32字节,8(Alignment) +16(Screen*) +8(Alignment) ; static 成员变量并不算在对象中 cout << "size of Screen: " << sizeof(Screen) << endl; int const N = 100; Screen* p[N]; for(int i = 0; i < N; i ++) { p[i] = new Screen(i); } for(int i =0 ; i < 10; i ++) { cout << p[i] << endl; } for(int i = 0;i < N; i ++){ delete p[i]; } return; } //重载operator new, 分配内存不带cookie void TEST_NO_COOKIE(void) { cout << endl << "TEST_NO_COOKIE...." << endl; //16字节 cout << "size of Screen_override: " << sizeof(Screen_override) << endl; int const N = 100; Screen_override* p[N]; for(int i =0 ; i < N; i ++) { p[i] = new Screen_override(i); } for(int i =0 ; i < 10; i ++) { cout << p[i] << endl; } for(int i =0 ; i < N; i ++) { delete p[i]; } return; } int main(void) { TEST_WITH_COOKIE(); TEST_NO_COOKIE(); return 0; } /* TEST_WITH_COOKIE size of Screen: 16 0x89fe80 9043584 0x89fea0 9043616 0x89fec0 9043648 0x89fee0 9043680 0x89ff00 9043712 0x89ff20 9043744 0x89ff40 9043776 0x89ff60 9043808 0x89ff80 9043840 0x89ffa0 9043872 间隔 32 64位系统 一个cookie是8,头尾是16 TEST_NO_COOKIE.... size of Screen_override: 16 0x8a0b00 9046784 0x8a0b10 9046800 0x8a0b20 9046816 0x8a0b30 9046832 0x8a0b40 9046848 //这里间隔32 代表新new了一个复合对象 头尾cookie+16 0x8a0b60 9046880 0x8a0b70 9046896 0x8a0b80 9046912 0x8a0b90 9046928 0x8a0ba0 9046944 */
-
-
allocator2.0 ( 类的内存分配器精简版本)
-
优点: next指针借用了分配的空间大小来当指针,当这个内存没被分配的时候,这个指针就用来关联内存池中各个内存块,当这个内存被分配出去被外部使用时,外部将自己的具体值写入,覆盖了这个临时指针有关系吗?没有,因为这东西已经借出去了,所以也就不在池中了,没必要维护和内存池的关系了,随便外部干嘛,等到还回来的时候,诶嘿,我再拿前面的几个字节来当指针用,完美。所以这么设计非常巧妙的剩下了指针的空间,利用了类原本的空间大小,没有膨胀率。但是有个要求,当对象大小要大于借用的指针大小(32位4,64位8),如果没有这么大那么就无法用这个策略被内存池管理。
-
缺点: 内存池最终delete后还是和1.0一样并没有还给OS ,并没有调用free(不是内存泄漏,就是自己缓存了这么一大块内存)
class Airplane { public: Airplane (){} ~Airplane (){} private: //声明 struct AirplaneRep{ unsigned long miles; //8字节 char type; //1字节 //总大小9 - > 16 对齐到16 }; private: //定义:union的巧妙之处,在于未正式分配的是时候借用其中的8个字节作为指针,这是主流的分配器实现必须要做的,不然就很低级。 union { //16字节 Embedded Pointer AirplaneRep rep; //这个就是对应具体的对象 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* deadObj, 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){ headOfFreeList = p->next; //如果P有效就把头移动到下个元素,p返回去(从池子里拿) }else{ Airplane* newBlock = static_cast<Airplane*>(::operator new(BLOCK_SIZE * sizeof(Airplane))); //申请一大块 //切成一片片,和上面的区别是这个指针是借用的 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* deadObj, size_t size) { if(!deadObj) return; if(size != sizeof(Airplane)) { //继承情况的时候不加校验这里会出问题 ::operator delete(deadObj); return; } //转回指针,回池 Airplane* carcass = static_cast<Airplane*>(deadObj); carcass->next = headOfFreeList; headOfFreeList = carcass; } int main(void) { //cout << sizeof(unsigned long) << endl; 8Bytes //cout << sizeof(Airplane*) << endl; 8Bytes cout << sizeof(Airplane) << endl; //16字节 size_t const N = 100; Airplane* p[N]; for(size_t i = 0; i < N; i ++) { p[i] = new Airplane; } p[1]->set(1000, 'A'); p[2]->set(2000, 'B'); p[3]->set(3000, 'C'); for(int i = 0; i < 10; i ++) { cout << p[i] << endl; } for(size_t i = 0; i < N; i ++) { delete p[i]; } return 0; }
-
-
static allocator 3.0+4.0对象内存池
-
这个原理和2.0一样,只是在使用上不必为不同的class重写一遍相同的member operator new/delete , 解决办法就是将allocator抽象成一个类 ,然后为了省事就用macro包起来就方便写了 MFC就这么偷懒
//DECLAREE_POOL_ALLOC-- used in class definition #define DECLARE_POOL_ALLOC() \ public: \ void* operator new(size_t size) {return myAlloc.allocate(size);}\ void operator delete(void* p) {return myAlloc.deallocate(p, 0);} \ public:\ static my_allocator myAlloc; //IMPLEMENT_POOL_ALLOC -- used in class implementation file #define IMPLEMENT_POOL_ALLOC(class_name) \ my_allocator class_name::myAlloc class my_allocator{ public: struct obj{ struct obj* next; }; public: my_allocator() {} ~my_allocator() {} public: void* allocate(size_t); void deallocate(void* p, size_t); //private: obj* freeStore = nullptr; int const CHUNK = 5; }; void* my_allocator::allocate(size_t size) { cout << "allocating..." << endl; obj* p; if(!freeStore) { freeStore = p = static_cast<obj*>(malloc(size * CHUNK)); for(int i = 1; i < CHUNK - 1; i ++) { p->next = static_cast<obj*>(p + size); p = p->next; } p->next = nullptr; } p = freeStore; freeStore = freeStore->next; return p; } void my_allocator::deallocate(void* p, size_t) { static_cast<obj*>(p)->next= freeStore; freeStore = static_cast<obj*>(p); } class Foo{ DECLARE_POOL_ALLOC(); public: long L; string str; public: Foo(long _L) : L(_L) {} }; IMPLEMENT_POOL_ALLOC(Foo); class Goo{ DECLARE_POOL_ALLOC(); public: complex<double> c; std::string str; public: Goo(const complex<double>& l) : c(l) {} }; IMPLEMENT_POOL_ALLOC(Goo); int main(void) { Foo* p[10]; cout << "sizeof(Foo)" << sizeof(Foo) << endl; for(int i =0 ; i < 5; i ++) { p[i] = new Foo(static_cast<long>(i)); cout << p[i] << endl; } for(int i = 0; i < 5; i ++) { delete p[i]; } return 0; }
-
-
-
- new_handler
- 当operator new没有能力分配出锁申请的memory,会抛出一个std::bad_alloc的exception或者返回0,编译器会在抛出异常之前,会不断调用一个可由客户端指定的handler
- 设计良好的new handler只有两个选择,1、让更多内存可用;2、调用abort()或exit()
- =default, =delete
- delete表示删除函数(不能生成函数实例)
- default函数的默认版本(拷贝构造,拷贝赋值,析构函数, 说是还支持这东西:operator new/new[], operator delete/delete[],试验后jb用没有)
- new_handler
-
- 基本对象
- 基本分析
- cookie头和cookie尾(32位是8位,64位16位)
- debug模式下会占用一个debug header 32 + 4大小的信息部分(灰色部分)
- 之后就是类中数据实际的数据空间大小
- 假设如果计算后的空间只有52 但VC以16字节对齐,所以52最近的16倍数是64,还应该填补12字节的空缺(青色pad部分)
- 总结
- debug下内存大小为(8 + 36 + 数据实际的大小+补充的对其空间大小)
- 对于release部分的complex对象,只添加了信息头和尾,实际空间大小为(8 + 数据实际的大小+补充的对其空间大小)
- 基本分析
- 数组对象
- 基本分析
- 其余部分和基本对象一致,假设数组有三个对象,编译器会在数组对象前插入“3”用于标记对象个数,占用一个int位,4字节
- 总结
- debug下内存大小为(8 + 36 + 4 + 数据实际的大小+补充的对其空间大小)
- 对于release部分的complex对象,只添加了信息头和尾,实际空间大小为(8 + 4 + 数据实际的大小+补充的对其空间大小)
- 基本分析
- 基本对象
-
-
主流平台的分配器实现
-
VC6.0和BorlandC5.0这两个著名的编译器标准库内部实现中,每个容器的
std::allocater
都是通过::operator new/delete
来完成的——本质就是直接调用malloc
其他什么都没有做。而GNU2.9 C++
中使用的版本是std::alloc
——使用了诸如pool等高级的分配逻辑,在经过更新迭代后在GNU4.9
中改名为__gnu_cxx::__pool_alloc
——也是本章的重点关注对象。 -
GNU2.9中的std::alloc内存分配流程:
-
size
容器所申请的对象大小;chunk
分配器提供的内存单元——共16个。 -
维护16条长度各异(第
n
条链表一次分配的chunk
大小为:8*(n + 1)Bytes
,)的自由链表,超过能分配最大的内存单元(8*16 = 128)会申请malloc
。所有使用malloc
的申请对象都会带cookie
。 -
分配器会将申请的内存
size
按照分配单元调整到合适的边界(比如std::alloc
是8的倍数,malloc
也会调整) -
拿到一个申请的
size
需要分配的对象,首先定位它在分配器上的指针端口。然后查看是否有战备池的内存可以用,如果有直接使用,这时候一般分配的数量是1~20个chunk
。没有则调用malloc
分配。第一次分配pool为零,则直接调用malloc
。 -
分配器调用
malloc
分配大小为size * 20 * 2 +ROUNDUP(memory_allocated >> 4);
其中会有20
个chunk
是为申请的对象准备的,剩下的内存(并不会分割)作为战备池为以后的分配器行为做准备——这是一个很重要的概念,分配全程需要关注战备区pool的容量。 其中ROUNDUP()
是对齐函数,memory_allocated
是已经分配好的内存总量。随着内存分配操作越来越多,memory_allocated也是越来越大——符合计算机内存分配越来越多的增长趋势,>>4就是除以16,为什么加这个额外申请空间,这应该是gnu里面开发的经验值,没有解释。 -
如果战备池剩下的内存小于要分配的
size
,那剩下的这一小块pool则被看成是碎片——计算应该放在那个链表后就会挂到相应的链表头指针上,作为多出来的备用item。 -
如果
malloc
系统内存分配失败,则需要在最接近的大于size
的已分配好的空闲chunk
中回填pool
,完成分配。如果向上走也无法分配成功,那系统分配才算完全失败。 -
由于一次性分配20个
chunk
,而每一次分配必须按照其size
来选择链表头结点,所以有很大概率某些指针上的空闲chunk
就会比较多。而std::alloc
对chunk
的选择一定是大于等于size
的。从技术的角度,将这些空闲的chunk
合并在一起难度非常大。 -
分配器面向的用户是容器——元素一样大的集合,如果用户直接使用分配器,就必须记住分配的大小。这是因为自主分配的内存不会带
cookie
,而容器的第一个目标参数都会是参数类型,只要sizeof
一下就可以计算出来所申请的size
。#include <iostream> #include <algorithm> #include <cstring> #include <list> using namespace std; /* 上述概念的精简版实现 重点理解: 备战池,内存对齐,内存碎片 */ /* 一级分配器 */ class alloc_pri{ public: static void* allocate(size_t size) { void* result = malloc(size); return result; } static void deallocate(void* p, size_t) { free(p); } }; typedef alloc_pri malloc_alloc; /* 二级分配器 */ class alloc_default{ private: //链表指标 static const int _align = 8; static const int _max = 128; static const int _list = _max / _align; //嵌入式指针 union obj{ union obj* next; }; private: //链表元素 static obj* free_list[_list]; static char* start_free; static char* end_free; static size_t heap_size; //工具函数 static size_t FREELIST_INDEX(size_t bytes); static size_t ROUND_UP(size_t bytes); //核心函数 static void* refill(size_t size); static char* chunk_alloc(size_t, int &); public: //分配函数 static void* allocate(size_t); static void deallocate(void*, size_t); static void realloc(void* p, size_t, size_t); }; /*类静态变量定义*/ char* alloc_default::start_free = 0; char* alloc_default::end_free = 0; size_t alloc_default::heap_size = 0; alloc_default::obj* alloc_default::free_list[_list] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; //16 chunks /************分配函数实现 和6per_class_allocator的分配算法完全一样 */ void* alloc_default::allocate(size_t size) { obj** my_free_list; //指向内存池链表的的指针 obj* result; //当需要申请的size大于allocator的最大chunk的时候, 直接调用一级分配器————>malloc() if(size > _max) return (malloc_alloc::allocate(size)); //根据申请的size判断内存池相应的指针端口 my_free_list = free_list + FREELIST_INDEX(size); result = *my_free_list; //如果链表为空 如果第一次或者List被用光了,则result为空 if(!result) { void* t = refill(ROUND_UP(size)); return t; } //如果有可用区块,分配 *my_free_list = result->next; return result; } void alloc_default::deallocate(void* p, size_t size) { obj* q = static_cast<obj*>(p); //当需要销毁的size大于deallocator的最大chunk的时候, 直接调用一级分配器————>free() if(size > _max) { malloc_alloc::deallocate(p, size); return; } obj** my_free_list = free_list + FREELIST_INDEX(size); q->next = *my_free_list; *my_free_list = q; } /**************工具函数实现 纯粹数学计算/理解向上取整 */ size_t alloc_default::FREELIST_INDEX(size_t bytes) { return ((bytes + _align - 1) / _align - 1); } size_t alloc_default::ROUND_UP(size_t bytes) { return (bytes + _align) & ~(_align - 1); } /********************核心函数 @功能: 充值 */ void* alloc_default::refill(size_t size) { int nobjs = 20; char* chunk = chunk_alloc(size, nobjs); // 不一定是20个 如果备战池为零,则重新分配20个chunk,而备战池有内存资源,则不一定分配20个 if(1 == nobjs) return(chunk); //如果分配了一个,则直接返回 //定位 obj** my_free_list = free_list + FREELIST_INDEX(size); obj* result = (obj*)chunk; //确定好了需要分配的一块 //将分配好的内存chunk链接到相应的指针端口,并初始化next_obj obj* current_obj, *next_obj; *my_free_list = next_obj = (obj*)(chunk + size); //利用嵌入式指针完成分配的内存的链表化 for(int i = 1; ; i ++) { current_obj = next_obj; next_obj = (obj*)((char*)next_obj + size); if(i == nobjs - 1) { current_obj->next = nullptr; break; }else { current_obj->next = next_obj; } } return (result); } /* @功能 按照申请的size,如果分配器的chunk都小于等于size;诉求第一级分配器即malloc 然后确定需要挂载的chunk区块,如果战备池有空闲的内存则直接分配,没有的话调用 refill()在周边区块上补充 而chunk_alloc()函数是备战池内存分配的核心逻辑 */ char* alloc_default::chunk_alloc(size_t size, int& nobjs) { size_t total_bytes = size * nobjs; size_t bytes_left = end_free - start_free; char* result; if(bytes_left >= total_bytes) { //pool空间满足申请内存 result = start_free; start_free += total_bytes; return (result); }else if(bytes_left >= size) { //pool空间满足一块以上 nobjs = bytes_left / size; //改变需求 total_bytes = size * nobjs; result = start_free; start_free += total_bytes; return (result); }else { //pool空间不满足(碎片) size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4); //处理碎片(将其挂到相应的chunk指针端口) if(bytes_to_get > 0) { obj** my_free_list = free_list + FREELIST_INDEX(bytes_left); ((obj*)start_free)->next = *my_free_list; *my_free_list = (obj*)start_free; } //开始从system-heap上分配内存 start_free = (char*)malloc(bytes_to_get); if(0 == start_free) { //如果当前chunk没有分配成功,则向上继续找chunk分配 obj** my_free_list, *p; for(int i = size; i <= _max; i += _align) { my_free_list = free_list + FREELIST_INDEX(i); p = *my_free_list; if(0 != p) { *my_free_list = p->next; start_free = (char*)p; end_free = start_free + i; return (chunk_alloc(size, nobjs)); } } end_free = 0; start_free = (char*)malloc_alloc::allocate(bytes_to_get); } heap_size += bytes_to_get; end_free = start_free + bytes_to_get; return (chunk_alloc(size, nobjs)); //备战池有内存了,所以递归重新处理分配逻辑 } } typedef alloc_default alloc; class Foo{ public: int m; }; int main(void) { //Foo(1)是stack memory, 而当需要push_back的时候,alloc需要准备一块内存 list<Foo, alloc> c; //不带cookie c.push_back(Foo(1)); //push_back 需要拷贝构造 //p是heap memory, 容器向分配器发出需求,alloc为其分配内存 Foo* p = new Foo(2); //带cookie c.push_back(*p); //push_back 需要拷贝构造 delete p; return 0; }
-
-
-
-
Windows
下VC6.0
中C++
程序的流程(简化版)——伪码中的API
都是Windows
平台下的。执行流程是前一个函数调用后一个函数。需要有OS-虚拟内存的基础。-
mainCRTStartup(); //由C Runtime Library
/*
系统启动最初由HeapAlloc分配4096字节并传给指针_crtheap,并建立16个Header。接着第一个Header的一个指针利用VirtualAlloc(0, 1Mb, MEM_RESERVE)函数向操作系统直接申请了1MB的虚拟地址空间(并不分配内存),另外一个指针从_crtheap中又分配了sizeof(region)大小的内存用来建立管理中心。
总结一下,最开始进行了两种操作,一个操作是真正的向OS要了4KB的物理内存_crtheap,用来建立region控制中心和16个Header——这里用的是API是HeapAlloc();而另一个操作是作为控制中心的region申请了1MB的虚拟地址空间(注意这里并没有直接分配内存)——用的API是VirtualAlloc()。这一切操作都是在ioinit函数真正申请内存之前就完成了。
*/ -
_heap_init()
- __sbh_heap_init() //再这里申请了16个HEADER用来服务于SBH
-
_ioinit()
第一次尝试申请内存共100h-
_heap_alloc_dbg()
调试模式下typedef struct _CrtMemBlockHeader{ //在debug模式下准备附加到所申请的内存之上的 所以在debug模式下分配的内存会比申请的内存大一些 struct _CrtMemBlockHeader* pBlockHeaderNext; struct _CrtMemBlockHeader* pBlockHeaderPrev; char* szFileName; //指向被调试文件的文件名 int nLine; //文件行 size_t nDataSize; //记录真正分配的大小 int nBlockUse; //记录这个内存块被干嘛用了 /* _FREE_BLOCK 0 _NOROMAL_BLOCK 1 用户申请的一般都是这个 _CRT_BLOCK 2 _IGNORE_BLOCK 3 _CLIENT_BLOCK 4 _MAX_BLOCKS 5 */ long IRequest;//id unsigned char gap[nNoMansLandSize]; //4个字符指针(填充无人区,后面还会补一个,用来检测有没有被写脏数据数据) }_CrtMemBlockHeader;
-
_heap_alloc_base();// 检查如果mallocl的内存小于1024采用SBH小区块分配器
-
_sbh_alloc_block();//计算好总共需要分配的内存字节,如果是debug模式就是36个debug的数据块+上下cookie,如果是正常模式则只有上下cookie(上下
cookie
是为了回收的时候上下合并): -
heap_alloc_new_region()
第一个
Header
的指针申请region
——由HeapAlloc(_crtheap, sizeof(region))
分配
内存分配原理: 16个
Header
(有一个头指针为其定位)。每一个Header
负责管理1MB
——申请虚拟地址空间调用Windows API——VirtualAlloc()
。每一个Header
有两根指针——其中一根指向其管理中心——region
。//region结构 typedef struct tagRegion{ int indGroupUse; //整数 char cntRegionSize[64]; //64个character //unsigned int 32位 高位和低位合并之后——>32组数据,每组64bits BITVEC bitvGroupHi[32]; BITVEC bitvGroupLo[32]; //32个group 每一个group管理32KB struct tagGroup grpHeadList[32]; }REGION,&PREGION; //group结构 typedef struct tagGroup{ int cntEntries; // 64对双向链表 struct tagListHead ListHead[64]; //类似alloc的分配方式,0号链表管理16字节,1号32字节...63管理1024字节且最后一条链表有个特殊设计,大于1k的全归他管 }GROUP, *PGROUP; //双向链表 typedef struct tagListHead{ struct tagEntry* pEntryNext; struct tagEntry* pEntryPrev; }LISTHEAD, *PLISTHEAD; typedef struct tagEntry{ int sizeFront; //记录4080Bytes struct tagEntry* pEntryNext; struct tagEntry* pEntryPrev; }ENTRY *PENTRY;
-
heap_alloc_new_group()
1MB
分为32个单元,每单元32KB
的大小。然后每一个单元又分为8个page
,每部分4KB
——对应操作系统的page
。而管理中心region
一共有32个group
,所以每一个group
管理8x4KB的内存资源。从上面的代码中可以知道:一个
group
共有64个双向指针,这些指针所管理的内存按照16的倍数递增(即1st—16字节,2nd—32字节...64th—>=1024字节)。 因此一个group
实际上可以管理的大小是16*(1 + 2 + ...+ 64) = 32KB + 512Bytes
。符合最开始的设定。根据
ioinit
申请的内存大小110h
,加上debug
模块和cookie
,再进行16字节的对齐。最后需要向每一个page
申请130h
字节的内存。最后还剩下ec0h = ff0h - 130h
。那一个page
便会被切割成为两部分——一部分是分配的130h
内存,这一部分需要将130h
改为131h
代表脱离了SBH系统的控制分离出去。 另一部分是剩下的ec0h
。双方的结构都是heap_alloc_dbg::struce _CrtMemBlockHeader
并且都需要更新cookie
。以后每一次分配都需要根据分配的size
计算所挂的链表——如果该链表上没有区块,则向上移动直到最后一条链表上再分配。至此,
malloc
函数的整个分配过程基本结束了。
-
-
-
-
free(p) //将p回收到相应的链表上
-
落在哪个
Header
上? -
落在哪个
Group
上? -
落在哪个
free-list
上?整个系统的主要结构就是:
Header/Region
——Group
——free-list
上述各个数据结构的大小都知道,所以很容易定位p
指针。Group`回收: `(p - 头指针)/32K
Header
回收: 使用头指针__sbh_pHeaderList
从第一个Header
开始索引判断。free-list
回收: 将区块size/16 - 1
然后判断回收需要挂在group
上的哪一个链表上面。合并:每一个区块都有上下的
cookie
,根据cookie
的最后一位是不是1
判断是否可以合并。Defering
:当出现两个Group
全回收(Counter
判断)的时候,则将Defer
指针指向第一个准备释放给OS,如果只有一个全回收的Group
则不释放。
-
-
-
-
Loki::allocator核心有三个类:
Chunk
,FixedAllocator
和SmallObjAllocator
。它们的类层次关系如下,其中SmallObjAllocator
是最上层的,直接供客户端使用的类。 -
Chunk
就是直接管理单一内存块的类。它负责向操作系统索取内存块,并将内存块串成 “链表”。-
先来看看其中的初始化函数
Init()
void FixedAllocator::Chunk::Init(std::size_t blockSize, unsigned char blocks) { pData_ = new unsigned char[blockSize * blocks]; Reset(blockSize, blocks); } void FixedAllocator::Chunk::Reset(std::size_t blockSize, unsigned char blocks) { firstAvailableBlock_ = 0; blocksAvailable_ = blocks; unsigned char i = 0; unsigned char* p = pData_; for (; i != blocks; p += blockSize) //指向下一个可用的 block 的 index { *p = ++i; } }
-
传入参数为 block 的大小和数量
-
用 operator new 分配出一大块内存 chunk,并用指针
pData_
指向 chunk。 -
Reset()
函数对这块内存进行分割。利用嵌入式指针的思想,每一块 block 的第一个 byte 存放的是下一个可用的 block 距离起始位置 pData_ 的偏移量(以 block 大小为单位),以这种形式将 block 串成 “链表”。(这里的核心就是申请的内存块其实是顺序结构的,而用index来模拟了指针) -
firstAvailableBlock_
表示当前可用的 block 的偏移量;blocksAvailable_
表示当前 chunk 中剩余的 block 数量。 -
初始状态的 chunk 如下图所示:
初始分配状态如下:
-
内存分配 allocate
void* FixedAllocator::Chunk::Allocate(std::size_t blockSize) { if (!blocksAvailable_) return 0;//如果没有可用空间了就返回 unsigned char* pResult = pData_ + (firstAvailableBlock_ * blockSize);//分配空间 firstAvailableBlock_ = *pResult;//下一个预分配的idx为这个分配出去的result内部的值标示的地址 --blocksAvailable_;//调整可用空间 return pResult; }
-
内存回收 deallocate
void FixedAllocator::Chunk::Deallocate(void* p, std::size_t blockSize) { unsigned char* toRelease = static_cast<unsigned char*>(p);//借他的空间来标示自己的内部的idx *toRelease = firstAvailableBlock_;//吧这个值存成之前的预分配的下一个的idx //修改 “头指针idx” //回收的 block 指针距离头指针 pData_ 的距离(以 block 为单位) firstAvailableBlock_ = static_cast<unsigned char>( (toRelease - pData_) / blockSize);//然后告诉容器下一个分配“我”,“我”可以用 ++blocksAvailable_;//可分配容量增加 }
-
-
FixedAllocate
-
FixedAllocate 则负责管理一个具有相同 block size 的 chunk 的集合。它负责根据客户需求,创建特定 block 大小的 chunk ,并放置在容器 vector 中进行管理。
-
allocate
void* FixedAllocator::Allocate() { if (allocChunk_ == 0 || allocChunk_->blocksAvailable_ == 0) { //目前没有标定的 chunk ,或者这个 chunk 已经用完了 //从头找起 Chunks::iterator i = chunks_.begin(); for (;; ++i) { //没找到,则创建一个新的 chunk, if (i == chunks_.end()) { // Initialize chunks_.reserve(chunks_.size() + 1); Chunk newChunk; newChunk.Init(blockSize_, numBlocks_); chunks_.push_back(newChunk); allocChunk_ = &chunks_.back(); //上面容器的大小会增长一个,可能会引起 vetor 的扩容操作,导致原来的元素被搬移到新的地方,而原来的deallocChunk指向的地址已经不可用,原来的那个经过搬移之后已经无法确定新的位置了,所以这里的 deallocChunk 需要重新标定,直接标定为第一个(就是因为已经全部不可用了才申请的新内存块,所以回收到哪里指向原来chunkslist的任何一个都同一个效果) deallocChunk_ = &chunks_.front(); break; } //找到的话直接分配新的allocChunk_ if (i->blocksAvailable_ > 0) { allocChunk_ = &*i; break; } } } //返回 return allocChunk_->Allocate(blockSize_); }
allocChunk_
指向当前正在使用中的 chunk。如果allocChunk_
指向的 chunk 中的 block 已经用完了,那么就在容器中去寻找其他可用的 chunk 。如果没有找到,就新建一个 chunk ,放进容器中,并标定为当前的allocChunk_
。- 本来这里的 allocate 动作跟
deallocChunk_
成员没有关系的。但是,创建新的 chunk 并添加进 vector 中后,可能会引起 vector 的内存重分配动作(内存搬移),导致原来的deallocChunk_
指向的内存并不存在了,所以要对deallocChunk_
重新标定。它这里直接重新标定为第一个 chunk,因为原来的那个已经无法确定位置了。
-
内存回收 deallocate
-
void FixedAllocator::Deallocate(void* p) { deallocChunk_ = VicinityFind(p); DoDeallocate(p); } /* VicinityFind() 函数采用一种分头查找的算法,从上一次 deallocChunk_ 的位置出发,在容器中分两头查找。这也应该是设计这个 deallocChunk_ 指针的原因把。内存分配通常是给容器服务的。而容器内元素连续创建时,通常就从同一个 chunk 获得连续的地址空间。归还的时候当然也需要归还到同一块 chunk 。通过对上一次归还 chunk 的记录,能提高搜索的效率。下面是 VicinityFind() 的实现代码: */ Chunk * FixedAllocator::VicinityFind( void * p ) const { if ( chunks_.empty() ) return NULL; assert(deallocChunk_); const std::size_t chunkLength = numBlocks_ * blockSize_; Chunk * lo = deallocChunk_; Chunk * hi = deallocChunk_ + 1; const Chunk * loBound = &chunks_.front(); const Chunk * hiBound = &chunks_.back() + 1; //特殊情况:deallocChunk_是数组中的最后一个 if (hi == hiBound) hi = NULL; //这里从上一次 deallocChunk_ 的位置出发,在容器中分两头查找。内存分配通常是给容器服务的。而容器内元素连续创建时,通常就从同一个 chunk 获得连续的地址空间。归还的时候当然也需要归还到同一块 chunk 。通过对上一次归还 chunk 的记录,能提高搜索的效率。但是这个搜索方式有个隐患, 双端查找没找 for (;;) { if (lo) { if ( lo->HasBlock( p, chunkLength ) ) return lo; if ( lo == loBound ) { lo = NULL; if ( NULL == hi ) break;//新版本改了,这是我拿的新版本代码,当上下都找不到的时候会退出循环,不然会死循环,什么时候出现呢?当你Deallocate一个并不是他分配出来的对象的时候,就肯定找不到,必死循环 } else --lo; } if (hi) { if ( hi->HasBlock( p, chunkLength ) ) return hi; if ( ++hi == hiBound ) { hi = NULL; if ( NULL == lo ) break; } } } return NULL; } /* 最后内存回收的动作由函数 DoDeallocate() 完成。如果当前回收的 chunk 已经将所有的 block 全部回收完了,即 deallocChunk_->blocksAvailable_ == numBlocks_ ,本来这块内存就可以归还给 OS 了的。但是这里采取了一个延迟归还的动作。把这个空的 chunk 通过 swap 函数放在 vector 的末尾,并且将 allocChunk_ 指向它,供下一次再使用。只有当有两个空 chunk 出现时,才会把上一个空的 chunk 归还给 OS。下面是源码: */ void FixedAllocator::DoDeallocate(void* p) { //调用块,将调整内部列表,但不会释放内存 deallocChunk_->Deallocate(p, blockSize_); //如果已经全回收了 if (deallocChunk_->blocksAvailable_ == numBlocks_) { // deallocChunk_是完全空的,这时候应该释放它了吗?(先等等,不要激进) Chunk& lastChunk = chunks_.back(); //最后一个就是当前的 deallocChunk if (&lastChunk == deallocChunk_) { //当有两个空deallocChunk_的时候,我再去释放,如果只有一个就缓存起来备用,万一马上就有人用了呢 if (chunks_.size() > 1 && deallocChunk_[-1].blocksAvailable_ == numBlocks_) { // 有两个,释放最后一个 lastChunk.Release(); chunks_.pop_back(); allocChunk_ = deallocChunk_ = &chunks_.front(); } return; } if (lastChunk.blocksAvailable_ == numBlocks_)//可用空间等于预分配大小,代表是全空的 { //和上面一样释放一下 lastChunk.Release(); chunks_.pop_back(); allocChunk_ = deallocChunk_; } else { //如果只有一个空的,要把这个空的放到容器末尾,让申请器去使用 std::swap(*deallocChunk_, lastChunk); allocChunk_ = &chunks_.back(); } } }
-
-
-
SmallObjAllocator
-
void* SmallObjAllocator::Allocate(std::size_t numBytes) { //SmallObjAllocator 不可能无穷无尽的满足客户不同的 block size 的需求。它设有一个最大的 block size 变量 `maxObjectSize_` 。如果客户端需求的 block size 大于这个 threshold,就直接交由 operator new 去进行处理。 if (numBytes > maxObjectSize_) return operator new(numBytes); //pLastAlloc_ 记录上一次分配 block 的 FixedAllocator object 。如果这一次需求的 block size 等于上一次分配的 block size,就直接使用同一个 FixedAllocator object 去分配内存。我认为这个变量的设计和 FixedAllocator 中 deallocChunk_ 的设计道理是一样的。 SmallObjAllocator 是给容器服务,而容器通常连续多次为其中的 element 索取多个相同 size 的 block,所以对上一次分配的 FixedAllocator object 进行记录能够减少不必要的查找动作。 if (pLastAlloc_ && pLastAlloc_->BlockSize() == numBytes) { return pLastAlloc_->Allocate(); } /* 如果这一次需求的 block size 不等于上一次分配的 block size,就遍历容器寻找不小于需求的 block size 而且最接近的位置,也就是 std::lower_bound() 函数的功能。如果找到 block size 相等的,就直接分配;如果没找到相等的,就在该位置上插入一个新的 FixedAllocator object。同样,为了防止 vector 扩容操作引起重新分配内存,需要对 pLastDealloc_进行重定位。 */ //找到第一个 >= numBytes 的位置 Pool::iterator i = std::lower_bound(pool_.begin(), pool_.end(), numBytes); //没找到相同的,就重新创建一个 FixedAllocator if (i == pool_.end() || i->BlockSize() != numBytes) { i = pool_.insert(i, FixedAllocator(numBytes)); pLastDealloc_ = &*pool_.begin(); } pLastAlloc_ = &*i; return pLastAlloc_->Allocate(); }
-
-
内存回收 deallocate
-
void SmallObjAllocator::Deallocate(void* p, std::size_t numBytes) { /*设计同上*/ if (numBytes > maxObjectSize_) return operator delete(p); if (pLastDealloc_ && pLastDealloc_->BlockSize() == numBytes) { pLastDealloc_->Deallocate(p); return; } Pool::iterator i = std::lower_bound(pool_.begin(), pool_.end(), numBytes); assert(i != pool_.end()); assert(i->BlockSize() == numBytes); pLastDealloc_ = &*i; pLastDealloc_->Deallocate(p); }
-
-
与alloc比较
- alloc 一旦向 OS 索取了新的 chunk,就不会还给 OS 了,一直在自己的掌控之中。因为它里面的指针拉扯比较复杂,几乎不可能去判断一块 chunk 中给出去的 block 是否全部归还了。但是
loki::allocator
通过利用一个blocksAvailable_
变量,就很容易的判断出某一块 chunk 中的 block 是否已经全部归还了,这样就可以归还给 OS。 - std::alloc 只负责一些特定 block size 的内存管理。如果客户端需要的 block size 它并不支持,那个客户端的 block size 会被取整到最接近的大小 (当然前提是小于它所能够分配的最大的 block size);但是
loki::allocator
能够为不大于最大 block size 的所有 block size 服务。
- alloc 一旦向 OS 索取了新的 chunk,就不会还给 OS 了,一直在自己的掌控之中。因为它里面的指针拉扯比较复杂,几乎不可能去判断一块 chunk 中给出去的 block 是否全部归还了。但是
-
-
-
GNU-C
下的6个分配器-
new_allocator
- 调用
::operator new/delete
,虽然底层还是调用malloc/free
,但是好处是可以支持全局重载::operator new/delete
让用户自定义分配内存的操作
- 调用
-
malloc_allocator
- 直接调用
malloc/free
- 直接调用
-
array_allocator
-
/* 该分配器直接使用静态数组进行分配,开局直接指定大小,牛皮,所以空间固定,而且回收不会释放,总的来说,没什么用 - - */ //数组分配器, 从数组中直接分配 template<typename _Tp, typename _Array = std::tr1::array<_Tp, 1>> //底部是C++数组 静态数组(不需要释放)——所以deallocate()是空的 class array_allocator : public array_allocator_base<_Tp> { public: typedef size_t size_type; typedef _Tp value_type; typedef _Array array_type; private: array_type* _M_array; //指向数组的指针 size_type _M_used; public: array_allocator(array_type* __array = NULL)throw() : _M_array(_array), _M_used(size_type()){} } using namespace std; using namespace std::tr1; using namespace __gnu::cxx; int my[65535]; array_allocator<int, array<int, 65535>> myalloc(&my); //初始化 //分配空间 但是不会回收释放过的空间 int* p1 = myalloc::allocate(1); int* p2 = myalloc.allocate(3); myalloc.deallocate(p1); myalloc.deallocate(p2); }
-
-
debug_allocator
- 就debug用的,内部加了一点数据类似cookie的功能来记录分配的空间大小,如果回收的时候大小不一致会用assert报错,提醒用户,可能调试的时候用的到,正式环境也没啥用。本来分配器就是为了省下多余cookie空间才搞的,不然具体malloc底层都给你管理得棒棒的了,现在保留了cookie还再加点数据头进去真是牛皮。
-
pool_allocator:
- 见上述Std::allocator
-
bitmap_allocator
- 分配(只服务于一个对象,不服务多个,就算大小一样也不行)
- 容器分配一个
blocks
,每个区块8个字节。加上bitmap
以及一些区块叫super-blocks
。假设有64个blocks
,则需要的bitmap
是64bits
——两个unsigned int
。bitmap
每一个bit
为1表示还未分配。use-count
表示用掉的区块计数(整数)。size-super-block
表示super-block
的大小——64 * 8(block size) +4(use-count) + 4*2(bitmap) = 524Bytes。
- 底层实现了一个
__mini_vector
: 自行实现的高仿Vector,用来管理结构管理bitmap
——成倍增长,为啥自己实现嘞,因为如果用了Vector,又服务于Vector,那不就鸡生蛋蛋生鸡了嘛。 super-block
是基本单元。当启动第二个super-block
的时候,首先区块数量会加倍——从64Bytes
变成128Bytes
。接着bitmap
会变成4个unsigned int
。这时候size-super-block
为128 * 8(block size) + 4 * 4(bitmap) + 4(use-count)= 1044bytes
。由于需要有两个控制中心——所以__mini_vector
也需要增加,因为是类vector
所以有时成倍增长。
- 容器分配一个
- 释放
- 会再增加一个
__mini_vector
用来控制回收的链表,内部按大小排序,最多64个entry
,如果超过了区块就会还最大的给操作系统。当freelist中有值的时候,再分配时不会new一个新的Entry,会从这里拿一个出去补给系统。
- 会再增加一个
- 分配(只服务于一个对象,不服务多个,就算大小一样也不行)
-
-
-
-
先看三种结构体的书写格式
struct TestSturct { char A; int B; short C; }test1; struct TestSturct { char A; int B; short C; }test2; struct TestSturct { char A; int B; short C; }test3;
我们都知道,char类型占用1个字节,int型占用4个字节,short类型占用2个字节,那么我们可能会犯一个错误就是直接1+4+2=7,该结构体占用7个字节。这是错的。
-
计算结构体大小时需要考虑其内存布局,结构体在内存中存放是按单元存放的,每个单元多大取决于结构体中最大基本类型的大小。
-
对格式一:
以int型占用4个来作为倍数,因为A占用一个字节后,B放不下,所以开辟新的单元,然后开辟新的单元放C,所以格式一占用的字节数为:3*4=12;
同理对于格式二:
A后面还有三个字节,足够C存放,所以C根着A后面存放,然后开辟新单元存放B数据。所以格式二占用的内存字节为2*4=8.
对于格式三:
上面结构计算大小,sizeof(Phone3) = 1 + 2 + 4 = 7, 其大小为结构体中个字段大小之和,这也是最节省空间的一种写法。
-
总结
- 第一种写法,空间浪费严重,sizeof 计算大小与预期不一致,但是保持了每个字段的数据类型。这也是最常见的漫不经心的写法,一般人很容易这样写
- 第三种写法,最节省空间的写法,也是使用 sizeof 求大小与预期一样的写法,但是全部使用字节类型,丢失了字段本生的数据类型,不方便使用
- 第二种写法,介于第一种和第三种写法之间,其空间上比较紧凑,同时又保持了结构体中字段的数据类型。
- 了解了这些这些写法的差异性,可以视情况选用
-
-
-
原因
- 在 C++ 程序中,主要涉及到的内存就是『栈』和『堆』,通常来说,一个线程的栈内存是有限的,通常来说是 8M 左右(取决于运行的环境)。栈上的内存通常是由编译器来自动管理的。当在栈上分配一个新的变量时,或进入一个函数时,栈的指针会下移,相当于在栈上分配了一块内存。我们把一个变量分配在栈上,也就是利用了栈上的内存空间。当这个变量的生命周期结束时,栈的指针会上移,相同于回收了内存。由于栈上的内存的分配和回收都是由编译器控制的,所以在栈上是不会发生内存泄露的,只会发生栈溢出(Stack Overflow),也就是分配的空间超过了规定的栈大小。而堆上的内存是由程序直接控制的,程序可以通过 malloc/free 或 new/delete 来分配和回收内存,如果程序中通过 malloc/new 分配了一块内存,但忘记使用 free/delete 来回收内存,就发生了内存泄露。
- 以引用方式返回局部定义的动态内存对象,当离开局部作用域后,唯一与动态内存对象有关联的就只有那个引用了,引用不同于指针,不能够通过delete来释放。从而该动态对象无法释放(当然也可以将引用的地址强行赋值给一个指针,然后用delete加该指针来释放,但这样显得太过偏门,而且一般情况下,对于一个引用,也不会想到要对它的内存进行手动释放)。需要杜绝这种返回局部动态对象引用的方式,可以用返回指针代替。
- 局部动态内存对象指针:返回的局部动态内存对象的指针,使用完后,往往会忘了得到的这个指针是一个指向动态内存对象的指针,会忘了需要手动来释放该对象。解决办法是使用智能指针,不需要手动进行释放。
- 动态内存变量的指针间的相互赋值:指向动态内存的指针,这些指针间相互赋值时,有时候会忘了先释放赋值符左边指针指向的动态内存。对于对象的赋值,如果对象内有指向动态内存的指针型成员,则可以将 "先释放操作” 实现在赋值运算符重载操作中。
- 析构函数没有定义成虚函数: 析构函数需要定义成虚函数,否则用delete加基类指针方式释放子类对象时,调用的是基类的析构函数,导致子类对象的部分成员没有被释放,从而导致内存泄漏。
- 释放动态内存对象数组时,只释放了第一个对象:用delete加中括号的方式通过指针释放在堆区申请的对象数组。有时会忘记指针指向的是对象数组而不是单个对象,也就是说在使用delete进行释放时会忘了加中括号,从而导致只释放了第一个动态内存对象,而后续数组元素没有被释放,从而导致内存泄漏。可以将指向对象数组的指针名取得更直观些来提醒自己delete需要释放的是一个数组。如将pObject更名为paObject (a代表Array)
- 多层动态内存嵌套,只释放了最上面一层: 在堆区创建的用来存放“动态内存的指针”的数组(或容器)就是一个二层动态内存嵌套,在用delete释放时,要先释放数组(或容器)中指针指向的动态内存(也就是堆区创建的对象),然后再释放这个数组(或容器)。很容易只释放了数组或容器所占动态内存空间(也就是第一层),而忘记释放数组或容器中指针指向的内存(也就是第二层),从而导致内存泄漏。
-
如何避免?
-
不要手动管理内存,可以尝试在适用的情况下使用智能指针。
-
使用string而不是char*。string类在内部处理所有内存管理,而且它速度快且优化得很好。
-
除非要用旧的lib接口,否则不要使用原始指针。
-
在C++中避免内存泄漏的最好方法是尽可能少地在程序级别上进行new和delete调用--最好是没有。任何需要动态内存的东西都应该隐藏在一个RAII对象中,当它超出范围时释放内存。RAII在构造函数中分配内存并在析构函数中释放内存,这样当变量离开当前范围时,内存就可以被释放。
(注:RAII资源获取即初始化,也就是说在构造函数中申请分配资源,在析构函数中释放资源)
-
使用了内存分配的函数,要记得使用其想用的函数释放掉内存。可以始终在new和delete之间编写代码,通过new关键字分配内存,通过delete关键字取消分配内存。
-
培养良好的编码习惯,在涉及内存的程序段中,检测内存是否发生泄漏。
-
-
-
- 在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区
- 栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
- 堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete.如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
- 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
- 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改)
- 自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
- 在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区
-
-
现代计算机都直接在代码底层支持栈的数据结构,这体现在,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。这种机制的特点是效率高,支持的数据有限,一般是整数,指针,浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构。因为栈的这种特点,对栈的使用在程序中是非常频繁的。对子程序的调用就是直接利用栈完成的。机器的call指令里隐含了把返回地址推入栈,然后跳转至子程序地址的操作,而子程序中的ret指令则隐含从堆栈中弹出返回地址并跳转之的操作。C/C++中的自动变量是直接利用栈的例子,这也就是为什么当函数返回时,该函数的自动变量自动失效的原因。
-
和栈不同,堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的。基本的malloc/realloc/free 函数维护了一套内部的堆数据结构。当程序使用这些函数去获得新的内存空间时,这套函数首先试图从内部堆中寻找可用的内存空间,如果没有可以使用的内存空间,则试图利用系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当的处理(比如和其他空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。这套复杂的分配机制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制有如下若干原因:
- 系统调用可能不支持任意大小的内存分配。有些系统的系统调用只支持固定大小及其倍数的内存请求(按页分配);这样的话对于大量的小内存分类来说会造成浪费。
- 系统调用申请内存可能是代价昂贵的。系统调用可能涉及用户态和核心态的转换。
- 没有管理的内存分配在大量复杂内存的分配释放操作下很容易造成内存碎片。
-
堆和栈的对比
- 栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活
- 堆是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。
- 栈是系统数据结构,对于进程/线程是唯一的
- 堆是函数库内部数据结构,不一定唯一。不同堆分配的内存无法互相操作。
- 栈空间分静态分配和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由alloca函数完成。栈的动态分配无需释放(是自动的),也就没有释放函数。从程序的可移植性上看,对栈的动态分配操作是不被鼓励的!
- 堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存/ 释放内存匹配是良好程序的基本要素。
-
不同问题的对比
- 碎片问题:
- 对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。
- 对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。
- 生长方向:
- 对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向
- 对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
- 分配方式:
- 堆都是动态分配的,没有静态分配的堆。
- 栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现
- 分配效率:
- 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
- 堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法,在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
- 碎片问题:
-
明确区分堆与栈:
void f() { int* p=new int[5]; delete[] p; }
这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p.在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中
-
堆和栈主要的区别由以下几点:
- 管理方式不同
- 对于栈来讲,是由编译器自动管理,无需我们手工控制
- 对于堆来说,释放工作由程序员控制,容易产生memory leak.
- 空间大小不同
- 一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M。当然,我们可以修改但是它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
- 能否产生碎片不同
- 堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
- 生长方向不同(上面有描述)
- 分配方式不同(上面有描述)
- 分配效率不同(上面有描述)
- 管理方式不同
-