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等动态内存函数在申请空间时会在堆区允许的地方申请一块连续的空间,但是动态内存函数申请的多个动态内存空间之间并不是连续的,这些空间之间就形成了内存碎片


总结

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构--线性表和串

个人介绍 hello hello~ &#xff0c;这里是 code袁~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的…

用 Notepad++ 写 Java 程序

安装包 百度网盘 提取码&#xff1a;6666 安装步骤 双击安装包开始安装。 安装完成&#xff1a; 配置编码 用 NotePad 写 Java 程序时&#xff0c;需要设置编码。 在 设置&#xff0c;首选项&#xff0c;新建 中进行设置&#xff0c;可以对每一个新建的文件起作用。 Note…

ElasticSearch教程(详解版)

本篇博客将向各位详细介绍elasticsearch&#xff0c;也算是对我最近学完elasticsearch的一个总结&#xff0c;对于如何在Kibana中使用DSL指令&#xff0c;本篇文章不会进行介绍&#xff0c;这里只会介绍在java中如何进行使用&#xff0c;保证你看完之后就会在项目中进行上手&am…

3072. 将元素分配到两个数组中 II

题目 给你一个下标从 1 开始、长度为 n 的整数数组 nums 。 现定义函数 greaterCount &#xff0c;使得 greaterCount(arr, val) 返回数组 arr 中 严格大于 val 的元素数量。 你需要使用 n 次操作&#xff0c;将 nums 的所有元素分配到两个数组 arr1 和 arr2 中。在第一次操…

四十二、openlayers官网示例Flight Animation扩展——在地图上绘制飞机航线、飞机随航线飞行效果

上篇在地图上绘制了动态的飞机航线&#xff0c;于是我想着&#xff0c;能不能加个飞机的图标跟着航线飞行。 在iconfont上下载一个飞机的svg图形&#xff0c;放在public的data/icons下面 因为图标需要随着航线的方向飞行&#xff0c;需要根据航线调整角度&#xff0c;因此在…

FPGA SPI采集ADC7606数据

一,SPI总线的构成及信号类型 SPI总线只需四条线(如图1所示)就可以完成MCU与各种外围器件的通讯: 1)MOSI – Master数据输出,Slave数据输入 2)MISO – Master数据输入,Slave数据输出 3)SCK – 时钟信号,由Master产生 4)/CS – Slave使能信号,由Master控制。 在一个SPI时…

递归【2】(组合回溯(生成括号)、子集回溯(背包问题))

括号对 &#xff08;组合型回溯&#xff09; 分解成子问题&#xff0c;每一次添加括号分两步&#xff1a; if左括号小于n&#xff0c;加左括号&#xff0c;然后k(index1), if左括号大于有括号&#xff0c;加右括号&#xff0c;k(index1),然后收尾括号单独考虑&#xff0c;到…

core dump核心转储

检查核心转储是否开启&#xff0c;否则无法生成core文件 ulimit -a 如果为0就需要修改 ulimit -c 10240 写一个会触发core命令的程序 以浮点数运算为例 #include <iostream>int main() {int i 1/0; } 在编译时使用-g选项 运行程序&#xff0c;生成core文件 gdb调试 g…

AI大模型在广告领域的应用

深度对谈&#xff1a;广告创意领域中AIGC的应用_生成式 AI_Tina_InfoQ精选文章

ChatGPT-4o, 腾讯元宝,通义千问对比测试中文文化

国内的大模型应用我选择了国内综合实力最强的两个&#xff0c;一个是腾讯元宝&#xff0c;一个是通义千问。其它的豆包&#xff0c;Kimi&#xff0c;文心一言等在某些领域也有强于竞品的表现。 问一个中文文化比较基础的问题,我满以为中文文化chatGPT不如国内的大模型。可事实…

【经典排序算法】堆排序(精简版)

