C/C++ 内存管理

1. 内存分配

在这里插入图片描述
栈区:运行函数而分配的局部变量函数参数返回数据返回地址等,函数结束时自动释放。
堆区: 动态分配,一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。
静态区(数据段):存放全局变量静态数据。程序结束由系统释放。
代码段: 存放 只读常量函数体(类成员函数和全局函数)的二进制代码
 
 

2. C语言动态内存分配

 
我们平时在定义数组时,如果不给定数组长度,我们将无法在栈上为数组开辟空间,所以必须指定长度,该空间是固定的,并在编译时分配内存空间。
 

2.1 malloc和free

 
1. malloc

  1. malloc()的原型是:void * malloc ( size_t  size);

  2. malloc向内存申请一块连续可用的空间
    开辟成功:返回指向这块空间的指针。
    开辟失败:返回空指针。

因此,malloc的返回值一定要做检查:

if(ptr==NULL)
{
    printf("malloc error!");
    return -1;
}
  1. void* 作为返回值,表示malloc不知道开辟空间类型,由程序员决定。
  2. 参数为0时,malloc的行为标准未定义,取决于编译器。
  3. malloc只负责开空间,不会初始化。
  4. 必须用free释放空间。

 
2. free
free()函数是来进行动态内存的释放和回收。

  1. free()的原型是:void free(void * ptr);
  2. 如果ptr指向的空间不是动态开辟的,那么free的行为未定义。
  3. 如果ptr指向空,则函数不用做任何操作。
  4. free完成后,有必要将指向该空间的指针置空,如果不置空的话,再访问会成为野指针。
  5. free只能释放堆上的空间,不能释放栈上的空间。
  6. 当free的指针是空指针,则free多次无任何问题;但如果不是空指针,则不能多次释放,只能释放一次。

 

举例:


#include<stdio.h>
#include<stdlib.h>  // system、malloc和free的头文件
int main()
{
	int num = 0;
	scanf("%d", &num);

	//int arr[num] = { 0 };  错误,这样开辟空间,长度必须是固定值
	
	int* ptr = NULL;
	ptr = (int*)malloc(num * sizeof(int));
	if (NULL != ptr)//判断ptr指针是否为空
	{
		int i = 0;
		for (i = 0; i < num; i++)
		{
			*(ptr + i) = 6;
		}
	}

	for (int i = 0; i < num; i++)
	{
		printf("%d ", *(ptr + i));
	}
	printf("\n");

	free(ptr);   //释放ptr所指向的动态内存
	ptr = NULL;  //防止成为野指针

	system("pause");
	return 0;
}

在这里插入图片描述



 

2.2 calloc和free

free同上,这里着重描述一下calloc

  1. calloc()的原型是:void * calloc (size_t num, size_t size);
                    num:对象个数      size:一个对象的大小
  2. 开辟size个大小为num的空间,并把空间每个字节初始化为0。
  3. void* 作为返回值,表示malloc不知道开辟空间类型,由程序员决定。
  4. calloc相当于malloc + 初始化(默认是0)
  5. calloc向内存申请一块连续可用的空间
    开辟成功:返回指向这块空间的指针。
    开辟失败:返回空指针。
  6. 必须用free释放空间。

举例:


#include<stdio.h>
#include<stdlib.h>  // system、calloc和free的头文件
int main()
{
	int num = 0;
	scanf("%d", &num);

	//int arr[num] = { 0 };  错误,这样开辟空间,长度必须是固定值

	int* ptr = NULL;
	ptr = (int*)calloc(num, num * sizeof(int));
	if (NULL != ptr)//判断ptr指针是否为空
	{
		printf("新开辟的空间:");
		for (int i = 0; i < num; i++)
		{
			printf("%d ", *(ptr + i));
		}
		printf("\n");


		printf("填充值后:");
		for (int i = 0; i < num; i++)
		{
			*(ptr + i) = 6;
		}
		for (int i = 0; i < num; i++)
		{
			printf("%d ", *(ptr + i));
		}
		printf("\n");
	}

	free(ptr);   //释放ptr所指向的动态内存
	ptr = NULL;  //防止成为野指针

	system("pause");
	return 0;
}

在这里插入图片描述

 


 
2.3 realloc和free

free同上,这里着重描述一下realloc

realloc()函数对动态开辟的内存空间进行大小调整(扩容/缩容)。

  1. realloc()函数的原型:void * realloc(void *  ptr, size_t size);
    返回调整后的内存起始位置
    ptr:要调整的内存地址
    size:调整后的新大小
  2. 调整原内存空间的大小,并将数据移动到新空间。
  3. 扩容默认用0填充。
  4. 如果realloc中的第一个参数如果为空则和malloc一样。
  5. realloc调整内存空间的两种情况:

情况1:原空间足够大。
原地扩容:直接在原空间后追加空间,原空间数据不变。
原地扩容的释放:只需释放扩容的新空间。还有旧空间没有释放。

