CPP内存管理 第一章
一、四种内存分配和释放的方法
通过直接或简介的对内存进行操作,这里只讨论上述CRT及以上的内存分配:
通过malloc和new分配内存、或者通过delete和free释放内存是最常见的,通过::operator new和::operator delete操作内存比较少见,allocator分配器操作内存在STL中使用比较多,不同的编译器的使用可能也会有差别。
二、基本构件之new/delete
1、内存申请
使用new操作编译器背后做的事:
1、通过operator new操作分配一个目标类型的内存大小
2、static_cast将得到的内存块地址强转换为目标类型指针
3、调用目标类型的构造函数
注意:operator new()内部调用的还是malloc()
2、内存释放
使用delete操作编译器背后做的事:
1、调用对象的析构函数
2、通过operator delete()释放内存,本质上调用的是free()
3、模拟编译器直接调用构造和析构函数
在VS2019中测试如下:
#include <iostream>
#include <string>
using namespace std;
class A{
public:
int id;
A() : id(0) { cout << "default ctor. this=" << this << " id=" << id << endl; }
A(int i) : id(i) { cout << "ctor. this=" << this << " id=" << id << endl; }
~A() { cout << "dtor. this=" << this << " id=" << id << endl; }
};
void test01(){
string* pstr = new string;
cout << "str= " << *pstr << endl;
//! pstr->string::string("aaaaa"); //[Error]
//! pstr->~string(); //crash -- 语法正确, 但是因为上一行不能执行,所以注释掉
//-----------------------------------
A* pA = new A(1); //ctor. this=0147F920 id=1
cout << pA->id << endl; //1
pA->A::A(3);
//in VC6 : 正确执行:ctor. this=0147F920 id=3
//in GCC : [Error]
cout << pA->id << endl; //3
delete pA; //dtor. this=000307A8
A::A(5); //这里的生命周期仅在这一行,调用完构造 马上析构
//in VC6 : ctor. this=012FF5D8 id=5
// dtor. this=012FF5D8 id=5
//in GCC : [Error]
//simulate new
void* p = ::operator new(sizeof(A));
cout << "p=" << p << endl; //p=01466370
pA = static_cast<A*>(p);
pA->A::A(2);
//in VC6 : ctor. this=01466370 id=2
//in GCC : [Error]
//simulate delete
pA->~A(); //dtor. this = 01466370 id = 2
::operator delete(pA); //free()
}
int main(void){
test01();
return 0;
}
VS可以直接使用内存空间调用构造和析构函数,但侯捷测试在GNU C下无法通过。
3、Array new
使用delete:
1、对于像int数组,或者析构函数没有意义的数组,不会造成问题。
2、若对象内部使用了new指向其他空间,只会调用一次析构函数,会造成内存泄漏。
使用delete[]:
1、会调用每个对象的析构函数
2、构造函数调用顺序是按照构建对象顺序来执行的,但是析构函数执行却相反。
new array对象的内存分配情况:
如果使用new分配十个内存的int,内存空间如上图所示,首先内存块会有一个头和尾,黄色部分为debug信息,灰色部分才是真正使用到的内存,蓝色部分的12 bytes是为了让该内存块以16字节对齐。在这个例子中delete pi和delete[] pi效果是一样的,因为int没有析构函数。但是下面的例子就不一样了:
上图通过new申请三个Demo空间大小,内存块使用了96 byte,这里是这样计算得到的:黄色部分调试信息32 + 4 = 36 byte;黄色部分下面的“3”用于标记实际分配给对象内存个数,这里是三个所以里面内容为3,消耗4 byte;Demo内有三个int类型成员变量,一个Demo消耗内存3 * 4 = 12 byte,由于有三个Demo,所以消耗了12 * 3 = 36 byte空间;到目前为止消耗36 + 4 + 36 = 76 byte,加上头尾cookie一共8 byte一共消耗84 byte,由于需要16位对齐,所以填充蓝色部分为12 byte,一共消耗了84 + 12 = 96 byte。这里释放内存时需要加上delete[],上面分配内存中有个标记“3”,所以编译器将释放三个Demo对象空间,如果不加就会报错。
4、placement new
5、重载
1、C++内存分配的途径
正常情况下,调用new会走第二条路线:
new =>(系统提供)::operator new() =>malloc()
类中重载了operator new,调用new 会走第一条路线:
new=>operator new()=>Foo::operator new()=>::operator new()=>malloc()
有点问题这里,为什么还需要调用系统的::operator new()?
对于GNU C,背后使用的allocate()函数最后也是调用了系统的::operator new()函数。
2、重载new 和delete
上图演示了如何让重载::operator new()函数,但是一般不推荐重载::operator new()函数,因为对全局有影响,使用不当容易造成问题。
推荐在类中重载operator new()函数,必须保证函数参数列表第一个参数是size_t类型变量。对于operator delete(),第一个参数必须是void* 类型,第二个size_t是可选项,可以去掉。
对于operator new[]和operator delete[]函数的重载,和前面类似。
3、Per Class Allocator(这里采用static allocator)
#include <iostream>
class Allocator {
private:
struct obj {
obj* next;
};
public:
void* allocate(size_t);
void deallocate(void*, size_t);
private:
obj* freeStore = nullptr;
const int CHUNK = 5;
};
void* Allocator::allocate(size_t size)
{
obj* p = nullptr;
if (!freeStore) {
//linked list为空,于是申请一大片
size_t chunk = CHUNK * size;
freeStore = p = (obj*)malloc(chunk);
//将分配的一大块当作linkedlist一般,小块小块连接起来
for (int i = 0; i < (CHUNK - 1); i++) {
p->next = (obj*)((char*)p + size);
p = p->next;
}
p->next = nullptr; //最后一块
}
p = freeStore;
//提前申请了5块,虽然你只需要一块,但是申请了5块
//下次还申请的话,因为这里的freeStore不是空的,下一次就直接将返回的指针指向这个freeStore的这一块
freeStore = freeStore->next;
return p;
}
void Allocator::deallocate(void* p, size_t size) {
//头插法将不需要的也就是delete的内存块继续挂到可使用的内存链表块上
//注意这里只是将内存块标记为可使用,但是没有交还给操作系统
((obj*)p)->next = freeStore;
freeStore = (obj*)p;
}
//怎么使用这个Allocator?
class Foo {
public:
int num;
//定义一个小型的内存池,用链表连接起来
static Allocator myAlloc;
public:
Foo(int num_) :num(num_) {}
//对operator new和operator delete进行重载
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;
void test01() {
Foo* p[50];
std::cout << "sizeof(Foo)" << sizeof(Foo) << std::endl;
for (int i = 0; i < 23; i++) {
p[i] = new Foo(i);
std::cout << p[i] << " " << p[i]->num << std::endl;
}
for (int i = 0; i < 23; i++) {
delete(p[i]);
}
}
int main() {
test01();
return 0;
}
/*
test01部分运行结果:前5块的内存地址相差是4个字节,说明内存池起作用
sizeof(Foo)4
015CF930 0
015CF934 1
015CF938 2
015CF93C 3
015CF940 4
015C63A0 5
015C63A4 6
015C63A8 7
*/
4、macro for static allocator
之前的几个版本都是在类的内部重载了operator new()和operator delete()函数,这些版本都将分配内存的工作放在这些函数中,但现在的这个版本将这些分配内存的操作放在了allocator类中,这就渐渐接近了标准库的方法。也可以使用宏来将这些高度相似的代码提取出来,简化类的内部结构,最后达到的结果是一样的:
5、global allocator
上述自己定义的分配器使用了一条链表来管理内存的,但标准库却用了多条链表来管理:
6、new handlle
如果用户调用new申请一块内存,如果由于系统原因或者申请内存过大导致申请失败,这时将抛出异常,在一些老的编译器中可能会直接返回0,可以参考上图右边代码,当无法分配内存时,operator new()函数内部将调用_calnewh()函数,这个函数通过左边的typedef传入,看程序员是否能自己写一个handler处理函数来处理该问题。一般有两个选择,让更多的Memory可用或者直接abort()或exit()。下面是测试的一个结果:
该部分中自定义了处理函数noMoreMemory()并通过set_new_handler来注册该处理函数,在BCB4编译器中会调用到自定义的noMoreMemory()函数,但在右边的dev c++中却没有调用,这个还要看平台。