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

news2024/11/24 3:31:37

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

  • 1.为什么我们需要动态内存管理
  • 2.动态内存管理的函数介绍
    • 2.1malloc函数和free函数
      • 2.1.1malloc函数
      • 2.1.2 free函数
    • 2.2calloc函数
    • 2.3realloc函数
  • 3.动态内存管理中经常出现的一些问题总结。
    • 3.1 越界访问
    • 3.2 对空指针进行解引用操作
    • 3.3 对同一片空间进行多次释放
    • 3.4 释放非动态开辟的空间进行释放
    • 3.5 忘记释放动态开辟的空间
    • 3.6 野指针问题
    • 3.7只释放一部分动态开辟的空间

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

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

运行截图:

在这里插入图片描述

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1171438.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

安全防御——二、ENSP防火墙实验学习

安全防御 一、防火墙接口以及模式配置1、untrust区域2、trust区域3、DMZ区域4、接口对演示 二、防火墙的策略1、定义与原理2、防火墙策略配置2.1 安全策略工作流程2.2 查询和创建会话 3、实验策略配置3.1 trust-to-untrust3.2 trust-to-dmz3.3 untrust-to-dmz 三、防火墙的区域…

归并排序--C语言实现

1. 简述 归并排序的原理是将&#xff0c;两个较大的数组分为大小几乎一致的两个数组。 再将两个数组进行合并成新的有序数组。 合并两个数组的时候需要额外的一个数组的空间。 2. 实现 上图说明过程 代码 #include <stdio.h>void Merge(int *arr, int *tmp, int …

freertos入门(stm32f10c8t6版闪烁灯)

首先到官网下载freertos源码&#xff0c;然后找一个stm32f10c8t6的空模板&#xff0c;这个空模板实现点灯之类的都行。 然后在这个空模板的工程下新建一个FreeRtos文件夹 接着在FreeRtos文件夹下新建三个文件夹&#xff0c;分别是src存放源码 inc 存放头文件&#xff0c;port …

Linux下查看文件夹大小命令

在Vscode上连接服务器&#xff0c;想查看文件夹大小&#xff1b; du -h path

4 个最常见的自动化测试挑战及应对措施

有人说&#xff1a;“杂乱无章的自动化只会带来更快的混乱。”不仅更快&#xff0c;而且是更严重、更大的混乱。如果使用得当&#xff0c;自动化可以成为测试团队中令人惊叹的生产力助推器和系统的质量增强器。自动化测试的关键是要正确运用&#xff0c;这是初始最困难的部分。…

动态路由协议OSPF优化提速特性

1.OSPF协议通信过程与部署&#xff1b; 2.OSPF协议在项目上的应用场景&#xff1b; 3.OSPF有哪些优化特性&#xff1f; - OSPF - 开放式最短路径优先 - 一个动态路由协议 - 路由协议 - 理解魏 运行在路由器的一个软件 - 目的&#xff1a;为了帮助路由器和路由器彼…

短期经济波动:均衡国民收入决定理论(二)

短期经济波动:国民收入决定理论(二) 文章目录 短期经济波动:国民收入决定理论(二)[toc]1 IS曲线1.1 IS曲线的代数推导1.1.1 代数法&#xff1a;计划支出等于实际支出1.1.2 代数法&#xff1a;计划投资等于储蓄1.1.3 代数法&#xff1a;非计划存货等于0 1.2 IS曲线的几何推导1.2…

ZZ038 物联网应用与服务赛题第D套

2023年全国职业院校技能大赛 中职组 物联网应用与服务 任 务 书 (D卷) 赛位号:______________ 竞赛须知 一、注意事项 1.检查硬件设备、电脑设备是否正常。检查竞赛所需的各项设备、软件和竞赛材料等; 2.竞赛任务中所使用的各类软件工具、软件安装文件等,都…

如何理解所谓的【指令执行速度】

公式&#xff1a; 指令执行速度 主频/平均CPI 先不看主频&#xff0c;如下图&#xff0c;假设一秒钟能有4个正弦波&#xff0c;那就说明频率是4。 而计算机很厉害&#xff0c;一秒能有很多个正弦波 把一个正弦波&#xff0c;看做一个时钟周期 则主频表示&#xff0c;计算机…

《视觉SLAM十四讲》-- 概述与预备知识

文章目录 01 概述与预备知识1.1 SLAM 是什么1.1.1 基本概念1.1.2 视觉 SLAM 框架1.1.3 SLAM 问题的数学表述 1.2 实践&#xff1a;编程基基础1.3 课后习题 01 概述与预备知识 1.1 SLAM 是什么 1.1.1 基本概念 &#xff08;1&#xff09;SLAM 是 Simultaneous Localization a…

【算法|二分查找No.2】leetcode 69. x 的平方根

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&#xff0c;希望…

Python基础入门例程29-NP29 用列表实现栈(列表)

最近的博文&#xff1a; Python基础入门例程28-NP28 密码游戏&#xff08;列表&#xff09;-CSDN博客 Python基础入门例程27-NP27 朋友们的喜好&#xff08;列表&#xff09;-CSDN博客 Python基础入门例程26-NP26 牛牛的反转列表&#xff08;列表&#xff09;-CSDN博客 目录…

0.专栏概述与几句闲话

引 还记得今年大年初一开始写《数据结构和算法》专栏的时候定了个小目标&#xff1a; 不知不觉间已经过去了十个月&#xff0c;我的第一个专栏也算是圆满收官了 。 这次PO一张成都熊猫基地的团子们&#xff0c;开启设计模式这个专栏吧。 目录与概述 犹记得一位身在广州的老…

CAN报文的信号和信号组传递的意义

CAN将数据发送到COM层&#xff0c;在这个过程中报文是如何传递的&#xff1f; 0x105指的是一帧CAN报文&#xff0c;信号组指的是一帧CAN报文里的所有数据&#xff0c;信号指的是一帧CAN报文里的每一个信号&#xff0c;PDU代表了一帧CAN报文&#xff0c;它由报文ID&#xff08;I…

wagtail的使用

文章目录 安装虚拟环境新建项目时指定虚拟环境打开已有项目添加虚拟环境 安装wagtail查看安装后的包 创建wagtail项目安装依赖迁移创建超级用户运行项目 管理工作台内容扩展首页的数据模型更新数据库修改模板页创建一个页面的过程 models中的基本字段templates字符型文本字段富…

[动态规划] (五) 路径问题: LeetCode 62.不同路径

[动态规划] (五) 路径问题: LeetCode 62.不同路径 文章目录 [动态规划] (五) 路径问题: LeetCode 62.不同路径题目解析解题思路状态表示状态转移方程初始化和填表返回值 代码实现总结 62. 不同路径 题目解析 (1) 机器人从左上角到右下角有多少方法 (2) 机器人只能向左或者向右…

【VsCode输出中文乱码问题】用vscode写c/c++时,终端输出结果为中文乱码如何解决?

文章目录 前言原因解决办法方法一&#xff1a;chcp临时修改编码方式1. 使用chcp命令可以查看cmd的编码方式&#xff0c;直接在当前文件夹目录下&#xff0c;输入&#xff1a;chcp2. 修改编码方式 方式二&#xff1a;更改VScode的默认编码方式为GBK 前言 因为我平时在vscode写代…

快速排序(Java)

基本思想 快速排序Quicksort&#xff09;是对冒泡排序的一种改进。 基本思想是分治的思想&#xff1a;通过一趟排序将要排序的数据分割成独立的两部分&#xff0c;其中一部分的所有数据都比另外一部分的所有数据都要小&#xff0c;然后再按此方法对这两部分数据分别进行快速排…

高等数学教材重难点题型总结(十)重积分

第十章错题整理&#xff0c;公式由于多元函数的引入看起来唬人而已&#xff0c;其实熟练掌握一元函数的积分后不算什么难事。重点在于计算时不要犯细节错误。此外&#xff0c;对于不同坐标系下的积分方式要重点关注~ 1.利用性质估计二重积分的值 2.计算直角坐标系下的二重积分 …

行情分析——加密货币市场大盘走势(11.4)

大饼昨日下跌过后开始有回调的迹象&#xff0c;现在还是在做指标修复&#xff0c;大饼的策略保持逢低做多。稳健的依然是不碰。 以太昨日给的策略&#xff0c;已经止盈了&#xff0c;现在已经达到1835&#xff0c;接下来就是等待即可&#xff0c;还会继续上涨的&#xff0c;当然…