情况2:原空间后空间不足。
重新定义新空间:在堆空间上另找一个合适大小的连续空间来使用,并拷贝数据到新空间,立即释放旧内存空间(自动释放,不需要free),返回一个新内存地址。
重新定义新空间的释放:只需释放新空间。旧空间的数据还存在。

如果申请失败,将返回NULL,此时,原来的指针仍然有效。

举例:缩容


#include<stdio.h>
#include<stdlib.h>  // system、realloc和free的头文件
int main()
{
	int num = 0;
	scanf("%d", &num);

	//int arr[num] = { 0 };  错误,这样开辟空间,长度必须是固定值

	int* ptr = NULL;
	ptr = (int*)calloc(num, num * sizeof(int));
	
	printf("calloc:");
	if (NULL != ptr)//判断ptr指针是否为空
	{
		printf("新开辟的空间:");
		for (int i = 0; i < num; i++)
		{
			printf("%d ", *(ptr + i));
		}
		printf("\n");


		printf("填充值后:");
		for (int i = 0; i < num; i++)
		{
			*(ptr + i) = 6;
		}
		for (int i = 0; i < num; i++)
		{
			printf("%d ", *(ptr + i));
		}
		printf("\n");

	}

	printf("对calloc的空间进行缩容:");
	int _num = num - 3; //将空间缩小3个单位
	int* pptr = NULL;
	
	pptr = (int*)realloc(ptr, _num * sizeof(int));
	if (NULL != pptr)
	{
		ptr = pptr;
	}
	printf("缩容后的数据:");
	for (int i = 0; i < _num; i++)
	{
		printf("%d ", *(ptr + i));
	}
	printf("\n");


	free(ptr);   //释放ptr所指向的动态内存
	ptr = NULL;  //防止成为野指针

	system("pause");
	return 0;
}

在这里插入图片描述

举例:扩容

#include<stdio.h>
#include<stdlib.h>  // system、realloc和free的头文件
int main()
{
	int num = 0;
	scanf("%d", &num);

	//int arr[num] = { 0 };  错误,这样开辟空间,长度必须是固定值

	int* ptr = NULL;
	ptr = (int*)calloc(num, num * sizeof(int));
	
	printf("calloc:");
	if (NULL != ptr)//判断ptr指针是否为空
	{
		printf("新开辟的空间:");
		for (int i = 0; i < num; i++)
		{
			printf("%d ", *(ptr + i));
		}
		printf("\n");


		printf("填充值后:");
		for (int i = 0; i < num; i++)
		{
			*(ptr + i) = 6;
		}
		for (int i = 0; i < num; i++)
		{
			printf("%d ", *(ptr + i));
		}
		printf("\n");

	}

	printf("对calloc的空间进行扩容:");
	int _num = num + 3; //将空间扩大3个单位
	int* pptr = NULL;
	
	pptr = (int*)realloc(ptr, _num * sizeof(int));
	if (NULL != pptr)
	{
		ptr = pptr;
	}
	printf("扩容后的数据:");
	for (int i = 0; i < _num; i++)
	{
		printf("%d ", *(ptr + i));
	}
	printf("\n");



	free(ptr);   //释放ptr所指向的动态内存
	ptr = NULL;  //防止成为野指针

	system("pause");
	return 0;
}

在这里插入图片描述

 


 
2.4 总结

常见的内存错误:

  1. 忘记释放不用的动态开辟的空间会造成内存泄漏;
  2. 对空指针的解引用;
  3. 对于动态开辟时的越界访问,不一定会报错,系统对越界的访问:定点抽查
  4. 对非动态开辟的空间进行free;
  5. 对同一块动态开辟的空间进行多次释放;

引申:memset()

int * p1 = (int*)malloc(sizeof(int) * 10);// 堆上开辟
memset(p1, 0, sizeof(int) * 10);// 按字节初始化,只能初始化为全 0 !!!

 


 

3. C++动态内存分配

C语言内存管理方式在C++中可以继续使用,C++也有自己的内存管理方式。
 

3.1 new和delete

 

  1. new和delete的用法:
动态申请一个int类型的空间
int * ptr1 = new int;
int * ptr11 = (int*)malloc(sizeof(int));

delete ptr1;
free(ptr11);


动态申请10int类型的空间
int * ptr3 = new int[10];
int * ptr33 = (int*)malloc(sizeof(int) * 10);

delete[] ptr3;
free(ptr33);


动态申请一个int类型的空间并初始化为10
int * ptr2 = new int(10);
malloc 不能初始化

delete ptr2;


动态申请10int类型的空间并初始化为3
int * ptr4 = new int[10](3);   这是错误的!!!

 

  1. 申请和释放单个元素的空间:用new和delete
    申请和释放连续的空间:用new[ ]和delete[ ]
     
  2. 在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc和free不会。
     
  3. 有了new,malloc还有用吗?
    有用,在不能抛异常的场景下,只能够用malloc。
    在申请基本类型对象时,这两者都是一样的;
    在申请自定义类型对象时,这两者不一样;
    eg:
    Date* p1 = (Date*)malloc(sizeof(Date));// 开空间
    Date* p2 = new Date;// 开空间 + 调用构造函数初始化

 
 

