【C语言进阶】之动态内存管理


📃博客主页: 小镇敲码人
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞

1.为什么我们需要动态内存管理

我们经常开辟内存有如下两种方法:

#include<stdio.h>

int main()
{
	int a = 0;
	int b[20] = { 0 };
	return 0;
}

这两种开辟内存的方法都有如下特点:

  1. 空间是固定的。
  2. 开在栈区。
  3. 数组需要指定大小。

但是我们在C语言刷题时,可能会遇见数组的大小在程序运行中输入了才知道的情况,如果直接开一个比较大的数组就很浪费空间,这个时候就需要用到动态内存管理。

另外,为什么要提到它是开在栈上的空间呢?因为栈上面开的空间它有一个特点,函数生命周期结束,它里面开的临时变量和固定大小的数组的内存系统也就回收了,我们如果想在一个非main函数里面开一块空间,要达到这个函数结束我的空间还在,没有被系统回收的目的,就需要动态内存管理函数的使用,因为其是在堆上开的空间,在堆上开的空间有一个特点,除非你手动释放,或者main函数结束,否则你的系统是不会回收这片空间的。

2.动态内存管理的函数介绍

内存管理函数有一个共同的头文件,stdlib.h

2.1malloc函数和free函数

2.1.1malloc函数

C语言提供了一个叫做malloc的函数,它的函数定义是这样的:

void* malloc(size_t size) ;

因为编译器不知道你要在堆上开辟哪个类型的空间,所以它的返回值就设为万能指针void *

因为开辟内存肯定返回值是一个地址,但是可以不指明地址的类型,我们在指针进阶篇谈到过,使用void*指针前是必须强制类型转换为我们需要的类型,这个是程序员自己控制的。

至于这个函数的一个参数,自然是你想开辟内存的大小,单位是字节,有人可能想问,如果这个参数传0会发生什么呢?这个是标准未定义行为,不同编译器不同。

如果开辟内存成功,就会返回一个void *地址的地址,如果开辟失败就会返会NULL它不会给空间初始化一个值。

