C语言进阶第九课 --------动态内存管理

2023-10-25 18:44

本文主要是介绍C语言进阶第九课 --------动态内存管理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

作者前言

🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂
​🎂 作者介绍: 🎂🎂
🎂 🎉🎉🎉🎉🎉🎉🎉 🎂
🎂作者id:老秦包你会, 🎂
简单介绍:🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂
喜欢学习C语言和python等编程语言,是一位爱分享的博主,有兴趣的小可爱可以来互讨 🎂🎂🎂🎂🎂🎂🎂🎂
🎂个人主页::小小页面🎂
🎂gitee页面:秦大大🎂
🎂🎂🎂🎂🎂🎂🎂🎂
🎂 一个爱分享的小博主 欢迎小可爱们前来借鉴🎂


动态内存

  • **作者前言**
  • 动态内存的意义
  • 动态内存函数的介绍
    • malloc
    • free
    • calloc
    • realloc
  • 常见的动态内存错误
    • 对NULL指针的解引用操作
    • 对动态内存的越界访问
    • 对非动态开辟的内存进行free释放
    • 使用free释放动态开辟的内存的一部分
    • 对一块空间多次释放
    • 动态开辟内存忘记释放(内存泄漏)
  • 经典笔试题
    • C/C++中程序内存区域分布
  • 柔性数组
    • 柔性数组的特点
    • 使用柔性数组的好处
  • 总结

动态内存的意义

在我们空间的开辟有定义变量

#include<stdio.h>
int main()
{int a = 0;char arr[] = "sdsd";printf("%p\n", &a);printf("%p\n", arr);return 0;
}

甚至还有局部变量都会向内存申请空间,但是这些空间不擦长久,或者很难随意操作这些空间进行销毁,或者不能随意扩大空间
但是上述的开辟空间的方式有两个特点:

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

动态内存函数的介绍

malloc

在这里插入图片描述
这个函数的动态内存开辟函数,
size_t是一个无符号整数类型,表示需要分配的内存大小(以字节为单位)。malloc函数会在堆上分配一块指定大小的内存,并返回指向该内存块的指针。如果分配失败,则返回NULL。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = (int*)malloc(sizeof(int) * INT_MAX);if (arr == NULL){perror("malloc:");return 1;}return 0;
}

可能有一些人会利用malloc申请0个字节,这种做法本身没有错误,但是这个就是脱裤子放屁多此一举
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
在这里插入图片描述

free

有心细的小可爱就会发现,当我们程序运行结束就会释放出开辟的动态内存(被动),如果我们要自己释放就要用到free函数
在这里插入图片描述
free是C语言中用于释放动态分配内存的函数。当我们使用malloc、calloc等函数在程序运行期间动态申请内存时,需要在使用完毕后使用free函数将其释放回系统,以便其他程序或进程可以使用。

其中,ptr是指向需要释放的内存块的指针。使用free函数可以将之前通过malloc、calloc等函数动态分配的内存块释放回系统,以避免内存泄漏和浪费。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = (int*)malloc(sizeof(int) * 10);if (arr == NULL){perror("malloc");return 1;}int i = 0;for (i = 0; i < 10; i++){arr[i] = i;printf("%d ", arr[i]);}free(arr);arr = NULL;return 0;
}

需要注意的是,在使用free函数释放内存块时需要注意以下几点:

只能释放之前通过malloc、calloc等函数动态分配的内存块,不能释放栈上的局部变量和全局变量等静态分配的内存;
不能重复释放同一块内存,否则会导致程序崩溃;
释放内存后,应该将指针设置为NULL,以避免出现野指针问题。

calloc

在这里插入图片描述
calloc是C语言中用于动态分配内存并进行初始化的函数。与malloc函数相似,calloc也用于在程序运行时动态分配内存,但与malloc不同的是,calloc会将分配的内存块初始化为零,而malloc不会
其中,num表示需要分配的元素个数,size表示每个元素的大小。calloc函数会分配num个大小为size的连续内存块,并将其初始化为零。如果分配成功,则返回指向分配内存块的指针;如果分配失败,则返回NULL。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = (int*)calloc(10, sizeof(int));if (arr == NULL){perror("calloc");return 1;}int i = 0;for (i = 0; i < 10; i++){arr[i] = i;}free(arr);arr = NULL;return 0;
}

realloc

realloc函数的出现让动态内存管理更加灵活,前面的malloc和calloc申请的空间无法避免申请空间过大或者过小,realloc函数就可以做到动态开辟内存的大小进行调整

