当前位置: 首页 > news >正文

C语言详解(动态内存管理)2

Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~
💥💥个人主页:奋斗的小羊
💥💥所属专栏:C语言

🚀本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为展示我的学习过程及理解。文笔、排版拙劣,望见谅。


目录

  • 前言
  • 1、常见动态内存错误
      • 1.1 对NULL指针的解引用操作
      • 1.2 对动态内存空间的越界访问
      • 1.3 对非动态开辟内存使用free释放
      • 1.4 使用free释放动态内存的一部分
      • 1.5 对同一快动态内存多次释放
      • 1.6 动态开辟内存忘记释放(内存泄漏)
  • 2、动态内存经典笔试题分析
      • 2.1 题目一
      • 2.2 题目二
      • 2.3 题目三
      • 2.4 题目四
  • 3、柔性数组
      • 3.1 什么是柔性数组
      • 3.2 柔性数组的特点
      • 3.3 柔性数组的使用
      • 3.4 柔性数组的优势
  • 总结

前言

总的来说,动态内存管理为我们提供了更加灵活、高效和可扩展的内存管理方式,但动态内存管理函数可能会带来一些风险,主要包括内存泄漏、内存溢出和野指针等问题,我们在使用动态内存管理函数时要多留心,避免风险的出现


1、常见动态内存错误

1.1 对NULL指针的解引用操作

如果我们写的代码不严谨,没有考虑到动态内存分配失败的可能,就会写出类似于下面的代码:

#include <stdio.h>
#include <stdlib.h>int main()
{int* p = (int*)malloc(10 * sizeof(int));//直接使用指针pint i = 0;for (i = 0; i < 10; i++){p[i] = i + 1;}return 0;
}

这样的代码可能并没有什么问题,但是存在很大的隐患,因为动态内存函数是有可能开辟内存空间失败的,当开辟失败时会返回NULL,而NULL指针是不能解引用的
像VS这样比较强大的编译器会立马检测到并提示你
在这里插入图片描述

为了避免这种错误,我们需要对指针p进行判断,再决定是否使用

#include <stdio.h>
#include <stdlib.h>int main()
{int* p = (int*)malloc(10 * sizeof(int));//判断p是否为空指针if (p == NULL){//打印出错误信息perror("malloc");//终止程序return 1;}int i = 0;for (i = 0; i < 10; i++){p[i] = i + 1;}return 0;
}

1.2 对动态内存空间的越界访问

我们用动态内存函数开辟多大的空间,我们就使用多大的空间,不能越界访问,例如:

#include <stdio.h>
#include <stdlib.h>int main()
{int* p = (int*)malloc(10 * sizeof(int));//判断p是否为空指针if (p == NULL){//打印出错误信息perror("malloc");//终止程序return 1;}int i = 0;//p+1跳过1个整型,p+10就会越界for (i = 0; i <= 10; i++){p[i] = i + 1;}return 0;
}

聪明的VS也会检测出错误提示你
在这里插入图片描述


1.3 对非动态开辟内存使用free释放

free函数是用来释放由动态内存函数开辟的空间的,不能释放普通内存

#include <stdio.h>
#include <stdlib.h>int main()
{int arr[10] = { 0 };int* p = arr;free(p);p = NULL;return 0;
}

当我们运行起来后就出问题了
在这里插入图片描述


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

上面我们用malloc函数申请了10个整型空间,然后通过for循环给这10个整型空间内放1~10的整数,有些同学可能会为了方便这样写代码:

#include <stdio.h>
#include <stdlib.h>int main()
{int* p = (int*)malloc(10 * sizeof(int));//判断p是否为空指针if (p == NULL){//打印出错误信息perror("malloc");//终止程序return 1;}//给申请的动态空间内存1~10int i = 0;for (i = 0; i < 5; i++){*p++ = i;}//释放动态内存空间free(p);p = NULLreturn 0;
}

当我们运行起来才发现写出了BUG
在这里插入图片描述
这又是为什么呢?
事实上此时free(p)中的p指针已经不再指向malloc开辟的动态内存的起始地址了,因为*p++这里对p的指向不断递增

free操作的指针必须指向要被释放的动态内存的起始地址


1.5 对同一快动态内存多次释放

