深度刨析C语言中的动态内存管理

文章目录

  • 1.为什么会存在动态内存分配
  • 2.动态内存函数介绍
    • 2.1 [malloc](https://legacy.cplusplus.com/reference/cstdlib/malloc/?kw=malloc)与[free](https://legacy.cplusplus.com/reference/cstdlib/free/?kw=free)
    • 2.2 [calloc](https://legacy.cplusplus.com/reference/cstdlib/calloc/?kw=calloc)
    • 2.3 [realloc](https://legacy.cplusplus.com/reference/cstdlib/realloc/?kw=realloc)
  • 3.常见的动态内存错误
    • 3.1 对NULL指针的解引用操作
    • 3.2 对动态开辟空间的越界访问
    • 3.3对非动态开辟的内存使用free释放
    • 3.4 使用free释放动态开辟内存的一部分
    • 3.5 对同一块动态内存多次释放
    • 3.6 动态开辟内存忘记释放(内存泄漏)
  • 4.经典笔试题
    • 4.1 练习1
    • 练习2
    • 练习3
    • 练习4
  • 5.C/C++程序的内存开辟
  • 6.柔性数组
    • 6.1 柔性数组的特点
    • 6.2 柔性数组的使用
    • 6.3 柔性数组的优势

1.为什么会存在动态内存分配

截至目前,我们已经掌握了两种内存开辟的方式了:

  • 单个变量的创建
  • 数组的创建
int a = 10;//在栈空间开辟4个字节
int arr[10] = {0};//在栈空间开辟了40个字节的连续空间

在上述的开辟空间的方式有两个特点:

  • 空间开辟的大小是固定的。
  • 数组在声明的时候,必须指定数组的大小,它所需的内存早编译时就已经分配。
    但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组这种在编译时就开辟空间的方式就不在合适了。
    这时候只能动态开辟了。

2.动态内存函数介绍

2.1 malloc与free

C语言提供了一个动态内存开辟的函数:

void* malloc(size_t size);

这个函数向内存申请一块连续可用的空间,并返回这块空间的指针。

  • 如果开辟成功,则返回开辟好空间的地址。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候,使用者自己来决定
  • 如果参数size为0,malloc的行为是标准未定义的,这取决于编译器,所以不要传0作为参数
    malloc申请空间,申请到后直接返回这块空间的起始地址,不会初始化空间的内容
    malloc申请的内存空间,当程序退出时,还给操作系统。当程序不退出,动态申请的内存不会主动释放,需要使用free函数来释放

C语言还提供了另一个函数,专门用来做动态内存的释放和回收的。

void free(void* ptr);

free函数是用来释放开辟的内存的。

  • 如果参数ptr指向的空间不是动态开辟的,那么fre函数的行为是标准未定义的。
  • 如果参数ptr是NULL指针,则函数什么事都不做。
    malloc和free函数都在stdlib.h头文件里。
#include <stdio.h>
#include <stdlib.h>
int main()
{int* p = (int* )malloc(10*sizeof(int));if(p == NULL){//开辟失败,打印错误原因perror("malloc");return 1;}for(int i = 0;i<10;++i){printf("%d ",p[i]);}free(p);p = NULL;return 0;
}
//打印结果:
/*
-842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451
*/

2.2 calloc

C语言还提供另一个函数叫calloccalloc函数也用来动态内存分配。

void* calloc(size_t num,size_t size);
  • 函数功能是为num个大小为size的元素开辟一块空间,并把空间的每个字节都初始化为0.
  • 与函数malloc的区别只在于calloc会在返回地址之前把申请的空间每个字节店铺初始化为0.
#include <stdio.h>
#include <stdlib.h>
int main()
{int* p = (int* )calloc(10,sizeof(int));if(p == NULL){//开辟失败,打印错误原因perror("calloc");return 1;}for(int i = 0;i<10;++i){printf("%d ",p[i]);}free(p);p = NULL;return 0;
}
//打印结果:
//0 0 0 0 0 0 0 0 0 0

初始化为0

所以说如果我们对申请的空间需要初始化,可以使用calloc来初始化为0.

2.3 realloc

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

realloc的出现让动态内存管理更加灵活。
有时我们会发现过去申请的空间太小了,有时候又可能觉得申请的空间太大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存的大小进行调整。

  • ptr是要调整的内存地址
  • size调整之后新的大小。
  • 返回值为调整后的内存起始位置。
  • 这个函数调整原内存大小的基础上,还会将原来内存中的数据移动到新的空间。
  • realloc在调整内存空间的时候存在两种情况:
    • 原有空间之后有足够大的空间
    • 原有空间后没有足够大的空间
      realloc处理的两种情况
      情况1:
      因为后续的空间充足,要扩展内存就直接在原有内存后直接追加空间,原空间的数据不会发生变化。
      情况2:
      因为后续的空间不足,扩展的方法是:在堆空间上另找一个合适的大小的连续空间来使用。同时会将原空间的数据拷贝到新的空间,然后原空间就被释放了。函数也就返回一个新的地址。
#include <stdio.h>
#include <stdlib.h>
int main()
{int* ptr = (int*)malloc(20);if(ptr==NULL){//检查}for(int i = 0;i<5;++i){ptr[i] = i+1;}int* tmp = (int*)realloc(ptr,40);if(tmp == NULL){//检查}ptr = tmp;for(int i = 0;i<10;++i){printf("%d ",ptr[i]);}free(ptr);ptr = NULL;return 0;
}
//打印结果
/*
1 2 3 4 5 -842150451 -842150451 -842150451 -842150451 -842150451
*/

3.常见的动态内存错误

3.1 对NULL指针的解引用操作

void test()
{int* p = (int*)malloc(INT_MAX/4);*p = 20;free(p);
}

解释:malloc是无法开辟特别大的内存空间的,当空间开辟失败时就会返回NULL指针。而对NULL解引用程序是会崩溃的。

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

void test()
{int* p = (int*)malloc(40);if(p==NULL){perror("malloc");return 1;}for(int i = 0;i<=10;++i){p[i] = i+1;//当i == 10时越界访问}free(p);p = NULL;
}

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

void test()
{int a = 10;int* p = &a;free(p);p = NULL;
}

解释:如果参数p指向的空间不是动态开辟的,那么free函数的行为是标准未定义的。程序会崩溃

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

void test()
{int* p = (int*)malloc(40);for(int i = 0;i<5;++i){*p = i+1;p++;}free(p);p = NULL;
}

释放空间时只能传递该动态开辟空间的起始地址,因为在起始地址附近会存在该开辟内存的信息,free函数只有找到了这个信息才能释放内存

3.5 对同一块动态内存多次释放

void test()
{int* p = (int*)malloc(40);free(p);free(p);
}

为了避免这个情况,每次在释放空间后就把指针置为NULL,free不会对NULL指针进行操作。

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

void test()
{int* p = (int*)malloc(100);if(p == NULL){//退出}
}
int main()
{for(int i = 0;i<100;++i){test();}
}

忘记释放不再使用的动态开辟的空间,会导致内存泄漏。
所以动态开辟的空间一定要记得释放。

4.经典笔试题

4.1 练习1

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void getmemory(char* p)
{p = (char*)malloc(100);
}
void test()
{char* str = NULL;getmemory(str);strcpy(str,"hello world");printf("%s\n",str);
}
int main()
{test();return 0;
}
//程序崩溃

为什么会导致程序崩溃呢?
在test函数中我们创建了一个str指针指向NULL,然后就直接把指针变量传入getmemory函数中,这里的传参是传值调用,对p的修改是不会影响到str的也就造成了,在strcpy函数中对NULL指针解引用的错误。
改正:把str的地址传到getmemort中,用二级指针接收。

void getmemory(char** p)
{*p = (char*)malloc(100);
}
void test()
{char* str = NULL;getmemory(&str);strcpy(str,"hello world");printf("%s\n",str);
}

练习2

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* getmemory()
{char p[] = "hello world";return p;
}
void test()
{char* str = NULL;str = getmemory();printf("%s\n",str);
}
int main()
{test();return 0;
}
//打印结果
//烫烫烫烫烫烫澌5騃

众所周知,函数中在栈上开辟的变量的作用域和生命周期都是在该函数内,在getmemory创建了一个字符数组然后再把这个字符数组返回,这样是不行的,一当getmemort函数结束,字符数组的生命周期也就结束了,内存要还给操作系统的,还给操作系统后如果被操作系统重新利用里面原先的值就会被覆盖也就造成了打印烫烫烫的局面。

练习3

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void getmemory(char** p,int num)
{*p = (char*)malloc(num);
}
void test()
{char* str = NULL;getmemory(&str,100);strcpy(str,"hello world");printf("%s\n",str);
}
int main()
{test();return 0;
}
//打印结果
//hello world

好像没问题对吧,和第一题的修改如出一辙,不过这个程序的问题就是内存泄漏了,没有用free释放空间。

练习4

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void test()
{char* str = (char*)malloc(100);strcpy(str,"hello");free(str);if(str!=NULL){strcpy(str,"world");printf("%s\n",str);}
}
int main()
{test();return 0;
}
//打印结果
//world

free释放后,str指向的空间就已经还给操作系统了,后续的使用都是不行的,这块空间已经不属于你了,相当于野指针。

5.C/C++程序的内存开辟

C/C++程序内存分配的几个区域:

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

数据在内存中的储存
有了这幅图,我们就能更好的理解在《C语言篇章》中讲的static关键字修饰的局部变量的例子了。

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量是存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,使用生命周期变长。

6.柔性数组

也许你从未听说过柔性数组,这个概念,但是它确实存在。
在c99中,规定:结构中的最后一个元素允许未知大小的数组,这就叫做柔性数组成员。

struct st_type
{int i;int a[0];//柔性数组成员
};

有些编译器会报错无法编译的话写成

struct st_type
{int i;int a[];
};

6.1 柔性数组的特点

  • 结构中柔性数组成员前面必须至少一个其他成员。
  • sizeof返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组的结构用malloc()函数进行内存的动态分配,并且分配的内存一个大于内存结构的大小,以适用柔性数组的预期大小。
#include <stdio.h>
typedef struct st_type
{int i;int a[0];//柔性数组成员
}type_a;int main()
{printf("%d\n",sizeof(type_a));
}
//打印结果:
//4

6.2 柔性数组的使用

//代码1
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{int i;int a[0];//柔性数组成员
}type_a;int main()
{type_a* p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));p->i = 100;for(int i = 0;i<100;++i){p->a[i] = i;}free(p);p = NULL;return 0; 
}

这样开辟的话,柔性数组成员a就相当于获得了100个整型元素的连续空间。

6.3 柔性数组的优势

你可能会想,这个柔性数组,我用一个指针也能达到相同的效果啊。也确实是这样的。

//代码2
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{int i;int* pa;
}type_a;int main()
{type_a* p = (type_a*)malloc(sizeof(type_a));p->i = 100;p->pa = (int*)malloc(p->i*sizeof(int));for(int i = 0;i<100;++i){p->a[i] = i;}free(p->pa);p->pa = NULL;free(p);p = NULL;return 0; 
}

上面的代码1和代码2都是可以完成相同的功能的,都是代码1的实现有两个好处:
好处1:方便内存释放

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

好处2:有利于访问速度

连续的内存有益于提高访问速度,也有益于减少内存碎片。

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Unity 批处理详讲(含URP)

咱们在项目中&#xff0c;优化性能最重要的一个环节就是合批处理&#xff0c;&#xff0c;在早期Unity中&#xff0c;对于合批的处理手段主要有三种&#xff1a; Static Batching Dynamic Batching GPU Instancing 如今Unity 为了提升合批范围与效率&#xff0c;提供了…

防火墙中的会话表及用户认证

防火墙相关技术&#xff1a; 1.会话表技术 会话表技术 --- 提高转发效率的关键 --- 老化机制 1&#xff0c;会话表老化时间过长 --- 占用资源&#xff0c;导致一些会话无法正常建立 2&#xff0c;老化时间过短 --- 会导致一些需要长时间发送一次的报文强行终端&#xff0c;…

Windows下ORACLE数据泵expdp和impdp使用

Windows下ORACLE数据泵expdp和impdp使用 一、基础环境 操作系统&#xff1a;Windows server 2008&#xff1b; 数据库版本&#xff1a;Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production 数据库工具&#xff1a;PL/SQL 12.0.7 实验内容&…

Python和MATLAB网络尺度结构和幂律度大型图生成式模型算法

&#x1f3af;要点 &#x1f3af;算法随机图模型数学概率 | &#x1f3af;图预期度序列数学定义 | &#x1f3af;生成具有任意指数的大型幂律网络&#xff0c;数学计算幂律指数和平均度 | &#x1f3af;随机图分析中巨型连接分量数学理论和推论 | &#x1f3af;生成式多层网络…

C语言航空售票系统

以下是系统部分页面 以下是部分源码&#xff0c;需要源码的私信 #include<stdio.h> #include<stdlib.h> #include<string.h> #define max_user 100 typedef struct ft {char name[50];//名字char start_place[50];//出发地char end_place[50];//目的地char …

【状态机动态规划 状态压缩】1434. 每个人戴不同帽子的方案数

本文涉及知识点 位运算、状态压缩、枚举子集汇总 动态规划汇总 LeetCode 1434. 每个人戴不同帽子的方案数 总共有 n 个人和 40 种不同的帽子&#xff0c;帽子编号从 1 到 40 。 给你一个整数列表的列表 hats &#xff0c;其中 hats[i] 是第 i 个人所有喜欢帽子的列表。 请你…

ipsec协议簇(详解)

IPSEC协议簇 IPSEC协议簇 --- 基于网络层的&#xff0c;应用密码学的安全通信协议组 IPV6中&#xff0c;IPSEC是要求强制使用的&#xff0c;但是&#xff0c;IPV4中作为可选项使用 IPSEC可以提供的安全服务 机密性 --- 数据加密 完整性 --- 防篡改可用性 数据源鉴别 -- 身份…

拼多多海外版temu平台官网,temu平台官网入口

在跨境电商领域&#xff0c;拼多多旗下的Temu平台正以惊人的速度崛起&#xff0c;成为众多卖家和消费者关注的焦点。今天&#xff0c;我们将深入探索拼多多海外版Temu平台的官网及其入口&#xff0c;带您领略这一跨境电商新蓝海的魅力。 做TEMU看数据用特喵数据&#xff0c;热…

中小银行数字化转型该怎么进行?银行数字化案例鉴赏

中小银行在发展中面临五大困境&#xff0c;国内“zx”&#xff08;这里以简称代替&#xff09;银行通过数字化转型进行破局&#xff0c;通过实施组织敏捷、提升数字化应用能力、运营模式向商业模式创新这三步法&#xff0c;引导公司走出一条数字化、智能化之路。 随着数字化技…

【java】力扣 跳跃游戏

文章目录 题目链接题目描述代码1.动态规划2.贪心 题目链接 55.跳跃游戏 题目描述 代码 1.动态规划 1.1 dp数组的含义 dp[i]&#xff1a;从[0,i]的任意一点处出发&#xff0c;你最大可以跳跃到的位置。 例如nums[2,3,1,1,4]中: dp[0]2 dp[1]4 dp[2]4 dp[3]4 dp[4]8&#xff…

基于Docker安装elasticsearch和kibana 8.14.3

需要先安装好Docker和DockerCompose 安装的是单机版本的elasticsearch 一、安装elasticsearch 8.14.3 复制下面的内容到elasticsearch-compose.yaml中services:elasticsearch:image: docker.elastic.co/elasticsearch/elasticsearch:8.14.3container_name: elasticsearchenvi…

开源XDR-SIEM一体化平台 Wazuh (1)基础架构

简介 Wazuh平台提供了XDR和SIEM功能&#xff0c;保护云、容器和服务器工作负载。这些功能包括日志数据分析、入侵和恶意软件检测、文件完整性监控、配置评估、漏洞检测以及对法规遵从性的支持。详细信息可以参考Wazuh - Open Source XDR. Open Source SIEM.官方网站 Wazuh解决…

SpringBoot原理解析(二)- Spring Bean的生命周期以及后处理器和回调接口

SpringBoot原理解析&#xff08;二&#xff09;- Spring Bean的生命周期以及后处理器和回调接口 文章目录 SpringBoot原理解析&#xff08;二&#xff09;- Spring Bean的生命周期以及后处理器和回调接口1.Bean的实例化阶段1.1.Bean 实例化的基本流程1.2.Bean 实例化图例1.3.实…

redis的学习(二):常见数据结构及其方法

简介 redis常见的数据结构和他们的常用方法 redis的数据结构 redis是一个key-value的nosql&#xff0c;key一般是字符串&#xff0c;value有很多的类型。 j基本类型&#xff1a; stringhashlistsetsortedSet 特殊类型&#xff1a; GEOBitMapHyperLog key的结构 可以使用…

常用的网络爬虫工具推荐

在推荐常用的网络爬虫工具时&#xff0c;我们可以根据工具的易用性、功能强大性、用户口碑以及是否支持多种操作系统等多个维度进行考量。以下是一些常用的网络爬虫工具推荐&#xff1a; 1. 八爪鱼 简介&#xff1a;八爪鱼是一款免费且功能强大的网站爬虫&#xff0c;能够满足…

mysql练习3

1.修改student 表中年龄(sage)字段属性&#xff0c;数据类型由int 改变为smallint 2.为Course表中Cno 课程号字段设置索引,并查看索引 3.为SC表建立按学号(sno)和课程号(cno)组合的升序的主键索引&#xff0c;索引名为SC_INDEX 4.创建一视图 stu info,查询全体学生的姓名&#…

MinIO使用基础教程

MinIO使用基础教程 一、背景二、快速安装2.1 虚拟机安装2.2 Windows安装2.2.1 下载MinIO服务器2.2.2 启动 MinIO Server2.2.3 通过浏览器访问MinIO服务控制台 三、使用介绍3.1 创建存储桶3.2 上传和下载文件3.3 设置文件公开访问 四、实战SpringBoot Minio实现文件上传和查询五…

思维+01背包,LeetCode LCP 47. 入场安检

一、题目 1、题目描述 「力扣挑战赛」 的入场仪式马上就要开始了&#xff0c;由于安保工作的需要&#xff0c;设置了可容纳人数总和为 M 的 N 个安检室&#xff0c;capacities[i] 记录第 i 个安检室可容纳人数。安检室拥有两种类型&#xff1a; 先进先出&#xff1a;在安检室中…

Git之repo sync -c与repo sync -dc用法区别四十八)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

看准JS逆向案例:webpack逆向解析

&#x1f50d; 逆向思路与步骤 抓包分析与参数定位 首先&#xff0c;我们通过抓包工具对看准网的请求进行分析。 发现请求中包含加密的参数b和kiv。 为了分析这些加密参数&#xff0c;我们需要进一步定位JS加密代码的位置。 扣取JS加密代码 定位到JS代码中的加密实现后&a…