3.2 operator new 和 operator delete

 

3.2.1 原理
  • new和delete是用户进行动态内存申请和释放的操作符。
  • operator new 和operator delete是系统提供的全局函数。
  • new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。operator new调用malloc申请;operator delete调用free释放。
  • new :
    调用operator new函数申请空间
    在申请的空间上执行构造函数,完成对象的构造
  • delete:
    在空间上执行析构函数,完成对象中资源的清理工作
    调用operator delete函数释放对象的空间
  • new Type[n]:
    调用operator new[]函数,在operator new[]中实际调用operator new函数完成n个对象空间的申请
    在申请的空间上执行n次构造函数
  • delete[]:
    在释放的对象空间上执行n次析构函数,完成n个对象中资源的清理
    调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

 

3.2.2 malloc、operator new、new的调用关系:自下向上

在这里插入图片描述

 

3.2.3 问题总结
  • 为什么new调operator new?
    因为operator new 比 malloc 多了一个抛出异常。
     

  • 有没有只适合用malloc而不用new的场景?
    不能抛异常的场景。例如:内存池。(仅仅把空间开出来,用的时候拿出来初始化)
     

  • malloc最大能开出多少空间?
    地址空间限制是有的,但是malloc通常情况下申请到的空间达不到地址空间上限。内存碎片会影响到你“一次”申请到的最大内存空间。比如你有10M空间,申请两次2M,一次1M,一次5M没有问题。但如果你申请两次2M,一次4M,一次1M,释放4M,那么剩下的空间虽然够5M,但是由于已经不是连续的内存区域,malloc也会失败。系统也会限制你的程序使用malloc申请到的最大内存。Windows下32位程序如果单纯看地址空间能有4G左右的内存可用,不过实际上系统会把其中2G的地址留给内核使用,所以你的程序最大能用2G的内存。除去其他开销,你能用malloc申请到的内存只有1.9G左右。

  • malloc是怎么实现的?
    malloc基本的实现原理就是维护一个内存空闲链表,当申请内存空间时,搜索内存空闲链表,找到适配的空闲内存空间,然后将空间分割成两个内存块,一个变成分配块,一个变成新的空闲块。如果没有搜索到,那么就会用sbrk()才推进brk指针来申请内存空间。
    搜索空闲块最常见的算法有:首次适配,下一次适配,最佳适配。
    首次适配:第一次找到足够大的内存块就分配,这种方法会产生很多的内存碎片。
    下一次适配:也就是说等第二次找到足够大的内存块就分配,这样会产生比较少的内存碎片。
    最佳适配:对堆进行彻底的搜索,从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块。

 

 

3.2.4 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在释放空间前会调用析构函数完成空间中资源的清理。
(7) new/delete比malloc和free的效率稍微低点,因为new/delete的底层封装了malloc/free。

 


 

4. 内存泄漏

什么是内存泄漏?
答:指的是因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害是什么?
随着程序的运行,可以使用的百内存就会越来越少,机子就会越来越卡;
普通的小程序,影响可以忽略。但是大程序,对内存要求很大的,例如服务器程序,内存泄漏后,内存的使用度就会越来越多直到耗尽,然后程序挂掉。服务器程序是不可以容忍内存泄漏的,因为服务器程序设计出来就是为了长期正常运行的,任何一点的内存泄漏都会累积起来,服务器最后瘫痪。

如何检测内存泄漏?
Windows平台下面Visual Studio 调试器和 C 运行时 (CRT) 库为我们提供了检测和识别内存泄漏的有效方法,原理大致如下:内存分配要通过CRT在运行时实现,只要在分配内存和释放内存时分别做好记录,程序结束时对比分配内存和释放内存的记录就可以确定是不是有内存泄漏。

如何防止内存泄漏?

  1. 尽早释放无用的对象;
  2. 函数递归时一定要注意函数递归调用的深度,深度过大很有可能会导致栈帧溢出;
  3. 尽量少用静态变量;
  4. 尽量减少不可预测的内存对象;
  5. 尽量使用内存池技术提高系统的性能;
  6. 避免循环中出现申请内存的操作。

 


 

5. 定位new

new表达式,默认下把内存开辟到堆区。
使用定位new表达式,可以在指定地址区域(栈区、堆区、静态区)构造对象,这好比是 把内存开辟到指定区域。

C++中,new有三种用法:
实例化一个对象
实例化一个数组
定位new表达式

原理:在内存中new一块内存空间,然后将大小合适的实例化对象放进该内存空间。
操作:在已申请的内存空间中调用构造函数

附上一个链接:https://www.cnblogs.com/readlearn/p/10806508.html




参考:
https://blog.csdn.net/lihaidong1991/article/details/90075978
https://blog.csdn.net/wz1226864411/article/details/77934941

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值