当我们用完一块动态内存空间后不再使用对其释放后,可能会因为忘记而重复释放一次,并且如果第一次释放时忘记给p指针赋NULL,那么程序就会出错

	//使用...//释放动态空间free(p);//...free(p);p = NULL;return 0;

但是如果我们两次释放时都给p指针赋了NULL,那基本不会发生什么事,相当于没有错,只是逻辑上讲不通
所以,在我们用free释放完动态内存空间后,紧跟着对指针赋NULL是很有必要的


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

动态开辟的空间一定要释放,并且正确释放

当我们写代码的时候,存在这样一种可能会出现的错误,那就是动态开辟的内存忘记释放或者因为某些原因还没有到free语句就提前终止代码,这里举个简单的例子

#include <stdio.h>
#include <stdlib.h>void text()
{int flag = 1;int* p = (int*)malloc(100);if (p == NULL){return 1;}//使用//因为某些原因函数提前返回了if (flag == 1){return;}//free函数free(p);p = NULL;
}int main()
{//自定义函数text();//后面还有大量代码//....return 0;
}

虽然我们确实用了free函数释放空间,但是当代码量较大时可能会因为某些原因还没到free函数就提前终止了,而我们还没意识到,就算后面我们意识到了这个问题这块内存我们也找不到了
只有整个程序结束后这块内存才能被释放,如果程序一直不结束这块空间就再也找不到了,这就叫内存泄漏

所以,就算动态内存申请使用后用了free,也是有可能犯内存泄漏的错误,我们要多加小心

内存泄漏是比较可怕的,尤其是某些24小时不断运行的服务器程序,如果存在内存泄漏,内存被耗干也只是时间的问题


2、动态内存经典笔试题分析

2.1 题目一

请问运行下面 text函数会有什么后果?

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

上面的代码一共有两个问题
第一个问题:malloc申请动态内存空间后没有使用free函数释放,这可能会导致内存泄漏

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

第二个问题: 函数传参传值调用和传址调用使用错误

这个代码的意思是申请一块动态内存空间地址交给指针p,通过指针p再交给指针str,再使用strcpy函数将字符串拷贝到动态内存空间内,最后打印出字符串
但是get_memory函数传参的时候使用的是传值调用,所以指针p跟指针str没有关系

有两种纠错方法
方法一: 将传值调用改为传址调用,此时p为二级指针

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

在这里插入图片描述

方法二: 直接返回指针p的地址,不需要传参

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

在这里插入图片描述


2.2 题目二

请问运行下面 text函数会有什么后果?

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

上面的代码是一个非常经典的例子,之前在C语言(指针)3中野指针一小节介绍过类似的例子

上面代码的问题:
我们在自定义函数get_memory中创建了一个局部临时数组存入字符串“hello world”,再将字符串的首地址返回用指针str接收,虽然此时指针str确实指向字符串“hello world”的首地址,但是此时str是没有权限访问这块空间的

因为在局部数组p在出了get_memory函数后就销毁了,它申请的空间会被收回,即使指针str能找到这块空间,但是它已经没有权限使用了,此时str就是一个野指针

在这里插入图片描述

所以我们应该避免返回栈空间地址

想要改正上面的代码也很简单,我们申请一块动态内存就行,同时也别忘了释放

#include <stdio.h>
#include <stdlib.h>
#include <string.h>char* get_memory(void)
{char* p = (char*)malloc(20);strcpy(p, "hello world");return p;
}void text(void)
{char* str = NULL;str = get_memory();printf(str);free(str);str = NULL;
}int main()
{text();return 0;
}

2.3 题目三

请问运行下面 text函数会有什么后果?

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

上面的代码是可以打印出“hello world”的,但是遗憾的是上面的代码中使用了动态内存函数malloc,但是没有使用free函数释放动态内存空间
虽然上面的代码可以实现我们想要的效果,但这样的代码是存在安全隐患的

动态内存开辟函数malloccallocrealloc和动态内存释放函数free必须成对出现

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

2.4 题目四

请问运行下面 text函数会有什么后果?

#include <stdio.h>
#include <stdlib.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;
}

