C语言 动态内存管理

目录

  • 1. C/C++程序的内存分配
  • 2. 动态内存分配的作用
  • 3. malloc - 分配内存
  • 4. free - 释放内存
  • 5. calloc - 分配并清零内存
  • 6. realloc - 调整之前分配的内存块
  • 7. 常见的动态内存的错误
    • 7.1 对空指针解引用
    • 7.2 对动态开辟空间的越界访问
    • 7.3 对非动态开辟内存使用free
    • 7.4 使用free释放动态开辟内存的一部分
    • 7.5 对同一块动态内存重复释放
    • 7.6 动态开辟内存未释放
  • 8. 动态内存相关题目
    • 8.1 题目1
    • 8.2 题目2
    • 8.3 题目3
    • 8.4 题目4
  • 9. 柔性数组
    • 9.1 柔性数组的定义
    • 9.2 柔性数组的特点
    • 9.3 柔性数组的使用
    • 9.4 柔性数组的优点


正文开始

动态内存管理,顾名思义就是动态的、灵活的管理内存的分配,这在工程中有着重要的用途,下面我们来学习一下如何实现。

1. C/C++程序的内存分配

C/C++程序会分配在以下位置:

  • 栈区(stack):主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等,函数执行结束后自动释放。
  • 堆区(heap):动态内存管理的区域,由程序员自行开辟和释放。
  • 数据段 / 静态区(static):存放全局变量、静态数据。程序结束后由系统释放。
  • 代码段:存放可执行代码、只读常量(例如字符串常量)等。

图例:
在这里插入图片描述

今天我们所学习的动态内存所分配的区域就是堆区(heap)

2. 动态内存分配的作用

通常我们开辟内存的方式有:

//在栈空间上开辟四个字节
int a = 0;//在栈空间上开辟二十个字节的连续空间
char b[20] = { 0 };

但是!上述开辟空间的两种方式有两个很致命的缺点:

  • 空间开辟的大小是固定不变
  • 数组在定义时,就已经确定了数组的长度,后期不能再次调整

在实际需求中,我们对于内存的需求往往是多变的,所以我们需要灵活的、可调整的内存申请方式,C语言中为我们引入了动态内存开辟,让开发者可以自己申请和释放空间。

3. malloc - 分配内存

作用:分配指定字节的未初始化内存;使用该函数须引用头文件stdlib.h,本文其他所学函数也同样须引用此头文件,后文不再赘述。详情戳我>>><stdlib.h>

函数原型:

void* malloc( size_t size );

在这里插入图片描述
malloc 用法:

  • 在栈空间上开辟size个字节的未被使用的空间
  • 函数返回值为void *,需要使用强制类型转换来确定类型
  • 如果开辟成功,则返回一个指向所开辟空间的指针
  • 如果开辟失败,则返回空指针NULL,所以使用 malloc 函数要检查其返回值
  • 如果参数size为0,则该函数行为未定义

例如:

#include <stdio.h>
#include <stdlib.h>int main()
{int num = 0;scanf("%d", &num);int arr[256] = { 0 };int* p = (int*)malloc(num * sizeof(int));//动态内存申请if (p == NULL)//判断是否申请成功return 1;int i = 0;//使用动态内存for (i = 0; i < num; i++){*(p + i) = i;}for (i = 0; i < num; i++){printf("%d ", *(p + i));}return 0;
}

在这里插入图片描述

4. free - 释放内存

作用:释放之前动态内存分配的空间,防止多余的空间占用。

函数原型:

void free( void* ptr );

在这里插入图片描述
free 用法:

  • 释放动态内存空间,即之前由malloc()calloc()aligned_alloc()realloc()所分配的内存
  • 参数ptr指向动态开辟内存的起点,即上述动态内存管理函数的返回值
  • ptr所指向的内存不是动态开辟的或者不是动态开辟内存的起点,则函数行为未定义
  • 若参数为NULL,则函数啥都不干

例如,将上述代码优化一下:

#include <stdio.h>
#include <stdlib.h>int main()
{int num = 0;scanf("%d", &num);int arr[256] = { 0 };int* p = (int*)malloc(num * sizeof(int));//动态内存申请if (p == NULL)//判断是否申请成功return 1;int i = 0;//使用动态内存for (i = 0; i < num; i++){*(p + i) = i;}for (i = 0; i < num; i++){printf("%d ", *(p + i));}//释放内存free(p);//设为空指针,避免野指针的出现p = NULL;return 0;
}

5. calloc - 分配并清零内存

作用:分配内存,并将分配存储中的所有字节初始化为0

函数原型:

void* calloc( size_t num, size_t size );

在这里插入图片描述

calloc 用法:

  • calloc 函数将num个大小为size的元素开辟一块空间,并且把空间的每个字节都初始化为0
  • 若开辟成功,返回值为指向开辟空间的首地址的指针
  • 若开辟失败,返回值为空指针NULL

例如:

#include <stdio.h>
#include <stdlib.h>int main()
{//申请空间int* p = (int*)calloc(10, sizeof(int));//判断是否申请成功if (p == NULL)return 1;*p = 2;int i = 0;for (i = 0; i < 10; i++){printf("%d ", *(p + i));}//释放内存free(p);//置为空指针,避免野指针的出现p = NULL;return 0;
}

malloc 和 calloc 对比:
在这里插入图片描述

6. realloc - 调整之前分配的内存块

作用:对动态开辟内存大小进行调整

函数原型:

void *realloc( void *ptr, size_t new_size );

在这里插入图片描述

realloc 用法:

  • ptr所指向的空间调整为new_size个字节的大小
  • ptr所指向的空间必须是动态开辟内存
  • 若待调整空间后面有足够大的空间,则直接在原有内存之后追加空间,原数据不发生变化
  • 若待调整空间后面没有足够大的空间,则重新在堆空间上另找一个合适大小的连续空间使用,并将原数据复制到新空间
  • 若成功,则返回指向新分配内存的指针;若失败,则返回空指针NULL

例如:

#include <stdio.h>
#include <stdlib.h>int main()
{//申请动态内存int* ptr = (int*)malloc(100);//判断是否申请成功if (ptr == NULL)return 1;//使用申请的空间//...//调整动态内存大小//1.直接使用待调整空间的地址接收返回值ptr = realloc(ptr, 200);//2.使用中间变量接收返回值int* p = (int*)realloc(ptr, 300);if (p == NULL)//判断是否调整成功return 1;ptr = p;return 0;
}

上述代码中,书写了两种接收 realloc 函数返回值的方式,我们更推荐第二种方式。第一种方式中,直接使用待调整空间的地址接收返值,若调整失败,则会返回空指针NULL,这样的话,原数据就会丢失。而第二种方式则是在确保了调整成功的情况下才将待调整空间的地址接收返回值,更为安全。

7. 常见的动态内存的错误

注:以下代码均为错误示范

7.1 对空指针解引用

void test1()
{int *p = (int*)malloc(40);//若开辟失败,则返回空指针,没有进行判断就直接解引用*p = 2;free(p);
}

7.2 对动态开辟空间的越界访问

void test2()
{int* p = (int*)malloc(20);if (p == NULL)return 1;//只能存放五个整型,所以越界访问了*(p + 5) = 3;free(p);
}

7.3 对非动态开辟内存使用free

void test3()
{int a = 0;int* p = &a;//非动态开辟内存free(p);
}

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

void test4()
{int *p = (int*)malloc(100);p++;//p不再是动态开辟内存的起点free(p);
}

7.5 对同一块动态内存重复释放

void test5()
{int *p = (int*)malloc(100);free(p);free(p);//重复释放
}

7.6 动态开辟内存未释放

void test()
{int *p = (int*)malloc(100);if(p == NULL)return 1;//申请完未释放//出了函数后使用者也不能再使用这一块空间//操作系统也没使用权限//造成了内存泄漏
}int main()
{test();while(1);
}