什么是堆排序&#xff1a; 堆排序(Heapsort)是指利用堆&#xff08;完全二叉树&#xff09;这种数据结构所设计的一种排序算法&#xff0c;它是选择排序的一种。需要注意的是排升序要建大堆&#xff0c;排降序建小堆。 堆排序排序的特性总结&#xff1a; 1. 堆排序使用堆来选数…

vivado DIAGRAM、HW_AXI

图表 描述 块设计&#xff08;.bd&#xff09;是在IP中创建的互连IP核的复杂系统 Vivado设计套件的集成商。Vivado IP集成器可让您创建复杂的 通过实例化和互连Vivado IP目录中的IP进行系统设计。一块 设计是一种分层设计&#xff0c;可以写入磁盘上的文件&#xff08;.bd&…

软考架构-计算机网络考点

会超纲&#xff0c;3-5分 网络分类 按分布范围划分 局域网 LAN 10m-1000m左右 房间、楼宇、校园 传输速率高 城域网 MAN 10km 城市 广域网 WAN 100km以上 国家或全球&#xff08;英特网&#xff09; 按拓扑结构划分 总线型&#xff1a;利用率低、干…

(2024,Vision-LSTM,ViL,xLSTM,ViT,ViM,双向扫描)xLSTM 作为通用视觉骨干

Vision-LSTM: xLSTM as Generic Vision Backbone 公和众与号&#xff1a;EDPJ&#xff08;进 Q 交流群&#xff1a;922230617 或加 VX&#xff1a;CV_EDPJ 进 V 交流群&#xff09; 目录 0. 摘要 2 方法 3 实验 3.1 分类设计 4 结论 0. 摘要 Transformer 被广泛用作计算…

基于深度学习的在线选修课程推荐系统

基于深度学习的在线选修课程推荐系统 1、效果图 点我查看Demo 2、功能 可联系我-微-信(1257309054) 登录注册、点赞收藏、评分评论&#xff0c;课程推荐&#xff0c;热门课程&#xff0c;个人中心&#xff0c;可视化&#xff0c;后台管理&#xff0c;课程选修3、核心推荐代…

Edge浏览器十大常见问题,一次性解决!

Edge曾被称为最好用的浏览器&#xff0c;拳打Chrome脚踢firefox, 可如今却隐藏着像是播放卡顿、下载缓慢、广告繁多等诸多问题&#xff0c;不知道各位还在用吗&#xff1f; 今天小编收集整理了Edge浏览器十大烦人问题&#xff0c;并提供简单有效的解决办法&#xff0c;让你的E…

277 基于MATLAB GUI火灾检测系统

基于MATLAB GUI火灾检测系统&#xff0c;可以实现图片和视频的火苗检测。火焰识别的三个特征&#xff1a;1个颜色特征&#xff0c;2个几何特征颜色特征&#xff1a;HSV颜色空间下&#xff0c;对三个通道值进行阈值滤波&#xff0c;几何特征1&#xff1a;长宽比&#xff0c;几何…

实战 | YOLOv10 自定义数据集训练实现车牌检测 (数据集+训练+预测 保姆级教程)

导读 本文主要介绍如何使用YOLOv10在自定义数据集训练实现车牌检测 (数据集训练预测 保姆级教程)。 YOLOv10简介 YOLOv10是清华大学研究人员在Ultralytics Python包的基础上&#xff0c;引入了一种新的实时目标检测方法&#xff0c;解决了YOLO以前版本在后处理和模型架构方面…

mac M1下安装PySide2

在M1下装不了PySide2, 是因为PySide2没有arm架构的包 1 先在M1上装qt5 安装qt主要是为了能用里面的Desinger, uic, rcc brew install qt5 我装完的路径在/opt/homebrew/opt/qt5 其中Designer就是用来设计界面的 rcc用resource compiler, 编绎rc资源文件的, 生成对应的py文件…

电子电气架构——车载诊断DTC一文通

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 没有人关注你。也无需有人关注你。你必须承认自己的价值,你不能站在他人的角度来反对自己。人生在世,最怕的就是把别人的眼光当成自己生活的唯一标…