使用malloc函数申请一块100个字节大小的动态内存空间放入字符串“hello”,然后使用free函数释放这一动态内存空间
但是此时指针str中还存着我们开辟的动态内存空间的地址,正确的写法free函数后应紧跟str = NULL;,但是上面的代码并没有这一条语句
if语句判断的时候指针str确实是不为空指针的,进入if语句后执行strcpy(str, "world");这条代码,根据我们对strcpy函数的了解,这里还要对指针str解引用,但是指针str我们之前已经用free函数释放过了,并且没有赋NULL所以str此时是野指针不能解引用,运行起来程序就会出错
在这里插入图片描述

这道题考察的还是free函数后紧跟p = NULL的问题


3、柔性数组

3.1 什么是柔性数组

C99中,结构体中的最后一个成员允许是未知大小的数组,这就叫柔性数组成员

  • 在结构体中
  • 最后一个成员
  • 未知大小的数组
struct S1
{int n;char c;double d;int arr[];//未知大小的数组
};
struct S2
{int n;char c;double d;int arr[0];//未知大小的数组
};

上面两种写法中arr都是柔性数组成员
有些编译器可能只支持其中的一种写法,VS中两种写法都支持


3.2 柔性数组的特点

  • 结构中的柔性数组成员前面必须至少有一个其他成员
  • sizeof返回的这种结构大小不包含柔性数组的内存
  • 包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小

在这里插入图片描述

正是因为sizeof返回的这种结构大小不包含柔性数组的内存,所以结构中的柔性数组成员前面必须至少有一个其他成员,否则结构体的大小没法计算


3.3 柔性数组的使用

包含柔性数组的结构怎么使用呢?
包含柔性数组的结构创建变量不会像一般结构那样创建,而是使用malloc函数进行内存的动态分配

#include <stdio.h>
#include <stdlib.h>struct S
{int n;int arr[];
};int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));if (ps == NULL){perror("malloc");//终止程序return;}//使用空间ps->n = 100;int i = 0;for (i = 0; i < 20; i++){ps->arr[i] = i + 1;}//...free(ps);ps = NULL;return 0;
}

在这里插入图片描述

柔性数组的柔性怎么体现呢?
因为上面包含柔性数组的结构是由malloc函数进行内存的动态分配,所以我们可以使用realloc函数进行动态内存的调整,那这个数组的大小就可大可小

#include <stdio.h>
#include <stdlib.h>struct S
{int n;int arr[];
};int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));if (ps == NULL){perror("malloc");//终止程序return 1;}//使用空间ps->n = 100;int i = 0;for (i = 0; i < 20; i++){ps->arr[i] = i + 1;}//调整ps指向的空间大小struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));//进行指针的非空判断,保护原地址if (ptr != NULL){ps = ptr;//防止ptr变成野指针ptr = NULL;}else{perror("realloc");//终止程序return 1;}for (i = 0; i < 40; i++){printf("%d ", ps->arr[i]);}//...free(ps);ps = NULL;return 0;
}

在这里插入图片描述

如果不使用柔性数组,还有一种办法能实现上面的效果

#include <stdio.h>
#include <stdlib.h>struct S
{int n;int* arr;
};int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S));if (ps == NULL){perror("malloc");return 1;}int* tmp = (int*)malloc(20 * sizeof(int));if (tmp == NULL){perror("malloc");return 1;}else{ps->arr = tmp;tmp = NULL;}ps->n = 100;int i = 0;//给指针arr指向的20个整型空间赋值for (i = 0; i < 20; i++){ps->arr[i] = i + 1;}//调整指针arr指向的空间大小tmp = (int*)realloc(ps->arr, 40 * sizeof(int));if (tmp != NULL){ps->arr = tmp;tmp = NULL;}else{perror("realloc");return 1;}for (i = 0; i < 40; i++){printf("%d ", ps->arr[i]);}//...free(ps->arr);ps->arr = NULL;free(ps);ps = NULL;return 0;
}

在这里插入图片描述

结构struct S中有一个指针成员,我们的想法是用malloc函数申请一块动态内存空间,再让结构中的这个指针指向这块动态分配的内存,然后这块由指针指向的动态内存空间就可以用realloc函数进行大小的调整了
可以看到这样实现的效果和柔性数组相似,那柔性数组为什么还要存在呢?
其实相比之下柔性数组还是有它的优势的


3.4 柔性数组的优势

  • 方便内存释放