realloc是C语言中用于重新分配内存的函数。在程序运行期间,我们有时需要重新分配内存块的大小,可以使用realloc函数来实现。realloc函数可以将之前分配的内存块的大小调整为新的大小,并返回指向新内存块的指针。
在这里插入图片描述
ptr是指向之前分配的内存块的指针,size是需要重新分配的内存块的大小。
如果重新分配成功,则返回指向新内存块的指针;如果分配失败,则返回NULL。需要注意的是,如果新的大小小于原来的大小,则realloc函数会截断原来的内存块;如果新的大小大于原来的大小,则realloc函数会在原来的内存块后面分配新的内存块。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = (int*)malloc(sizeof(int) * 10);int* arr1 = (int*)calloc(20, sizeof(int));if (arr == NULL){perror("malloc");}if (arr1 == NULL){perror("calloc");}//调整空间int *arr3 = (int*)realloc(arr, 2000 * sizeof(int));if (arr3 != NULL){arr = arr3;}free(arr);free(arr1);arr = NULL;arr1 = NULL;arr3 = NULL;return 0;
}

realloc函数开辟的空间情况分两种
第一种:
在这里插入图片描述
之前malloc开辟空间的后面刚好满足扩大后的空间大小就会在原来的地址处重新扩大,这种情况返回的新地址就会和旧地址的值是一样的
在这里插入图片描述

第二种:

之前malloc开辟的空间的后面不能满足扩大后的空间大小,就会放弃在原来的空间扩大,反而会重新找一块能满足条件的内存进行开辟,一次性开辟好,会将旧的空间的数据就会拷贝到新的空间,旧的空间就会销毁
在这里插入图片描述

常见的动态内存错误

对NULL指针的解引用操作

这种情况的出现是因为没有对返回值进行判断造成的

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = malloc(sizeof(int) * INT_MAX);int i = 0;for (i = 0; i < INT_MAX; i++){arr[i] = 1;}return 0;
}

如上面代码一样,没有判断是否创建成功,直接解引用操作就会报错
在这里插入图片描述

对动态内存的越界访问

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = (int*)calloc(10, sizeof(int));if (arr == NULL){perror("calloc");return 1;}int i = 0;for (i = 0; i <= 10; i++){arr[i] = i;}for (i = 0; i <= 10; i++){printf("%d ", *(arr + i));}free(arr);arr = NULL;return 0;
}

这种情况就和我们创建数组,越界访问是一样的

对非动态开辟的内存进行free释放

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int a = 0;int* p = &a;free(p);p = NULL;return 0;
}

这种情况就是懵了

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

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = calloc(10, sizeof(int));int i = 0; for (i = 0; i < 5; i++){*arr = i;arr++;}free(arr);arr = NULL;return 0;
}

在这里插入图片描述

对一块空间多次释放

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = (int*)malloc(sizeof(int) * 10);if (arr == NULL){perror("malloc");return 1;}free(arr);free(arr);return 0;
}

解决这个错误我们可以养成一个习惯,只要释放过的空间,对应的变量全部赋值为NULL,因为free释放NULL不会有啥错误

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

include <stdio.h>
#include <stdlib.h>
#include <limits.h>
void Other()
{int* arr = (int*)malloc(sizeof(int) * 10);
}
int main()
{Other();while (1){;}return 0;
}

这样很容易造成空间不足,我们要记住谁申请,谁释放,如果没有释放交接任务也要告诉别人释放

经典笔试题

第一题

void GetMemory(char *p)
{p = (char *)malloc(100);
}
void Test(void)
{char *str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);
}

上面这些代码主要的错误就是当我们调用Test函数时,也会调用到 GetMemory函数,而传参传递的时变量str的值,而不是地址,最后str的值还是NULL,还造成了内存泄漏对NULL解引用就会报错,如果把 GetMemory函数更改为 GetMemory(char* *p ),然后传递str的地址
更改后:

void GetMemory(char* *p)
{*p = (char*)malloc(100);
}
void Test(void)
{char* str = NULL;GetMemory(&str);strcpy(str, "hello world");printf(str);free(str);str = NULL;
}
int main()
{Test();return 0;
}

还要注意的printf函数的使用没有错,是可以这样写的,这个写法只针对字符串
第二题

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
char* GetMemory(void)
{char p[] = "hello world";return p;
}
void Test(void)
{char* str = NULL;str = GetMemory();printf(str);
}
int main()
{Test();return 0;
}

这里的错误是GetMemory函数在调用结束后,p的空间销毁掉了,返回的值也就成了野指针
第三题

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
void GetMemory(char** p, int num)
{*p = (char*)malloc(num);
}
void Test(void)
{char* str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);
}
int main()
{Test();return 0;
}

这道题主要是少了free释放,内存泄漏了
第四题

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
void Test(void)
{char* str = (char*)malloc(100);strcpy(str, "hello");free(str);if (str != NULL){strcpy(str, "world");printf(str);}
}
int main()
{Test();return 0;
}

