C语言【动态内存】

1.为什么要有动态内存

我们现在掌握的内存开辟方法有:

c 复制代码
int val = 20;//在栈空间开辟4个字节
char str[10]={0};//在栈空间开辟10个字节的连续的空间

但是上述的方式有两个点要注意:

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

2.数组在申明的时候,一定要指定数组的长度(就算是变长数组也是先给变量赋值,再根据变量的大小,来开辟空间),数组空间一旦确定了大小是不能调整的

但是我们在实际上对于内存的需求方式,绝不仅仅是上述的情况。有时候我们需要的空间大小是在程序运行的时候才能知道,数组是在编译时开辟空间的,这是就不能满足需求了。

这时C语言就引入了动态内存开辟,让程序员可以自己根据需求来申请和释放空间这样就比较灵活。

2.动态内存函数

要想学会动态开辟内存就必须掌握这四个函数malloc free calloc realloc

它们的头文件都为<stdlib.h>
他们开辟空间的大小单位都为字节。

2.1 malloc

函数原型:void* malloc(size_t size);

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

  1. 如果开辟成功,返回一个指向开辟好空间的指针。
  2. 如果开辟失败,则会返回一个NULL指针,因此malloc的返回值一定要做检查。

因为返回类型是void*,所以malloc函数并不知道开辟空间时是什么类型,在实际使用时类型是有使用者决定的。

如果参数size为 0,malloc的行为是标准未定义的,结果取决于编译器。

代码如下:

c 复制代码
#include<stdio.h>
#include<stdilb.h>
int main()
{
	int* parr = (int*)malloc(10 * sizeof(int));
	if (parr == NULL)
	{
		perror("malloc");
		exit(1);
	}
	for (int i = 0; i < 10; i++)
	{
		//初始化
		*(parr + i) = i + 1;
		//打印
		printf("%d ", *parr + i);
	}

	return 0;
}

注意:malloc开辟的空间并未进行初始化,里面是随机值

c 复制代码
#include<stdio.h>
#include<stdilb.h>
int main()
{
	int* parr = (int*)malloc(10 * sizeof(int));
	if (parr == NULL)
	{
		perror("malloc");
		exit(1);
	}
	for (int i = 0; i < 10; i++)
	{
		//初始化
		*(parr + i) = i + 1;
		//打印
		printf("%d ", *parr + i);
	}

	return 0;
}
c 复制代码
#include<stdio.h>
#include<stdilb.h>
int main()
{
	int* parr = (int*)malloc(10 * sizeof(int));
	if (parr == NULL)
	{
		perror("malloc");
	}
	for (int i = 0; i < 10; i++)
	{
		//未进行初始化
		//打印
		printf("%d ", *parr + i);
	}

	return 0;
}

结果如下:

这些动态开辟的空间是存放在哪里呢?

如图:

2.2 free

申请了空间在使用完的时候肯定是要还回去的嘛,那怎么还呢?

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

  • 如果参数ptr指向的空间不是动态内存的时候,free函数的行为是未被定义的
  • 如果参数ptrNULL指针,那么free函数什么都不会做

注意:传递给free函数的参数是要被释放空间的起始地址

代码如下:

c 复制代码
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* parr = (int*)malloc(10 * sizeof(int));
	if (parr == NULL)
	{
		perror("malloc");
	}
	for (int i = 0; i < 10; i++)
	{
		//初始化
		*(parr + i) = i + 1;
		//打印
		printf("%d ", *parr + i);
	}

	//只用完动态申请的空间要进行释放
	free(parr);

	parr = NULL;
	return 0;
}

​free仅仅是将空间的使用权限还给了操作系统;

​但parr还指向原来的地址,这就成为野指针;

为了避免成为野指针,及时将parr置为NULL。

如果一直不用函数free来释放申请的空间,这样很可能会造成内存泄漏!!!

2.3 calloc

C语言还提供了一个函数叫calloccalloc函数也用来动态内存分配。原型如下:
void * calloc(size_t num, size_t size);

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

由于函数malloc比函数calloc少一步(将内存初始化),所以malloccalloc更快,如果更追求效率的话可以使用malloc,如果不想自己初始化的话可以使用calloc

代码如下:

c 复制代码
int main()
{
	int* pca = (int*)calloc(10,sizeof(int));
	if (pca == NULL)//判断是否开辟失败
	{
		perror("calloc");
		exit(1);//直接退出程序
	}
	printf("calloc: ");
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *pca + i);
	}
	printf("\n\n");


	int* pma = (int*)malloc(10 * sizeof(int));
	if (pma == NULL)
	{
		perror("malloc");
		exit(1);
	}
	printf("malloc: ");
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *pma + i);
	}

	return 0;
}

结果如图:

2.4 realloc

realloc函数能让动态内存管理更加灵活。

有些时候我们会发现我们申请的空间太小了,有时候又感觉申请的空间太大了,那么为了合理的使用空间,我们就会对申请的空间大小进行灵活的调整,那么realloc函数就能做到对动态空间大小的调整。

