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

news/2024/12/2 18:41: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.ppmy.cn/news/1196676.html

相关文章

docker的安装Centos8

在CentOS 7中&#xff0c;可以使用yum安装Docker。Docker官方提供了一个yum源&#xff0c;可以用于安装Docker。以下是安装Docker的步骤&#xff1a; 卸载旧版本的Docker&#xff08;如果有&#xff09; 如果你之前安装过Docker&#xff0c;需要先卸载旧版本的Docker。执行以…

Linux文本编辑器vim使用和配置详解

vim介绍 ​ vim是Linux的一款文本编辑器&#xff0c;可以用来编辑代码&#xff0c;而且支持语法高亮&#xff0c;还可以进行一系列配置使vim更多样化。也可以运行于windows&#xff0c;mac os上。 ​ vim有多种模式&#xff0c;但目前我们只介绍绝大多数场景用的到的模式&…

Java日期比较大小的3种方式及拓展

目录 一、字符串String的日期比较 二、数值型long比较 三、日期型Date直接比较 四、Date型日期的获取方式 五、Calendar获取年月日【拓展】 一、字符串String的日期比较 String型的日期通过compareTo()来比较&#xff0c;因为String实现了comparable接口 endDate.compare…

医学影像处理系统源码(PACS)

通用医学图像处理平台覆盖全模态、多维度临床应用&#xff0c;助力提供医学图像分析的全景高清视角&#xff0c;赋能临床精准诊断。 一、PACS覆盖CT、MR、MI等多模态影像及心血管、肿瘤、神经等多临床场景&#xff0c;助力医生精准高效诊断。 二、临床应用 1.基础应用 &#…

Prompt 设计与大语言模型微调,没有比这篇更详细的了吧!

本文主要介绍了Prompt设计、大语言模型SFT和LLM在手机天猫AI导购助理项目应用。 ChatGPT基本原理 “会说话的AI”&#xff0c;“智能体” 简单概括成以下几个步骤&#xff1a; 预处理文本&#xff1a;ChatGPT的输入文本需要进行预处理。 输入编码&#xff1a;ChatGPT将经过预…

【深度学习】Yolov8 区域计数

ref&#xff1a;https://github.com/ultralytics/ultralytics/blob/main/examples/YOLOv8-Region-Counter/readme.md 很长时间没有做yolov的项目了&#xff0c;最近一看yolov8有一个区域计数的功能&#xff0c;不得不说很实用啊。

职场被迫内卷,云认证破局

前言&#xff1a; 2023年作为疫情全面放开的第一年&#xff0c;经济并没有像22年底时我们想象的那样&#xff0c;快速复苏&#xff0c;GDP增长超10%。取而代之的是&#xff0c;2023年经济大环境对各个行业来说&#xff0c;相比22年显的更加艰难&#xff0c;GDP增长预计在5%左右…

Java 正则表达式字符篇

精确匹配一个字符 精确匹配字符串 abc &#xff0c; //精确匹配字符串 "abc"String regexabc "abc";System.out.println("abc".matches(regexabc));// trueSystem.out.println("ABC".matches(regexabc));// falseSystem.out.println…