【C语言】动态内存管理

文章目录


前言

malloc、calloc、realloc 和 free这些动态内存函数都声明在 stdlib.h 头文件中。
一、动态内存管理的意义
二~三、malloc和calloc函数的对比;malloc、calloc、realloc函数与free结合使用;realloc函数在调整内存空间时的不同情况
四、常见的动态内存的错误
五、C语言中程序内存区域划分(栈区、堆区、静态区和代码段)


一、动态内存管理的意义

在未学习动态内存的知识之前,我们一般会使用以下方式来在内存中开辟空间:

int val = 20;//在内存空间上开辟四个字节
char arr[10] = {0};//在内存空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:
• 空间开辟大小是固定的。
• 数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知
道,那数组在编译时就开辟好空间的方式就不能满足要求。
于是C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。

二、malloc和free

1.malloc

C语言提供了⼀个动态内存开辟的函数,函数原型如下:

void * malloc (size_t size);

这个函数向内存申请⼀块连续可用的空间,并返回指向这块空间的指针。
• 如果开辟成功,则返回⼀个指向开辟好空间的指针。
• 如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
• 返回值的类型是 void * ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
• 如果参数 size 为 0,malloc的行为是标准是未定义的,取决于编译器。

 malloc函数的使用示例:
     int* p = (int*)malloc(10*sizeof(int));  
 //用malloc函数动态开辟一块大小为10个整形(40个字节)的内存空间
 //如果开辟成功,返回一个指向开辟好空间的指针,指针的类型是void*
 //我们想用int*类型的指针p来接收返回值,所以将返回的void*类型指针强制类型转换成int*

2.free

C语言提供了另外⼀个函数free,是专门用来做动态内存的释放和回收的,函数原型如下:

void free (void * ptr);

free函数是专门用来释放动态开辟的内存的。
• 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
• 如果参数 ptr 是NULL指针,则free函数不进行任何操作,程序也不会报错。

 free函数的使用示例:
        free(p);            //释放指针变量p所指向的已分配的动态空间

注:动态开辟了一块空间,但使用完这块空间之后忘记使用free函数释放这块空间,这并不代表这块内存空间就不回收了,当程序退出的时候,系统会自动回收这块内存空间。
但是为了提高代码的运行效率,最好动态开辟的空间使用完之后就立刻用free函数释放掉,并且正确释放,否则在代码运行过程中会造成内存空间被浪费的情况。

示例(malloc函数是要和free函数组合使用的):

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
	//用malloc函数动态开辟一块大小为10个整形(40个字节)的内存空间
	int* p = (int*)malloc(10 * sizeof(int));
	//检查malloc函数返回的地址是否有效(不是空指针才有效)
	if (p == NULL)
	{
		perror("malloc");//打印错误信息
		exit(1);//exit()函数的作用是提前退出程序,只要括号内数字不为0都表示异常退出
	}
	//使用动态开辟的空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i;//相当于 *(p+i)=i;
		printf("%d ", p[i]);
	}
	printf("\n");
	//回收动态开辟的空间
	free(p);
	p = NULL;//p指向的空间已被释放,交还内存,所以要将p置为NULL,防止其访问不属于该程序的内存
	return 0;
}

三、calloc和realloc

1.calloc

C语言还提供了⼀个函数叫 calloc , calloc 函数也用来动态内存分配,函数原型如下:

void * calloc (size_t num, size_t size);

• 函数的功能是为 num 个大小为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。
• 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

 calloc函数的使用示例:
     int* p = (int*)calloc( 10, sizeof(int));  
 //用calloc函数动态开辟一块大小为10个整形(40个字节)的内存空间

示例(malloc和calloc函数动态开辟空间时的区别):

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
	int* p1 = (int*)malloc(10 * sizeof(int));//malloc不会将动态开辟的空间中的数据初始化
	int* p2 = (int*)calloc(10, sizeof(int));//calloc在返回地址之前把申请的空间的每个字节初始化为全0

	free(p1);
	free(p2);
	p1 = p2 = NULL;
	return 0;
}

2.realloc

realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们⼀定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小进行调整。
函数原型如下:

void * realloc (void * ptr, size_t size);

• ptr 是要调整的内存地址
• size 是调整之后的新大小
• 返回值为调整之后的内存起始位置。
• 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

 realloc函数的使用示例:
     int* p = (int*)calloc(10, sizeof(int));
     //想将p指向的动态空间加大到20个int的空间,用realloc函数调整空间
     int* ptr = (int*)realloc(p, 20*sizeof(int));

realloc在调整内存空间的时候是存在两种情况的:
(1) 情况一:原有空间之后有足够大的空间
(2) 情况二:原有空间之后没有足够大的空间

