动态内存管理(上)

一、 为什么要有动态内存分配

我们已经掌握的内存开辟方式有:

int val = 20;// 在栈空间上开辟四个字节

char arr10 = {0};// 在栈空间上开辟 10 个字节的连续空间

但是上述的开辟空间的方式有两个特点:

1.空间开辟大小是固定的。

2.数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整

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

二、 malloc和free

2.1 malloc

C语言提供了一个动态内存开辟的函数:

void* malloc (size_t size);

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

2.2 free

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

void free (void* ptr);

free函数用来释放动态开辟的内存。如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。如果参数 ptr 是NULL指针,则函数什么事都不做。 malloc和free都声明在 stdlib.h 头文件中。 举个例子:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
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) = 0;
		}

	}
	free(ptr);//释放ptr所指向的动态内存

	ptr = NULL;//是否有必要?

	return 0;
}

最后一句 ptr = NULL; 从程序运行角度来说并不是必须的,但在实际开发中推荐保留。因为 free(ptr); 只是释放了指针所指向的动态内存,并不会自动把指针变量本身清空,释放后 ptr 仍然保存着原来的地址,此时它就变成了悬空指针(野指针)。如果后续代码中不小心再次使用这个指针,比如解引用或再次调用 free,就可能导致程序崩溃或出现未定义行为。将指针置为 NULL 可以有效避免误用,因为对 NULL 指针的再次 free 是安全的。

三、calloc和realloc

3.1 calloc

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

void* calloc (size_t num, size_t size);

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

c 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (NULL != p)
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}

代码中通过 calloc(10, sizeof(int)) 申请空间,与 malloc 不同的是,calloc 在分配内存的同时会把这块内存全部初始化为 0,因此后面的循环打印时,每个元素默认都是 0。程序先判断指针 p 是否为空,避免申请失败时发生非法访问;如果申请成功,就通过指针遍历的方式依次输出数组中的值。最后调用 free(p) 释放动态内存,防止内存泄漏,并把指针置为 NULL,以避免形成悬空指针。

输出结果:

0 0 0 0 0 0 0 0 0 0

所以如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。

3.2 realloc

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

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

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

realloc在调整内存空间的是存在两种情况:

1.原有空间之后有足够大的空间

2.原有空间之后没有足够大的空间

​编辑

1:当是1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

2:当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适da'x的连续空间来使⽤。这样函数返回的是一个新的内存地址。

由于上述的两种情况,realloc函数的使用就要注意一些。

arduino 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* ptr = (int*)malloc(100);
	if (ptr != NULL)
	{
		//业务处理

	}
	else
	{
		return -1;
	}
	//扩展容量
	//代码1 -直接将realloc的返回值放到ptr中

	ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)

	//代码2 -先将realloc函数的返回值放在p中,不为NULL,在放ptr中

	int* p = NULL;
	p = realloc(ptr, 1000);
	if (p != NULL)
	{
		ptr = p;
	}
	//业务处理

	free(ptr);
	return 0;
}

这段代码的关键问题在于对 realloc 返回值的处理。代码1中直接写成
ptr = (int*)realloc(ptr, 1000); 在语法上是可以的,但存在风险:如果扩容失败,realloc 会返回 NULL,而原来的内存块并不会被释放。此时已经把原来的地址覆盖掉了,既拿不到新空间,又丢失了旧空间的指针,最终会造成内存泄漏,而且后续再使用 ptr 还可能发生空指针错误。

相比之下,代码2是更安全、更规范的写法。先用一个临时指针 p 接收 realloc 的返回值,如果 p 不为 NULL,说明扩容成功,再把它赋给 ptr;如果为 NULL,原来的 ptr 仍然有效,可以决定是否继续使用或释放。

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

4.1 对NULL指针的解引用操作

void test()

{

int* p = (int*)malloc(INT_MAX / 4);

*p = 20;//如果p的值是NULL,就会有问题

free(p);

}

正确做法是在使用指针之前先判断是否申请成功,例如:

int* p = (int*)malloc(INT_MAX / 4);

if (p == NULL) {

// 处理申请失败,返回或报错

return;

}

*p = 20;

free(p);

p = NULL;

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

void test()

{

int i = 0;

int* p = (int*)malloc(10 * sizeof(int));

if (NULL == p)

{

exit(EXIT_FAILURE);

}

for (i = 0; i <= 10; i++)

{

*(p + i) = i;//当i是10的时候越界访问

}

free(p);

}

for 循环条件写成了 i <= 10,当 i == 10 时,表达式 *(p + i) 会访问到第 11 个元素的位置,已经超出了申请的内存范围,属于未定义行为,可能导致数据破坏或程序崩溃。

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

void test()

{

int a = 10;

int* p = &a;

free(p);//ok?

}

free 只能释放由动态内存分配函数(如 malloccallocrealloc)申请的内存,而代码中的 p 指向的是局部变量 a 的地址,这块内存位于栈区,不是动态分配的。如果对它调用 free(p),就会产生未定义行为,常见结果是程序崩溃或运行异常。

4.4 使用free释放一块动态开辟内存的一部分

void test()

{

int* p = (int*)malloc(100);

p++;

free(p);//p不再指向动态内存的起始位置

}

free 必须接收最初由 malloc(或 calloc、realloc)返回的那个原始指针值,而代码中在 p++ 之后,p 已经不再指向动态内存块的起始地址,而是指向中间位置。此时调用 free(p) 属于未定义行为,常见结果是程序崩溃或内存管理错误。

4.5 对同一块动态内存多次释放

void test()

{

int* p = (int*)malloc(100);

free(p);

free(p);//重复释放

}

free(p); 第一次调用后,这块动态内存已经被释放,指针 p 变成了悬空指针;如果再次对同一个指针调用 free,就会产生未定义行为,常见结果包括程序崩溃、堆结构损坏或安全漏洞。

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

void test()

{

int* p = (int*)malloc(100);

if (NULL != p)

{

*p = 20;

}

}

int main()

{

test();

while (1);

}

函数 test 中通过 malloc(100) 动态申请了一块堆内存,但在函数结束前并没有调用 free 进行释放,因此这块内存会一直占用着,无法被回收。尤其是在 main 中又进入了死循环 while (1);,程序不会结束,操作系统也不会帮你回收这块内存,泄漏会一直存在。

相关推荐
caimouse2 小时前
reactos编码规范
c语言·开发语言
星辰徐哥7 小时前
Spring Boot 微服务架构设计与实现
spring boot·后端·微服务
星辰徐哥7 小时前
Spring Boot 数据导入导出与报表生成
spring boot·后端·ui
明夜之约7 小时前
Spring Boot 自动装配源码
java·spring boot·后端
Leaton Lee7 小时前
Spring Boot分层架构详解:从Controller到Service再到Mapper的完整流程
java·spring boot·后端·架构
Micro麦可乐7 小时前
Spring Boot 实战:从零设计一个短链系统(含完整代码与数据库设计)
数据库·spring boot·后端·哈希算法·雪花算法·短链系统
Jinkxs7 小时前
Resilience4j- 与 Spring Boot 快速集成:自动配置与基础注解使用
java·spring boot·后端
毕设源码_郑学姐7 小时前
计算机毕业设计springboot网络相册设计与实现 基于Spring Boot框架的在线相册管理系统开发与应用 Spring Boot驱动的网络影集设计与实践
spring boot·后端·课程设计
辣机小司7 小时前
【踩坑记录:Spring Boot 配置文件读取值不一致?警惕 YAML 的“八进制陷阱”与 SnakeYAML 版本之谜】
java·spring boot·后端·yaml·踩坑记录
码农阿豪7 小时前
从零到一:Spring Boot快速接入金仓数据库实战
数据库·spring boot·后端