C++内存管理

前言:

本篇将介绍c/c++的内存空间结构与c++中对内存进行管理的用法,包括new,delete,operator new与operator delete,定位new以及与c中malloc和free的区别等,到stl容器的底层实现篇将会对内存操作进行模拟实现,会进一步加深对内存管理的理解。

目录

前言:

1.new与delete操作符

2.c/c++内存分布

3.operator new与operator delete函数(不是运算符重载

4.malloc/free与new/delete的区别

5.new失败后的抛异常

6.定位new

总结: 文章对c++的内存管理进行了简单的分析与介绍,异常,内存池等内容将在后面介绍。如有错误,请指正!


1.new与delete操作符

开空间:

	//开空间
	int* p1 = (int*)malloc(sizeof(int));
	if (p1 == nullptr)
		perror("malloc fail");

	int* pp1 = new int;

	free(p1);
	delete pp1;
  • new后需要使用delete进行内存释放(调用析构函数),不然一样会造成内存泄漏。
  • new后面跟要开空间的类型。
  • 与malloc相比,方便的是不用手动检查开辟空间失败的问题,这与底层实现有关。

开10个int空间:

	//开10个int空间
	int* p2 = (int*)malloc(sizeof(int) * 10);
	if (p2 == nullptr)
		perror("malloc fail");

	int* pp2 = new int[10];

	free(p2);
	delete[] pp2;
  • 使用new int [10]意思就是开辟10个int空间,申请一个10个int的数组。
  • 注意new开辟的空间为10,是一组对象,所以销毁时要用配套的delete []进行所有元素的销毁即调用每一个元素的析构,也就是delete只会调用第一个数组元素的析构函数。对于基本类型,没有析构函数,二者没有区别。

初始化:

//初始化
int* p3 = new int(10);
int* p4 = new int[10] {1, 2, 3, 4};

delete p3;
delete[] p4;

 p3是开辟或者说申请一个int,初始化为10;p4是开辟10个int,初始化后面给的数据,其它的初始化为0。

对于自定义类型:

class A
{
public:
	A(int a)
		:_a(a)
	{
		cout << "A()" << endl;
	}
private:
	int _a;
};
int main()
{
	//开空间
	int* p1 = (int*)malloc(sizeof(int));
	if (p1 == nullptr)
		perror("malloc fail");

	int* pp1 = new int;

	free(p1);
	delete pp1;

	//开10个int空间
	int* p2 = (int*)malloc(sizeof(int) * 10);
	if (p2 == nullptr)
		perror("malloc fail");

	int* pp2 = new int[10];

	free(p2);
	delete[] pp2;

	//初始化
	int* p3 = new int(10);
	int* p4 = new int[10] {1, 2, 3, 4};

	delete p3;
	delete[] p4;


	A* paa = new A(2);
	return 0;
}

new对于自定义类型,会调用它的构造函数进行初始化:

 new之后可以传参,调用构造函数初始化:

注意如果没有默认构造,直接new是不行的;如果构造没有参数列表,new时传参也是不行的,都会报错。

new后面可以使用free释放,但是有时会出错,建议都配套使用,这与底层实现有关。

2.c/c++内存分布

这里补充复习一点内存的知识:

内存空间分布:

  •  栈又叫做堆栈,用来存储非静态局部变量/函数参数/返回值等等,栈是向下增长的(也就是从高地址到低地址)。
  • 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库,用户可使用系统接口创建共享共享内存,做进程间通信(linux中的进程地址空间)。
  • 堆用于程序运行时动态内存分配,堆是可以向上增长的。
  • 数据段用来存储全局数据和静态数据。
  • 代码段用来储存可执行的代码/只读常量。

来看3个例子,运用一下:

char char1[] = "abcd";

const char* pChar2 = "abcd";

int* ptr1 = (int*)malloc(sizeof(int));

第一个,对数组名解引用*char,是存放在栈区还是常量区(代码段)?虽然字符串是存放在常量区的,但是数组存放在栈区,这里是将存放在常量区的字符串拷贝到栈区上,且这个字符数组是可以修改的。

第二个,我们要知道const修饰的是pChar2这个指针指向的空间,而pChar2这个指针是存放在栈区的;因为const修饰的是指针指向的空间,所以这块空间是在常量区的,所以这个字符数组不能修改。

第三个,ptr1是指针,是临时变量,存放在栈上的,而解引用*ptr1是代表指针指向的空间,这块空间是动态开辟的,所以是在堆上的。

3.operator new与operator delete函数(不是运算符重载)

首先强调new和delete是进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层调用operator delete全局函数来释放空间。

	//失败抛异常
	int* p1 = (int*)operator new(sizeof(int));

	//失败返回nullptr
	int* p2 = (int*)malloc(sizeof(int));
	if (p2 == nullptr)
		perror("malloc fail");

我们要知道,operator new是malloc的封装,operator delete是free的封装。

new对于内置类型,会直接调用operator new分配内存(也就是调用封装在operator new中的malloc);对于自定义类型,会先调用operator new分配内存,然后再调用构造函数初始化。new[]对于简单的类型,会直接计算出大小进行开空间,对于自定义类型,会在指针的前size_t个字节(不一定是4字节,应该是在指针前size_t个字节(32位是4,64位是8)大小写入数组大小,析构也是析构的指针大小减去这个size_t字节大小指向的空间,也就是说多开的用来存储数组大小的也要释放)写入数组的大小,然后调用对应次的构造函数,也就是说会额外存储数组的大小。

delete对于内置类型会直接调用free函数释放掉开辟的内存,对于自定义类型,会先调用析构函数清理掉new开辟空间中的资源,再调用operator delete也就是调用封装在其中的free释放掉这块开辟的空间。

为什么要封装呢?

机制在于失败了的处理不一样,malloc失败会直接返回空,而new或者delete失败则会抛异常(异常篇再分析)。

对于这样的,new时会先调用operator new []再调用operator new,只是多了一层封装,然后开辟空间,同时开辟空间时会在指针的前4个字节记录数组大小n,在构造时则会构造数组大小n次;delete时也是一样,先调用析构函数,会取出之前的数组大小,然后析构n次,再调用operator delete[],再调用operator delete,也就去调用free释放指针减size_t指向的这块空间(指针开始是指向new开辟的空间的,前面的存储数组大小这块空间也要释放)。 

我们来看看如果不匹配使用操作符一定会报错的场景:

class A
{
public:
	A()
		:_a(nullptr)
	{
		_a = new int;
		cout << "A()" << endl;
	}

	~A()
	{
		delete _a;
	}
private:
	int* _a;
};

int main()
{
	A* p1 = new A[10];
	delete[] p1;//正确

	delete p1;//报错
	
	free(p1);//报错
	return 0;
}

根据之前的知识,我们就能分析: 

第一个正确,调用delete[]时先调用析构,取出之前存储的数组的大小n,析构n次,然后再调用operator delete[],再调用operator delete,也就是调用free释放空间。

第二个报错,原因是没有使用配套的delete[],此时释放指针指向空间的位置就不对了:

前面的空间没有被释放,导致了内存泄露。

第三个报错,没有配套使用,释放的空间位置不对。但是当我们不写析构函数时,编译器觉得析构函数不需要调用,就不会开这4个字节了,所以拿free或者delete释放也就没有问题,但是还是实际还是要配套使用。

4.malloc/free与new/delete的区别

共同点:

malloc/free和new/delete都是从堆上申请空间,并且需要用户手动释放。

不同点:

  1. malloc/free是函数,new/delete是操作符。
  2. malloc申请的空间不会初始化,new对于内置类型可以初始化,对应自定义类型会去调用构造函数初始化。
  3. malloc申请空间时,需要手动计算空间大小并传递参数,new只需要在后面跟上空间的类型即可,如果是多个类型,[]中指定对象个数即可。
  4. malloc返回值是void*,在使用时必须强转,new不需要,因为new后面跟的是空间类型。
  5. malloc申请空间失败时,返回NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数和析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。

5.new失败后的抛异常

malloc失败后,开不到2G跳出循环。

new失败,抛异常,在异常篇会补充。

6.定位new

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

使用格式:

new (place_address) type或者new (place_address) type (initializer-list)

place_address为一个指针,initialize-list为类型的初始化列表。

class A
{
public:
	A(int a=1)
		:_a(a)
	{

	}
private:
	int _a;
};

int main()
{
	A aa;
	A* p1 = (A*)malloc(sizeof(A));
	if (p1 == nullptr)
	{
		perror("malloc fail");
	}
	
	//对一块已有的空间初始化
	new(p1)A(1);
	return 0;
}

那直接new不比使用malloc再定位new香吗?定位new适用于节省效率的场景,用于内存池申请的内存没有初始化:

先简单了解一下,

直接new就是直接去操作系统的堆去申请空间,然后再返回这块空间,访问慢;

而有了内存池,直接去内存池申请,申请到返回这块空间,如果没有申请到,就去

操作系统的堆上去申请大块内存,再返回给内存池,这样能提升效率。

类比去山下打水就是直接去操作系统申请空间,而去内存池是在家里的蓄水池里打水,

不够了再下山打水。

总结:
 文章对c++的内存管理进行了简单的分析与介绍,异常,内存池等内容将在后面介绍。如有错误,请指正!

  • 85
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 53
    评论
评论 53
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值