示例(研究realloc调整内存空间时的两种不同情况):

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
	//用calloc函数动态开辟一块大小为10个整形(40个字节)的内存空间
	int* p = (int*)calloc(10, sizeof(int));
	//检查calloc函数返回的地址是否有效(不是空指针才有效)
	if (p == NULL)
	{
		perror("calloc");
		exit(1);
	}
	//使用动态开辟的空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i;
	}
	//想将p指向的动态空间加大到20个int的空间,用realloc函数调整空间
	int* ptr = (int*)realloc(p, 20 * sizeof(int));
	//检查空间调整是否成功,当ptr!=NULL,证明调整成功,再将调整之后的地址赋给p
	if (ptr != NULL)//当ptr==NULL,证明调整失败,就不能把ptr赋给p,否则会丢失之前已开辟空间的地址
	{
		p = ptr;
	}
	//回收动态开辟的空间
	free(p);
	p = NULL;//p指向的空间已被释放,交还内存,所以要将p置为NULL,防止其访问不属于该程序的内存
	return 0;
}

情况一(原有空间之后有足够大的空间):
原有空间之后有足够大的空间,要扩展内存就直接在原有空间之后追加空间,原来空间的数据不发生变化。

情况二(原有空间之后没有足够大的空间):
原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样realloc函数返回的是⼀个新的内存地址。
(原有空间中的数据会移动到新开辟的空间,然后原有空间会被释放还给系统)

四、 常见的动态内存的错误

1.对NULL指针的解引用操作

c 复制代码
int main()
{
	int* p = (int*)malloc(INT_MAX*10);
	*p = 20;//如果p的值是NULL,对NULL进行解引用就会报错
	free(p);
	return 0;
}

C/C++中的 <limits.h> 头文件中定义:
#define INT_MAX 2147483647
INT_MAX为 2^31-1 ,即 2147483647 ;

当malloc要开辟的动态空间过大的时候,动态空间的开辟可能会失败,开辟失败就会返回⼀个 NULL 指针。

以上代码中,我们想用malloc开辟一个(INT_MAX*10)个字节的动态空间,这个空间过于庞大,所以开辟失败,返回的是NULL,而对NULL进行解引用是会报错的。所以我们要对上述代码进行改进,在使用malloc开辟了动态空间之后,对malloc返回的指针进行检查,如下:

c 复制代码
int main()
{
	int* p = (int*)malloc(INT_MAX * 10);
	//检查malloc函数返回的地址是否有效(不是空指针才有效)
	if (p == NULL)
	{
		perror("malloc");//打印错误信息
		exit(1);//exit()函数的作用是提前退出程序,只要括号内数字不为0都表示异常退出
	}
	*p = 20;
	free(p);
	return 0;
}


如果p为NULL,证明malloc函数开辟空间失败,我们使用perror函数打印错误信息,可以看到错误原因正是:"没有充足空间",然后用exit函数提前退出程序,因为空间开辟都失败了,后续对开辟的动态空间进行的操作就无需进行了。

2.对动态开辟空间的越界访问

c 复制代码
int main()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));//动态开辟了10个整形的内存空间
	if (p == NULL)
	{
		perror("malloc");
		exit(1);
	}
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;//循环进行了11次,当访问第11个整形空间时越界访问,程序报错
	}
	free(p);
	p = NULL;
	return 0;
}

3.对非动态开辟内存使用free释放

c 复制代码
int main()
{
	int a = 10;
	int* p = &a;
	free(p);//free只能释放malloc、calloc和realloc动态开辟的空间
	return 0;//对非动态开辟的内存使用free会报错
}

4.使用free释放⼀块动态开辟内存的⼀部分

c 复制代码
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		exit(1);
	}
	p++;//p不再指向动态开辟空间的起始位置
	free(p);//试图用free释放一块动态开辟空间的一部分,程序报错
	p = NULL;
	return 0;
}

5.对同⼀块动态内存多次释放

c 复制代码
int main()
{
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		perror("malloc");
		exit(1);
	}
	free(p);
	free(p);//对同⼀块动态内存多次释放,程序报错
	return 0;
}

6.动态开辟内存忘记释放(内存泄漏)

c 复制代码
void test()
{
	int* p = (int*)malloc(100);//动态开辟了一个100字节的空间之后,p中存储了该空间的地址,
	if (p == NULL)             //test()函数调用结束后,指向该空间的指针p被销毁,
	{                          //后续再也找不到此空间,动态空间占用了内存,却无法被使用,
		perror("malloc");      //内存被浪费,导致了内存泄漏
		exit(1);
	}
	*p = 20;
}

int main()
{
	test();
	int arr[100] = { 0 };
	int i = 0;
	for (i = 0; i < 100; i++)
	{
		arr[i] = i;
	}
	return 0;
}

忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间使用完之后⼀定要释放,并且正确释放。

五、C语言中程序内存区域划分


C程序运行时,操作系统将内存划分为以上图示中的4个区域:

1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):一般由程序员(使用malloc、calloc、realloc和free函数)分配释放, 若程序员不释放,程序结束时由操作系统回收 。
3. 静态区(数据段):存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体的二进制代码。


相关推荐
Oneforlove_twoforjob1 分钟前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
向宇it18 分钟前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行20 分钟前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
诚丞成43 分钟前
计算世界之安生:C++继承的文水和智慧(上)
开发语言·c++
星河梦瑾1 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富1 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想1 小时前
JMeter 使用详解
java·jmeter
言、雲1 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
TT哇1 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
东风吹柳2 小时前
观察者模式(sigslot in C++)
c++·观察者模式·信号槽·sigslot