下面我们来演示一下这个函数的使用:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main()
{
	int n = 0;
	scanf("%d", &n);
	int* a = (int*)malloc(sizeof(int) * n);
	if (a == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	memset(a, 0, sizeof(int) * n);
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	free(a);
	a = NULL;
	return 0;
}

最后三行我们先不管,至于中间判断开辟内存失败的代码,如果你不知道,可以去看一下博主这篇文章【数据结构初阶】之单链表

我们来看看运行结果:

在这里插入图片描述
另外我们也可以测试一下什么时候会malloc失败:

在这里插入图片描述
可以看到当我们在堆上开3000 000 00*4个字节的空间时,会malloc失败。

因为1B就是1字节,1KB = 1024B,1MB = 1024KB,1G = 1024MB,我们算了一下大概是开286MB左右的空间,malloc才会失败,所以我们平时写代码可以不加这个判断,但是在大的工程项目中加上可以增加我们代码的健壮性。

至于如果传的大小是0会怎么样,我们可以看一下VS2019是如何处理的:


可以看到程序是正常退出了的。

  • 注意,有时候我们把下面的a又叫做动态数组。
int* a = (int*)malloc(sizeof(int) * 6);

因为a的空间是连续,而且可以变化,不是固定的,能多次更改,而且可以通过[]操作符访问,我们把这个a又叫做动态数组。

2.1.2 free函数

free函数是用来手动释放动态开辟空间的函数,它的声明是这样的:

void free (void* ptr);

我们只需要传一个保存了动态数组首元素地址的那个指针变量就可以回收那片空间。

2.2calloc函数

C语言还提供了一个动态内存管理的函数,叫做calloc,它的函数声明是这样的:

void* realloc(size_t num,size_t size);
  • 函数的功能是为开辟num个大小为size的元素开辟一片空间,并给它们的每个字节初始化为0,它和malloc函数的区别就在于,malloc函数不会初始化。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main()
{
	int n = 0;
	scanf("%d", &n);
	int* a = (int*)calloc(n,sizeof(int));
	if (a == NULL)
	{
		perror("calloc failed");
		exit(-1);
	}
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	free(a);
	a = NULL;
	return 0;
}

a的内存调试结果:

在这里插入图片描述
可以看到,程序执行到光标位置,a的内存每一个字节的内容已经全部被初始化为0了。

2.3realloc函数

realloc函数也是C语言给我们提供的一个函数,有时候我们malloc一片空间后,发现不够用了,就需要使用realloc给那个空间扩容。

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

它的第一个参数是一个指针变量,第二个参数size是调整之后的新大小。
我们通常会出现如下两种情况:

  1. 有一片size大小的连续的空间,返回的地址还是原先的指针变量的地址。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main()
{
	int n = 0;
	scanf("%d", &n);
	int* a = (int*)malloc(n * sizeof(int));
	if (a == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	int* tmp = (int*)realloc(a, (n + 1) * sizeof(int));
	printf("%p %p", a, tmp);
	free(a);
	a = NULL;
	return 0;
}

这里我们只扩容了4个字节的空间应该是不用重新找一片连续的空间的,我们看运行截图:


我们可以看到返回的地址确实和未扩容前a的地址是一样的。

2.没有一片连续的size大小的空间,如果开辟空间成功,realloc会将之前的数据拷贝到一片新的连续的空间中,并帮助你把原先旧的空间给释放掉。

如果你不相信,我们可以通过下面的代码来验证一下:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main()
{
	int n = 0;
	scanf("%d", &n);
	int* a = (int*)malloc(n * sizeof(int));
	if (a == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	int* tmp = (int*)realloc(a, (n + 500) * sizeof(int));
	if (tmp == NULL)
	{
		perror("realloc failed");
		exit(-1);
	}
	printf("%p %p",a,tmp);
	free(tmp);
	tmp = NULL;
	return 0;
}

运行截图:

在这里插入图片描述
此时a的地址已经和tmp的不相同了,因为我们在原先的堆区的位置,找不到一片连续的510字节的空间,a的地址那片空间已经释放过了,如果你再释放a,系统就会报错:

在这里插入图片描述

如果扩容失败就会返回NULL指针:

在这里插入图片描述

所以我们在使用realloc指针时应该先用tmp来保存其返回的地址,因为如果扩容失败,返回NULL直接把NULL赋值给a,那我们a的数据就找不到了,所以先赋值给tmp,并加上判断,是为了数据的安全考虑,正确的使用方法是这样:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main()
{
	int n = 0;
	scanf("%d", &n);
	int* a = (int*)malloc(n * sizeof(int));
	if (a == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	int* tmp = (int*)realloc(a, (n + 10) * sizeof(int));
	if (tmp == NULL)
	{
		perror("realloc failed");
		exit(-1);
	}
	a = tmp;
	memset(a, 0, (n + 10) * sizeof(int));
	for (int i = 0; i < n + 10; i++)
	{
		printf("%d ", *(a + i));
	}
	free(a);
	a = NULL;
	return 0;
}

realloc也不会给它开辟的地址空间初始化,而且当第一个参数传NULL时,它的功能就相当于malloc函数,

在这里插入图片描述
我们可以简单的使用一下:

在这里插入图片描述

3.动态内存管理中经常出现的一些问题总结。

3.1 越界访问

越界访问就是对不属于你的空间进行操作,在进行free操作的时候会报错,请看如下代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main()
{
	int* a = (int*)malloc(sizeof(int) * 10);
	if (a == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	for (int i = 0; i <= 10; i++)
	{
		*(a + i) += 1;
	}
	free(a);
	a = NULL;
	return 0;
}

报错截图:

在这里插入图片描述

这里正常应该没有等于,因为我们只开了10个int型的空间,有了等于就非法访问了后面一个不属于我们的四个字节的空间,free时会报错。

在这里插入图片描述
如果只是遍历一下,打印一下那里面的值,编译器似乎是检查不出来的,

在这里插入图片描述
这种情况编译器虽然不报错,但还是比较危险,严格意义上也属于越界访问,不要去做。

3.2 对空指针进行解引用操作

NULL是不能进行解引用操作的,我们在使用动态内存函数时可能会出现这种情况:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main()
{
	int* a = (int*)malloc(sizeof(int) * INT_MAX);
	*a = 4;
	return 0;
}

运行截图:

在这里插入图片描述

这里我们如果加上一个a == NULL的判断,我们就可以知道问题了,也不会出现对空指针进行解引用的未定义操作,程序就不会异常挂掉了。

3.3 对同一片空间进行多次释放

我们不能多次释放我们已经释放过的空间。

在这里插入图片描述
否则编译器会强制的报错。

3.4 释放非动态开辟的空间进行释放

free只能释放动态开辟的空间,不能释放临时变量的空间。
在这里插入图片描述
这里我们释放掉n的空间,程序崩溃了,因为n是临时变量。

3.5 忘记释放动态开辟的空间

这里有人就要问了?为什么要手动释放堆上开的空间呢?程序运行结束之后,系统不是自动回收吗,我们这样做不是多次一举吗?

堆上开的空间想释放只有两种办法:

  1. free函数手动释放。
  2. main函数结束,程序运行结束,系统自动回收。

注意:有些程序是永远都在运行着的,比如我们手机上的淘宝,你一打开它就一直运行,很多空间都是堆上开的,一个函数可能会重复执行很多次,如果你使用了堆上的空间不主动释放,程序也还没结束,就会造成内存泄漏,久而久之内存被占完了,程序就会挂掉。

3.6 野指针问题

还有一点,为什么释放那片空间后,还要把相应的指针变量赋值为空呢?因为那片空间已经不属于我们了,被系统回收了,是野指针,为了防止你非法访问造成一些我们很难查出的问题,赋为空值是最好选择。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main()
{
	int* a = (int*)malloc(sizeof(int) * 10);
	if (a == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	printf("%p\n", a);
	memset(a, 0, sizeof(int) * 10);
	free(a);
	*a = 4;
	printf("%p\n%d", a, *a);
	return 0;
}

运行截图:

在这里插入图片描述
可以看到,程序运行是正常的,但是a的空间被系统回收后,a仍然保存的还是那片空间的地址,但那片空间已经被系统回收了,它就是一个野指针了。

我们访问那个地址是非法的,但是编译器检查不出来,如果我们养成好习惯,在释放空间后主动将a赋为NULL就不会出现检查不出来的问题了,因为对NULL解引用程序会崩溃。

3.7只释放一部分动态开辟的空间

我们也不能只释放a的一部分空间,这是编译器不允许的行为:


#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main()
{
	int* a = (int*)malloc(sizeof(int) * 10);
	if (a == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	int* p = a + 1;
	free(p);
	p = NULL;
	return 0;
}

运行截图:

在这里插入图片描述

为了防止出现这种问题,我们尽量做到,空间是谁申请的就由谁去释放。

  • 39
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 53
    评论
高级c语言教程 目录 1. C 语言中的指针和内存泄漏 5 2. C语言难点分析整理 10 3. C语言难点 18 4. C/C++实现冒泡排序算法 32 5. C++中指针和引用的区别 35 6. const char*, char const*, char*const的区别 36 7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. C语言复杂表达式的执行步骤 66 16. C语言字符串函数大全 68 17. C语言宏定义技巧 89 18. C语言实现动态数组 100 19. C语言笔试-运算符和表达式 104 20. C语言编程准则之稳定篇 107 21. C语言编程常见问题分析 108 22. C语言编程易犯毛病集合 112 23. C语言缺陷与陷阱(笔记) 119 24. C语言防止缓冲区溢出方法 126 25. C语言高效编程秘籍 128 26. C运算符优先级口诀 133 27. do/while(0)的妙用 134 28. exit()和return()的区别 140 29. exit子程序终止函数与return的差别 141 30. extern与static存储空间矛盾 145 31. PC-Lint与C\C++代码质量 147 32. spirntf函数使用大全 158 33. 二叉树的数据结构 167 34. 位运算应用口诀和实例 170 35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针 224 46. 标准C中字符串分割的方法 228 47. 汉诺塔源码 231 48. 洗牌算法 234 49. 深入理解C语言指针的奥秘 236 50. 游戏外挂的编写原理 254 51. 程序实例分析-为什么会陷入死循环 258 52. 空指针究竟指向了内存的哪个地方 260 53. 算术表达式的计算 265 54. 结构体对齐的具体含义 269 55. 连连看AI算法 274 56. 连连看寻路算法的思路 283 57. 重新认识:指向函数的指针 288 58. 链表的源码 291 59. 高质量的子程序 295 60. 高级C语言程序员测试必过的十六道最佳题目+答案详解 297 61. C语言常见错误 320 62. 超强的指针学习笔记 325 63. 程序员之路──关于代码风格 343 64. 指针、结构体、联合体的安全规范 346 65. C指针讲解 352 66. 关于指向指针的指针 368 67. C/C++ 误区一:void main() 373 68. C/C++ 误区二:fflush(stdin) 376 69. C/C++ 误区三:强制转换 malloc() 的返回值 380 70. C/C++ 误区四:char c = getchar(); 381 71. C/C++ 误区五:检查 new 的返回值 383 72. C 是 C++ 的子集吗? 384 73. C和C++的区别是什么? 387 74. 无条件循环 388 75. 产生随机数的方法 389 76. 顺序表及其操作 390 77. 单链表的实现及其操作 391 78. 双向链表 395 79. 程序员数据结构笔记 399 80. Hashtable和HashMap的区别 408 81. hash 表学习笔记 410 82. C程序设计常用算法源代码 412 83. C语言有头结点链表的经典实现 419 84. C语言惠通面试题 428 85. C语言常用宏定义 450

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小镇敲码人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值