C语言进阶——动态内存管理

本文详细介绍了C语言中的动态内存管理函数malloc、free、calloc、realloc,强调了它们的使用、错误处理以及柔性数组的概念。文章着重讨论了内存申请、释放的正确实践,以及常见的内存管理错误,如NULL指针操作、越界访问、内存泄漏等。
摘要由CSDN通过智能技术生成


今天给大家讲解动态内存管理函数:

  1. malloc
  2. free
  3. calloc
  4. realloc

一、为什么存在动态内存管理

在介绍这几个函数之前,我们先来回忆一下已经掌握的向内存中开辟空间的方式:

int main()
{
	int a;//向内存申请一个4字节的空间
	int arr[10];//向内存申请一个40个字节的空间
	return 0;
}

int a,整型类型开辟4字节的空间,int arr[10],10个整型类型元素的数组,开辟40个字节的空间,这样向内存中申请空间有什么缺点呢?

  1. 空间开辟大小是固定的。
  2. 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

只能申请固定字节的空间,并不能根据需求随时变动,不灵活。
比如在实现通讯录的过程中,我们并不知道将来会存放多少个联系人,如果一次性给100个,可能会给太多了,浪费内存空间,也可能给的不够用,将来某天存不下了。
那么动态内存管理函数,就可以向内存中申请我们想要字节数的空间,了解了动态内存管理函数的作用,下面给大家进行讲解。

二、动态内存管理函数的介绍

1. malloc函数

malloc和free都声明在 stdlib.h 头文件中

1.1 malloc的介绍

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

void* malloc (size_t size);//动态开辟size个字节空间

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  1. 如果开辟成功,则返回一个指向开辟好空间的指针。
  2. 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  3. 返回值的类型是 void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  4. 如果参数 size为0,malloc的行为是标准是未定义的,取决于编译器。
    这里需要说明:当使用malloc 的时候,一定说明我们需要向内存中申请一块空间。那当传入的size为0时,就说明你想申请0个空间,这种做法是不提倡的。

1.2 malloc的使用

假设我们想申请40个字节的空间,并且以整型的方式访问。

int* p = (int)malloc(40);
  1. malloc的参数为:想向内存申请空间的字节数,也就是40字节。
  2. malloc的返回值:是指向这40个字节起始地址的指针,类型为void*

作为malloc函数的使用者,我们应该清楚申请这块空间是做什么的,所以我们把malloc的返回值强制类型转化为int* 类型,把申请到的空间的起始地址保存到int* 类型的 p 指针中去

2. free函数

2.1 free的介绍

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

void free (void* ptr);

free函数用来释放动态开辟的内存。

  1. 如果参数 ptr 是NULL指针,则函数什么事都不做。
  2. 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
    例如下面的指针p所指向的空间不是动态开辟的,free释放后会导致程序崩溃。(标准未定义行为)后面还会提到
    在这里插入图片描述

2.2 动态申请内存空间的完整流程

一个好的习惯,动态申请的内存空间使用完以后,需要使用free释放。

int main()
{
	int* p = (int*)malloc(40);
	if ( p == NULL)//如果 p为空指针,说明malloc开辟空间失败
	{
		perror("malloc");
		//输出错误原因,perror内部输入malloc
		//方便后续我们可以从错误信息中知道是malloc这里出现错误
		return 1;
	}
	//开辟成功
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i + 1;
		printf("%d\n", p[i]);
	}
	//malloc申请的内存空间,当程序退出时,才还给操作系统
	//程序不退出时:动态申请的内存空间,不会主动还给操作系统
	//用完以后需要手动使用free释放空间
	free(p);//把p指向的(动态申请的)空间,释放掉
	//释放空间时,free不会把指向动态申请的空间的指针置为空指针
	p = NULL;//把p置为空指针,否则p成为野指针
	return 0;
}

3. calloc函数

3.1 calloc的介绍

calloc 函数也用来动态内存分配。原型如下:

void* calloc (size_t num, size_t size);
  1. 函数的功能是为 num 个大小为 size(单位是字节) 的元素开辟一块空间,并且把空间的每个字节初始化为0。
  2. 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

3.2 calloc的使用

int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if ( p == NULL )
	{
		perror("calloc");
		return 1;
	}
	//开辟成功
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i + 1;//访问每个整型并赋值
		printf("%d\n", p[i]);
	}
	free(p);
	p = NULL;
	return 0;
}

这里我们可以看到p指向的(10个整型大小的)空间,被初始化成0。
所以如果我们对申请的内存空间的内容要求初始化,那么可以使用calloc函数来完成任务。
在这里插入图片描述

4. realloc函数

4.1 realloc的介绍

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

void* realloc (void* ptr, size_t size);
  1. ptr 是要调整的内存地址
  2. size 调整之后新大小
  3. 返回值为调整之后的内存起始位置。
  4. realloc在调整内存空间成功时存在两种情况:

情况1:原有空间之后有足够大的空间
直接将原空间扩容至新空间的大小,返回原空间的起始地址。

情况2:原有空间之后没有足够大的空间

如果原空间之后的空间不够调整为新的空间大小,这个函数会进行如下操作:

  1. 找一块新的空间一次性开辟出新空间的大小
  2. 将原来内存中的数据移动到新的空间
  3. 释放旧的空间
  4. 返回新空间的起始地址。