所以在使用动态内存的时候,要确保在哪个函数内申请的空间,就在哪个函数内正确释放掉,否则就会出现内存泄漏

8. 动态内存相关题目

8.1 题目1

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

在主函数中调用了Test()函数,Test()函数中调用了GetMemory()函数,其中 str 作为参数传递进去,但 str 是一个指针,GetMemory()函数的参数是一个指针,所以将 str 传递进去就相当于传值调用,也就是说,GetMemory()函数并没有真正的改变 str 所指向的地址,它依旧为空指针,传进 strcpy 函数的第一个参数是一个空指针,这就导致了程序崩溃

可修改为:

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

8.2 题目2

#include <stdio.h>
#include <stdlib.h>
#include <string.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,并且在Test()中使用 str 接收,但char p[]变量在Test()中已经销毁了,使用权限已经还给操作系统了
也就是说,GetMemory()仅仅是将一个地址传递了出去,但地址所指向的内存已经没有使用权限了
那么 str 接收地址后,就变成了一个野指针,所指向的內容是不确定的

8.3 题目3

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

上述代码唯一的问题就是,使用完动态内存后没有将动态内存释放

8.4 题目4

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

上述代码中已经将 str 释放掉了,将使用权限还给了操作系统,后面却继续使用 str,导致非法访问内存空间

9. 柔性数组

当我们要保存相同类型的数据的时候,首先想到的肯定就是使用数组了,但是数组有着很大的限制,它并不能够根据使用者的需求来灵活的调整大小,尽管C99提供了变长数组的功能,但当他一旦确定了大小,后续的使用中同样也不可以改变
而柔性数组就可以完美地解决这个问题,下面我们一起学习一下

9.1 柔性数组的定义

C99中,结构中的最后一个元素允许是未知大小的数组,称为柔性数组成员

例如:

struct Sarr
{int i;int a[0];//或者int a[];
};

9.2 柔性数组的特点

柔性数组有以下特点:

  • 结构中的柔性数组成员前必须有一个或多个其他成员
  • 柔性数组成员的大小是未知的
  • sizeof 返回的这类结构的大小不包括柔性数组的内存

例如:

#include <stdio.h>struct Sarr
{int i;int a[0];//或者int a[];
};int main()
{printf("%zd\n", sizeof(struct Sarr));return 0;
}

运行结果:
在这里插入图片描述

9.3 柔性数组的使用

我们可以通过 malloc() 函数对柔性数组成员的结构进行动态内存分配,其中分配的内存应该大于结构的大小,以适应柔性数组的预期大小

例如:

#include <stdio.h>
#include <stdlib.h>struct Sarr
{int i;int a[0];
};int main()
{//柔性数组成员申请内存struct Sarr* p = (struct Sarr*)malloc(sizeof(struct Sarr) + 20 * sizeof(int));int i = 0;//柔性数组的使用p->i = 20;for (i = 0; i < 20; i++){p->a[i] = i;}for (i = 0; i < 20; i++){printf("%d ", p->a[i]);}//释放动态内存free(p);p = NULL;return 0;
}

运行结果:
在这里插入图片描述

9.4 柔性数组的优点

上述代码也能写成这样:

//代码2
#include <stdio.h>
#include <stdlib.h>struct Sarr
{int i;int* p_a;
};int main()
{//给变量p开辟结构体大小的空间struct Sarr* p = (struct Sarr*)malloc(sizeof(struct Sarr));//指定数组大小p->i = 20;//给数组开辟空间p->p_a = (struct Sarr*)malloc(p->i * sizeof(int));int i = 0;//使用数组for (i = 0; i < 20; i++){p->p_a[i] = i;}for (i = 0; i < 20; i++){printf("%d ", p->p_a[i]);}//释放动态内存free(p->p_a);p->p_a = NULL;free(p);p = NULL;return 0;
}

运行结果:
在这里插入图片描述

上述代码同样实现了柔性数组的功能,但使用柔性数组有两个好处:

  • 方便内存释放:在代码2中,我们首先对结构的内存进行了分配,然后再对结构中的成员进行了内存分配,这样当我们使用完毕后释放内存时,就需要释放两次内存;而使用柔性数组就需要释放一次,一步到位!
  • 访问速度快:连续的内存有益于提高访问速度,也有益于减少内存碎片(多块使用中的内存之间的部分)


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

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

相关文章

发电机组远程管理,提升管控力,降低运维成本

发电机组是指发电机发动机以及控制系统的总称&#xff0c;用来把发动机提供的动能转化为电能。它通常由动力系统、控制系统、消音系统、减震系统、排气系统组成。发电机组远程管理系统利用物联网技术与PLC远程控制模块集成解决方案&#xff0c;在提高发电机组的运行效率、降低运…

【计算机科学速成课】笔记三——操作系统

文章目录 18.操作系统问题引出——批处理设备驱动程序多任务处理虚拟内存内存保护Unix 18.操作系统 问题引出—— Computers in the 1940s and early 50s ran one program at a time. 1940,1950 年代的电脑&#xff0c;每次只能运行一个程序 A programmer would write one at…

Django框架四-项目

一、项目准备 1.流程与人员 2.需求分析 项目主要页面 归纳项目主要模块 3.架构设计 项目开发模式 项目架构设计

【C++STL详解(八)】--------stack和queue的模拟实现

目录 前言 一、stack模拟实现 二、queue的模拟实现 前言 前面也介绍了stack和queue的常见接口&#xff0c;我们也知道stack和queue实际上是一种容器适配器&#xff0c;它们只不过是对底层容器的接口进行封装而已&#xff0c;所以模拟实现起来比较简单&#xff01;一起来看看是…

pxe远程安装

PXE 规模化&#xff1a;可以同时装配多台服务器 自动化&#xff1a;自动安装操作系统和各种配置 不需要光盘U盘 前置需要一台PXE服务器 pxe是预启动执行环境&#xff0c;再操作系统之前运行 实验&#xff1a; 首先先关闭防火墙等操作 [rootlocalhost ~]# systemc…

【busybox记录】【shell指令】uniq

目录 内容来源&#xff1a; 【GUN】【uniq】指令介绍 【busybox】【uniq】指令介绍 【linux】【uniq】指令介绍 使用示例&#xff1a; 去除重复行 - 默认输出 去除重复行 - 跳过第n段&#xff08;空格隔开&#xff09;&#xff0c;比较n1以后的内容&#xff0c;去重 去…

使用Express+Node.js搭建网站

Express是一个基于Node.js平台的快速、开放、极简的Web开发框架。它的作用是专门用来创建Web服务器&#xff0c;与Node.js内置的http模块功能相似&#xff0c;但更为简便和高效。 Express中文官网&#xff1a;Express - 基于 Node.js 平台的 web 应用开发框架 - Express中文文…

Vulnhub项目:NAPPING: 1.0.1

1、靶机介绍 靶机地址&#xff1a;Napping: 1.0.1 ~ VulnHub 2、渗透过程 老规矩&#xff0c;先探测&#xff0c;靶机ip&#xff1a;192.168.56.152 本机ip&#xff1a;192.168.56.146 来看一看靶机开放哪些端口&#xff0c;nmap一下 nmap -sS -sV -A -T5 192.168.56.152 开…

k8s ReplicaSet

ReplicaSet 是替代 ReplicationController 的&#xff0c;ReplicaSet 的行为与 ReplicationController 完全相同&#xff0c; 但pod 选择器的表达能力更强。 ReplicaSet 和 ReplicationController 的区别&#xff1a; ReplicationController 的标签选择器只允许包含某个标签的…

基于SpringBoot的大学生心理咨询系统

项目介绍 基于Spring Boot技术栈构建的大学生心理咨询系统&#xff0c;旨在提供一个全方位、定制化的心理健康管理平台。系统采用前后端分离架构&#xff0c;后端利用Spring Boot框架进行深度二次开发&#xff0c;以实现高效稳定的服务端逻辑处理和数据交互&#xff1b;前端界…

Dynamics 365: 从0到1了解如何创建Custom API(3) - Custom API的调试之插件调试

对于Custom API的调试&#xff0c;主要有三种方式&#xff1a; 插件代码中添加log插件调试单元测试 对于这三种方式&#xff0c;说白了也就相当于两种&#xff0c;第一种打log&#xff0c;这种方式很多时候我们是在插件调试突然不好使的时候&#xff0c;或者在不调试时还想看…

7-zip下载、安装

7-Zip 官方中文网站 (sparanoid.com) 7-Zip - 程序下载 (sparanoid.com)

【Linux】文件内容相关的命令,补充:管道符

1、查看文件内容 &#xff08;1-1&#xff09;查看文件内容&#xff1a;cat&#xff0c;tac&#xff0c;head&#xff0c;tail 查看文件内容cat 文件名查看文件内容并显示行号cat -n 文件名倒着查看文件内容&#xff08;从最后一行开始&#xff09;tac 文件名查看文件前10行…

latex参考文献引用网址,不显示网址问题

以引用UCI数据集为例 1、加入宏包 \usepackage{url} 2、在参考文献bib文件中加入网址文献 misc{UCI, author {{D. Dua, E. Karra Taniskidou}}, year {2024}, title {UCI Machine Learning Repository}, howpublished {\url{http://archive.ics.uci.edu/ml}} } 完成&#x…

【机器学习系统的构建】从模型开发的过程讲清楚K-Fold 交叉验证 (Cross-Validation)的原理和应用

0、前言 最近在学习集成学习的时候了解到了k折交叉验证&#xff0c;其实在之前学习吴恩达老师的课程中也学过交叉验证&#xff0c;但是当时也不是很明白。这次借着自己的疑问以及网上搜找资料&#xff0c;终于把交叉验证给弄明白了。 在弄清楚前&#xff0c;我有这样几个疑问…

【typescript 小秘籍 - 类型自动推导】

今天发现个typescript的小技巧&#xff0c;原来在vscode里面 typescript是可以根据数据&#xff0c;自动推导其类型的&#xff0c;这样就不用自己去手敲定义了。比如 鼠标移动到person上&#xff0c;可以看到 其自动推导了person的类型 然后直接复制下来 直接使用即可。

Python运维-日志记录、FTP、邮件提醒

本章目录如下&#xff1a; 五、日志记录 5.1、日志模块简介 5.2、logging模块的配置与使用 六、搭建FTP服务器与客户端 6.1、FTP服务器模式 6.2、搭建服务器 6.3、编写FTP客户端程序 七、邮件提醒 7.1、发送邮件 7.2、接收邮件 7.3、实例&#xff1a;将报警信息实时…

基于Flask的岗位就业可视化系统(一)

&#x1f31f;欢迎来到 我的博客 —— 探索技术的无限可能&#xff01; &#x1f31f;博客的简介&#xff08;文章目录&#xff09; 前言 本项目综合了基本数据分析的流程&#xff0c;包括数据采集&#xff08;爬虫&#xff09;、数据清洗、数据存储、数据前后端可视化等 推荐…

ChatGPT-Next-Web漏洞利用分析(CVE-2023-49785)

1. 漏洞介绍 ​ 日常网上冲浪&#xff0c;突然粗看以为是有关Chat-GPT的CVE披露出来了&#xff0c;但是仔细一看原来是ChatGPT-Next-Web的漏洞。漏洞描述大致如下&#xff1a;&#xff08;如果有自己搭建了还没更新的速速修复升级防止被人利用&#xff0c;2.11.3已经出来了&am…

个人IP打造孵化运营产业链商业计划书

【干货资料持续更新&#xff0c;以防走丢】 个人IP打造孵化运营产业链商业计划书 部分资料预览 资料部分是网络整理&#xff0c;仅供学习参考。 PPT共90页&#xff08;完整资料包含以下内容&#xff09; 目录 个人IP运营方案&#xff1a; 1. 个人IP定位与构建 1.1 人格画像构…