这里的错误就是野指针,在我们释放了动态开辟的内存,str还是指向那块地址,这样就会非法访问内存,这里我们要注意释放后一定要赋值NULL

C/C++中程序内存区域分布

在这里插入图片描述
其中代码段是用来存储代码翻译成的二进制指令只读常量(字符串常量),里面的数据不可更改
在这里插入图片描述
这个图就可以很明了的知道动态内存开辟的空间是在堆区,局部变量和形式参数的空间是在栈区m全局变量和静态变量的空间是在数据段(静态区)
还要注意的是函数的空间也是在栈上创建的,函数栈帧的创建和销毁前面我已经写过,可以去看看
C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
    束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
    分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
    回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
    配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

柔性数组

这个概念是在C99引入的,之前是没有的
形成柔性数组的条件:

  1. 要在结构体中
  2. 是结构体的最后一个成员,并且这个数组是一个未知大小的数组(变长数组)

写法:

struct S{int a;int arr[];//柔性数组}a1;struct S1{int a;int arr[0];//柔性数组}a2;

代码中 的两个结构体属于两种写法,

柔性数组的特点

  1. 结构中的柔性数组成员前面必须至少一个其他成员。
  2. sizeof 返回的这种结构大小不包括柔性数组的内存。
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
struct S1
{int a;int arr[0];
};
int main()
{printf("%d", sizeof(struct S1);return 0;
}

在这里插入图片描述
可以看到sizeof返回的大小不包含柔性数组,假设结构体只有一个成员,而这个成员是柔性数组就会发现这个结构体的大小是0


3. 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
struct S1
{int a;int arr[0];
};
int main()
{struct S1* p = (struct S1*)malloc(sizeof(int) + 20);if (p == NULL){perror("malloc");return;}p->a = 10;int i = 0;for (i = 0; i < 4; i++){p->arr[i] = i;}free(p);p = NULL;return 0;
}

其实我们可以不使用柔性数组,我们也可以模拟出来

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
struct S1
{int a;int arr[0];
};
struct S2
{int a;int* arr;
};
int main()
{/*struct S1* p = (struct S1*)malloc(sizeof(int) + 20);if (p == NULL){perror("malloc");return;}p->a = 10;int i = 0;for (i = 0; i < 4; i++){p->arr[i] = i;}free(p);p = NULL;*/struct S2 a2;a2.a = 10;a2.arr = (int*)malloc(sizeof(int) * 4);if (a2.arr == NULL){perror("malloc");return;}printf("%d", sizeof(struct S2));free(a2.arr);a2.arr = NULL;return 0;
}

使用这种方法虽然可以代替柔性数组,但是使用free的次数会增多,而使用柔性数组只需使用一次free

当我们使用malloc开辟的数次越多,产生的内存碎片就会越多,内存的利用率就会下降

使用柔性数组的好处

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

总结

在这里介绍了动态内存的开辟、常见错误柔性数组,有不懂的小可爱可以私聊

这篇关于C语言进阶第九课 --------动态内存管理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/284492

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

软考系统规划与管理师考试证书含金量高吗?

2024年软考系统规划与管理师考试报名时间节点: 报名时间:2024年上半年软考将于3月中旬陆续开始报名 考试时间:上半年5月25日到28日,下半年11月9日到12日 分数线:所有科目成绩均须达到45分以上(包括45分)方可通过考试 成绩查询:可在“中国计算机技术职业资格网”上查询软考成绩 出成绩时间:预计在11月左右 证书领取时间:一般在考试成绩公布后3~4个月,各地领取时间有所不同

安全管理体系化的智慧油站开源了。

AI视频监控平台简介 AI视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。用户只需在界面上进行简单的操作,就可以实现全视频的接入及布控。摄像头管理模块用于多种终端设备、智能设备的接入及管理。平台支持包括摄像头等终端感知设备接入,为整个平台提

[MySQL表的增删改查-进阶]

🌈个人主页:努力学编程’ ⛅个人推荐: c语言从初阶到进阶 JavaEE详解 数据结构 ⚡学好数据结构,刷题刻不容缓:点击一起刷题 🌙心灵鸡汤:总有人要赢,为什么不能是我呢 💻💻💻数据库约束 🔭🔭🔭约束类型 not null: 指示某列不能存储 NULL 值unique: 保证某列的每行必须有唯一的值default: 规定没有给列赋值时的默认值.primary key:

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Flutter 进阶:绘制加载动画

绘制加载动画:由小圆组成的大圆 1. 定义 LoadingScreen 类2. 实现 _LoadingScreenState 类3. 定义 LoadingPainter 类4. 总结 实现加载动画 我们需要定义两个类:LoadingScreen 和 LoadingPainter。LoadingScreen 负责控制动画的状态,而 LoadingPainter 则负责绘制动画。