在这里插入图片描述
如果调整内存空间失败,也就是找不到任何一块空间,能存放的下新的空间大小,那么realloc函数将返回空指针。

4.2 realloc的易错点

那么我们来看下面这段代码:

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//开辟成功
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i + 1;//初始化1~10
	}
	//增加一些空间
	p = (int*)realloc(p, 80);//这样写合适吗?
	//释放空间
	free(p);
	p = NULL;
	return 0;
}

增加空间时,把realloc的返回值,直接赋值给维护原空间的指针p怎么样呢?
上面我们提到,当调整空间失败时,realloc将返回空指针。
那么当realloc返回值为NULL时,将NULL赋值给指针p,访问原空间的指针就消失了,原来动态申请的空间,再也访问不到了。
(本来p指向40个字节的空间,realloc调整失败后,返回空指针,p原来指向的40个字节的空间也找不到了)

这时就产生了两个问题:

  1. 内存泄漏,原来动态申请的空间,既没有被free释放掉,又没有办法使用。(原来申请的空间既用不到,也找不见,这是一个很严重的问题)
  2. 数据丢失,原空间无法访问,里面的数据相当于丢失了。

4.3 realloc的正确使用

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//开辟成功
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i + 1;//初始化1~10
	}
	//增加一些空间
	p = (int*)realloc(p, 80);//这段代码这样写是不合适的
	//正确写法:
	//单独定义一个指针接收realloc的返回值
	int ptr = (int*)realloc(p, 80);
	if (ptr != NULL)//如果ptr不为空,再将ptr赋值给p
	{
		p = ptr;
	}
	else
	{
		perror("realloc");//ptr为空,说明调整空间失败,打印错误信息
		return 1;
	}
	//释放空间
	free(p);
	p = NULL;
	return 0;
}

这种写法就巧妙的避免了上述提到的两个问题。

5. 常见的动态内存错误

5.1 对NULL指针的解引用操作

这种写法没有考虑到:
malloc向内存申请空间可能失败,返回空指针。

void test()
{
	int *p = (int *)malloc(INT_MAX/4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
}

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

这种写法跟数组越界访问类似:
只申请了10个整型大小的空间,却访问11个整型大小。

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);
}

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

这种写法是标准未定义行为,前面提到过:
会导致程序崩溃。

void test()
{
	int a = 10;
	int *p = &a;
	free(p);//ok?
}

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

这种写法导致p指针不再指向原空间的起始地址,p指针发生了移动。
只释放了p指针指向的后面的空间,p指针前面的空间没有被free释放掉,造成了内存泄漏。

void test()
{
	int *p = (int *)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}

在这里插入图片描述

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

这种写法对已经释放过的动态内存又一次释放,会导致程序崩溃。

void test()
{
	int *p = (int *)malloc(100);
	free(p);
	free(p);//重复释放
}

在这里插入图片描述

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

这种写法和realloc导致的内存泄漏类似,都是内存泄漏。
动态开辟空间后,没有使用free进行释放,程序一直不退出,被泄漏的内存无法使用。

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

void test()
{
	int *p = (int *)malloc(100);
	if(NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();//动态开辟空间后,没有使用free进行释放
	while(1);//程序一直不退出,被泄漏的内存无法使用
}

6. 柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员

6.1 柔性数组的两种写法

//方法一:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
//方法二:
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;

这两种写法,有些编译器会选择其一进行报错、无法编译,两种方法肯定有一种是可行的。

6.2 柔性数组的特点

  1. 结构中的柔性数组成员前面必须至少一个其他成员。(也可以多个成员)
  2. sizeof 返回的这种结构的大小时,不包括数组的大小。
//code1
typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//计算柔性数组大小时,输出的是4,不包括最后的数组大小
  1. 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
    malloc申请动态内存时,应该申请:
    (柔性数组其他成员的总大小+最后一个数组预期需要的空间)的总和
//代码1
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));

在这里插入图片描述

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

6.3 为什么使用柔性数组

上述的 type_a 结构也可以设计为:

//代码2
typedef struct st_type
{
	int i;
	int* p_a;
}type_a;
int main()
{
	type_a* p = (type_a*)malloc(sizeof(type_a));
	p->i = 100;
	p->p_a = (int*)malloc(p->i * sizeof(int));
	//初始化0~99
	for (p->i = 0; p->i < 100; p->i++)
	{
		p->p_a[p->i] = p->i;
	}
	//释放空间
	free(p->p_a);
	p->p_a = NULL;
	free(p);
	p = NULL;
	return 0;
}

这里malloc两次,和6.2中的开辟方式,达成了类似的效果。
不需要柔性数组也能达成柔性数组的效果,那么为什么还要用柔性数组呢?

在这里插入图片描述
上述 代码1 和 代码2 可以完成同样的功能,但是 代码1 (也就是柔性数组)的实现有两个好处:

  1. 第一个好处是:方便内存释放,如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
  2. 第二个好处是:有利于访问速度,连续的内存有益于提高访问速度,也有益于减少内存碎片。(但其实,对效率的提升并不大,因为依然避免不了要用做偏移量的加法来寻址)

以上就是本篇文章的全部内容了,感谢你的观看,希望能对你有所帮助,谢谢!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值