如果我们的代码是在一个给别人用的函数中,你在里面做了两次内存分配,并把整个结构体返回给用户,用户调佣free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事
所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存释放

  • 这样有利于访问速度

连续的内存有利于提高访问速度,也有利于减少内存碎片
因为malloc等动态内存函数在申请空间时会在堆区允许的地方申请一块连续的空间,但是动态内存函数申请的多个动态内存空间之间并不是连续的,这些空间之间就形成了内存碎片


总结

  • 动态内存管理是一把双刃剑,它能给我们提供灵活的内存管理方式,但同样也会带来风险
  • 检查动态内存分配是否成功:在使用动态内存管理函数时,应该检查分配内存是否成功,以确保程序正常运行,这是比较容易忽略的点

相关文章:

  • Nvidia Jetson/Orin/算能 +FPGA+AI大算力边缘计算盒子:潍柴雷沃智慧农业无人驾驶
  • idea debug时提示”Method breakpoints may dramatically slow down debugging“的解决办法
  • go语言实战--基于Vue3+gin框架的实战Cetide网项目(讲解开发过程中的各种踩坑)
  • Unity学习要点
  • 【线性代数】第一章 概率论的基本概念
  • 关于使用南墙waf防护halo网站主页请求404报错的解决方案
  • 如何把linux安装到单片机中
  • git 空仓库笔记
  • 赚钱而已,你又不是宠物,干嘛让所有人都喜欢你?
  • 数据结构之ArrayList与顺序表(下)
  • Python在股票交易分析中的应用:布林带与K线图的实战回测
  • nginx mirror流量镜像详细介绍以及实战示例
  • 【Vue】练习-mutations的减法功能
  • 玄机平台应急响应—apache日志分析
  • openi启智社区 aarch64 npu环境安装飞桨paddlepaddle和PaddleNLP(失败)
  • 「前端早读君006」移动开发必备:那些玩转H5的小技巧
  • 【知识碎片】第三方登录弹窗效果
  • 11111111
  • Flannel解读
  • pdf文件如何在线转换为jpg图片
  • PHP的类修饰符与访问修饰符
  • SegmentFault 技术周刊 Vol.27 - Git 学习宝典:程序员走江湖必备
  • Spring Security中异常上抛机制及对于转型处理的一些感悟
  • 猴子数据域名防封接口降低小说被封的风险
  • 深入 Nginx 之配置篇
  • 使用SAX解析XML
  • 数据仓库的几种建模方法
  • 它承受着该等级不该有的简单, leetcode 564 寻找最近的回文数
  • 无服务器化是企业 IT 架构的未来吗?
  • 小程序01:wepy框架整合iview webapp UI
  • 一道面试题引发的“血案”
  • 原生 js 实现移动端 Touch 滑动反弹
  • (152)时序收敛--->(02)时序收敛二
  • (M)unity2D敌人的创建、人物属性设置,遇敌掉血
  • (SERIES12)DM性能优化
  • (草履虫都可以看懂的)PyQt子窗口向主窗口传递参数,主窗口接收子窗口信号、参数。
  • (附源码)springboot炼糖厂地磅全自动控制系统 毕业设计 341357
  • (十六)视图变换 正交投影 透视投影
  • (四)图像的%2线性拉伸
  • (未解决)jmeter报错之“请在微信客户端打开链接”
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • .NET “底层”异步编程模式——异步编程模型(Asynchronous Programming Model,APM)...
  • .NET 8 中引入新的 IHostedLifecycleService 接口 实现定时任务
  • .net core 6 redis操作类
  • .NET 表达式计算:Expression Evaluator
  • .NET 某和OA办公系统全局绕过漏洞分析
  • .net 提取注释生成API文档 帮助文档
  • .NET 通过系统影子账户实现权限维持
  • .NET/C# 编译期间能确定的相同字符串,在运行期间是相同的实例
  • .NET导入Excel数据
  • .NET中的Event与Delegates,从Publisher到Subscriber的衔接!
  • .NET周刊【7月第4期 2024-07-28】
  • @manytomany 保存后数据被删除_[Windows] 数据恢复软件RStudio v8.14.179675 便携特别版...
  • [ IO.File ] FileSystemWatcher
  • [<死锁专题>]