函数原型如下:
void* realloc(void* ptr, size_t size)

  • ptr是要被调整的内存地址
  • size是调整之后该内存的大小
  • 返回值为调整之后的内存空间的起始位置

2.4.1 realloc在调整内存空间有三种情况

  1. 原空间后面有足够的空间用来调整。
  2. 原空间后面没有足够空间用来调整,扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
  3. 空间调整失败,返回NULL(这是最坏的情况)
    如图所示

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

int main()
{
	//开辟
	int* parr = (int*)malloc(5 * sizeof(int));
	if (parr == NULL)
	{
		perror("malloc");
	}
	//使用
	for (int i = 0; i < 5; i++)
	{
		*(parr + i) = i + 1;
	}
	//假设要多插入5个int类型的数据
	//这时空间就不够了,需要调整

	
	int* parr = (int*)realloc(parr, 10 * sizeof(int));
	//上述代码合适吗
	//不合适,因为万一调整失败,会返回NULL
	//这时我原来开辟的空间就找不到了

	//所以正确的方法应该如下
	//先申请调整空间
	int* ptr = (int*)realloc(parr, 10 * sizeof(int));
	//调整成功
	if (ptr != NULL)
	{
		//将申请的空间地址赋给parr(如果是情况二,parr的原空间已经被释放了)
		parr = ptr;
	}
	else
	{
		perror("realloc");
	}
	//处理要求......

	free(parr);
	return 0;
}

上述代码int* parr = (int*)realloc(parr, 10 * sizeof(int));代码合适吗?
完全不合适!!! 因为万一调整失败,会返回NULL;

这时我原来开辟的空间就找不到了。

所以正确的realloc使用方式是先创建一个指针变量来接收开辟的空间,如果判断ptr是否开辟成功,如果开辟成功。将ptr赋给parr

如果开辟失败parr指向的原空间也能继续使用。

补充:realloc也可以完成malloc的功能;

如果传的是NULL指针,realloc就会开辟一块新空间。

3.常见的动态内存的错误

3.1对NULL指针的解引用

具体如下:

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

int main()
{
	int* ptr = (int*)malloc(INT_MAX);

	*ptr = 20;//如果ptr是NULL,就会有问题

	return 0;
}

解决方法:开辟空间后,要第一时间判断是否空

3.2对动态开辟空间进行越界访问

具体如下:

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

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

int main()
{
	test();

	return 0;
}

解决方法:控制好访问的范围

3.3对非动态开辟空间使用free释放

具体如下:

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

int main()
{
	int a = 0;
	int* p = &a;
	//处理......
	free(p);
	//这样行吗,肯定不行!!!
	//a是局部变量,他开辟的空间是放在栈区的,而不是堆区的
	//free释放的是realloc、malloc、calloc开辟的空间(动态内存空间是在堆区的)
	return 0;
}
复制代码
//这样行吗,肯定不行!!!
//a是局部变量,他开辟的空间是放在栈区的,而不是堆区的
//free释放的是realloc、malloc、calloc开辟的空间(动态内存空间是在堆区的)

编译都无法编译过去

解决方法:不要对非动态开辟空间进行释放!!!

3.4使用free释放的并不是动态开辟空间的起始位置

具体如下:

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

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

同样也无法编译过去

解决方法:传给free的时候确保是空间的起始地址

3.5对同一块内存进行对此释放

具体如下:

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

int main()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);
	return 0;
}

同样还是无法编译过去

解决方法:在第一次释放后及时的给p赋NULL

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

int main()
{
	int* p = (int*)malloc(100);
	free(p);
	//解决方法
	p = NULL;

	free(p);
	return 0;
}

前面也说过了,如果传给free函数的是NULLfree不会进行如何的操作。

3.6忘记释放动态开辟空间(内存泄漏)

具体如下:

c 复制代码
void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();//出来后test就被销毁了,就无法找到p所指向的空间了
	 
	while (1);
}

忘记释放不再使用的空间会造成内存泄漏

释放动态内存有两个方法:

  1. 在不用的时候使用free函数进行释放

  2. 如果一直没有被free释放,当程序运行结束后,会由操作系统来回收

    解决方法:

    1.谁申请的空间谁释放,如果在函数里,那么在出函数前记得释放malloc/calloc/realloc要和free成对出现。

    2.如果不能释放(后续会用到),要告诉下一个使用者,记得释放动态开辟空间。

切记:动态开辟的空间⼀定要释放,并且正确释放。

结语

最后感谢您能阅读完此片文章,如果有任何建议或纠正欢迎在评论区留言。如果您认为这篇文章对您有所收获,点一个小小的赞就是我创作的巨大动力,谢谢!!!

相关推荐
Scott9999HH8 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社9 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海9 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖10 小时前
JDK 26 新特性详解
java·开发语言
马优晨11 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区12 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大13 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai13 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜13 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa14 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio