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++中却没有调用,这个还要看平台。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值