十三.动态内存管理

news2024/11/26 15:52:14

目录

一.为什么存在动态内存分配

二.动态内存函数的介绍

1.malloc函数

2.free函数

3.calloc函数

4.reallco函数

三.常见的动态内存错误

1.对NULL空指针的解引用操作

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

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

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

5.对同一动态内存多次释放

6.动态开辟内存忘记释放导致内存泄露

四.柔性数组

1.柔性数组的定义及特点

2.柔性数组的使用

3.柔性数组的优势


一.为什么存在动态内存分配

目前我们已经掌握了以下两种开辟内存的方式:

// 在栈上开辟4个字节
int val = 20;
 
// 在栈空间上开辟10个字节的连续空间
char arr[10] = {0};

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

  • 空间开辟的大小是固定的。
  • 数组在声明时必须指定数组的长度,在编译时会开辟并分配其所需要的内存空间。

动态内存分配的定义:

所谓动态内存分配(Dynamic Memory Allocation) 就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。

 

为什么会存在动态内存开辟?

  • 有时我们需要的空间大小在程序运行的时候才能知道,这时在数组编译时开辟空间的方式就不能满足了,这时我们就需要动态内存开辟来解决问题。

二.动态内存函数的介绍

1.malloc函数

头文件:stdlib.h

void *malloc(size_t size)

malloc 是C语言提供的一个动态内存开辟的函数,该函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。具体情况如下:

  1. 如果开辟成功,则返回一个指向开辟好空间的指针。
  2. 如果开辟失败,则返回一个 NULL 指针。
  3. 返回值的类型为 void* ,malloc 函数并不知道开辟空间的类型,由使用者自己决定。
  4. 如果 size 为 0(开辟0个字节),malloc 的行为是标准未定义的,结果将取决于编译器。 

2.free函数

头文件:stdlib.h

void free(void *ptr)

free 函数用来释放动态开辟的内存空间。具体情况如下:

  1. 如果参数 ptr 指向的空间不是动态开辟的,那么 free 函数的行为是未定义的。
  2. 如果参数 ptr 是 NULL 指针,那么 free 将不会执行任何动作。
  3. 使用完之后一定要记得使用 free 函数释放所开辟的内存空间。
  4. 使用指针指向动态开辟的内存,使用完并 free 之后一定要记得将其置为NULL空指针。

用法演示: 动态内存开辟10个整型空间

#include <stdio.h>
#include <stdlib.h>
 
int main(void) 
{
    // 假设开辟10个整型空间
    int arr[10]; // 在栈区上开辟
 
    // 动态内存开辟
    int* p = (int*)malloc(10*sizeof(int)); // 开辟10个大小为int的空间
 
    // 使用时先判断是否开辟成功
    if (p == NULL) {
        perror("main"); // main: 错误信息
        return 0;
    }
    
    // 使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i;
    }
    for (i = 0; i < 10; i++) {
        printf("%d ", p[i]);
    }
 
    // 回收空间
    free(p);
    p = NULL; // 需要手动置为空指针
 
    return 0;
}

运行结果:0 1 2 3 4 5 6 7 8 9 

那么问题来了,为什么 free 之后,一定要把 p 置为空指针?

因为 free 之后那块开辟的内存空间已经不在了,它的功能只是把开辟的空间回收掉,但是 p 仍然还指向那块内存空间的起始位置,为了防止后续再对这块空间执行操作,我们需要及时使用 p = NULL 把他置成空指针。

3.calloc函数

头文件:stdlib.h

void *calloc(size_t num, size_t size)    

calloc 函数的功能实为 num 个大小为 size 的元素开辟一块空间,并把空间的每个字节初始化为 0,返回一个指向它的指针。 

malloc函数做对比:

  • malloc 只有一个参数,而 calloc 有两个参数,分别为元素的个数和元素的大小。
  • 与函数 malloc 的区别在于 calloc 会在返回地址前把申请的空间的每个字节初始化为 0 

用法演示:

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    //malloc
    int* p1 = (int*)malloc(40);  //开辟40个空间
    //calloc
    int* p2 = (int*)calloc(10, sizeof(int));  //开辟10个大小为int的空间,40
   
    if (p1 == NULL)
        return 1;
    if (p2 == NULL)
        return 1;
   
    int i = 0;
    for (i = 0; i < 10; i++)
        printf("%d ", *(p1 + i));
    printf("\n");
    for (i = 0; i < 10; i++)
        printf("%d ", *(p2 + i));
   
    free(p1);
    p1 = NULL;
    free(p2);
    p2 = NULL;
    
    return 0;
}

 运行结果:10个随机值       0 0 0 0 0 0 0 0 0 0 

说明 calloc 会对内存进行初始化,把空间的每个字节初始化为 0 。如果我们对于申请的内存空间的内容,要求其初始化,我们就可以使用 calloc 函数来轻松实现。

4.reallco函数

头文件:stdlib.h

void *realloc(void *ptr, size_t size)

realloc 函数,让动态内存管理更加灵活。用于重新调整之前调用 malloccalloc 所分配的 ptr 所指向的内存块的大小,可以对动态开辟的内存进行大小的调整。具体介绍如下: 

  1. ptr 为指针要调整的内存地址。
  2. size 为调整之后的新大小。
  3. 返回值为调整之后的内存起始位置,请求失败则返回空指针。
  4. realloc 函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

realloc 函数在调整内存空间时存在的三种情况:

  1. 当原有空间之后没有足够大的空间时,直接在原有内存之后直接追加空间,原来空间的数组不发生变化。
  2. 当原有空间之后没有足够大的空间时,会在堆空间上另找一个合适大小的连续的空间来使用。函数的返回值将是一个新的内存地址。
  3. 如果找不到合适的空间,就会返回一个空指针。

用法演示:如果 realloc 找不到合适的空间,就会返回空指针,所以realloc在开辟空间时存在返回空指针的危险,我们不要拿指针直接接收 realloc,可以使用临时指针判断一下realloc是否为空

#include <stdio.h>
#include <stdlib.h>
 
int main() 
{
    int* p = (int*)calloc(10, sizeof(int));
    if (p == NULL) {
        perror("main");
        return 1;
    }
    //使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i)  = 5;
    }
    
    //此时,这里需要 p 指向的空间更大,需要 20 个int的空间
    //realloc 调整空间
    int* ptmp = (int*)realloc(p, 20*sizeof(int));
    
    //如果ptmp不等于空指针,再把p交付给它
    if (ptmp != NULL) {
        p = ptmp;
    }
 
    //释放
    free(p);
    p = NULL;
}

三.常见的动态内存错误

1.对NULL空指针的解引用操作

错误代码演示:

#include <stdlib.h>
#include <stdio.h>
 
int main()
{
    int* p = (int*)malloc(9999999999);
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i; // 对空指针进行解引用操作,非法访问内存
    }
 
    return 0;
}

解决方案:对 malloc 函数的返回值做判空处理

#include <stdlib.h>
#include <stdio.h>
 
int main()
{
    int* p = (int*)malloc(9999999999);
    // 对malloc函数的返回值做判空处理
    if (p == NULL) {
        perror("main");
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i; // 对空指针进行解引用操作,非法访问内存
    }
 
    return 0;
}

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

错误代码演示:

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int* p = (int*)malloc(10*sizeof(int));  //申请10个整型的空间
    if (p == NULL) {
        perror("main");
        return 1;
    }
    int i = 0;
    
    //越界访问 - 指针p只管理10个整型的空间,根本无法访问40个
    for (i = 0; i < 40; i++) {
        *(p + i) = i;
    }
 
    free(p);
    p = NULL;
 
    return 0;
}

为了防止越界访问,使用空间时一定要注意开辟的空间大小。 

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

错误代码演示:

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int arr[10] = {0};  //在栈区上开辟
    int* p = arr;
 
    free(p);  //使用free释放非动态开辟的空间
    p = NULL;
 
    return 0;   
}

运行结果:

 

所以,不要对非动态开辟的内存使用 free,否则会出现难以意料的错误。

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

错误代码演示:

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int* p = malloc(10*sizeof(int));
    if (p == NULL) {
        return 1;
    }
    int i = 0;
    for (i = 0; i < 5; i++) {
        *p++ = i;  //p指向的空间被改变了
    }
 
    free(p);
    p = NULL;
  
    return 0;
}

p++ 会使p指向p后面的空间,原本开辟给p的动态内存的起始地址发生改变,free时会导致 p 只释放了后面的空间,存在内存泄漏问题。释放内存空间的时候一定要从头开始释放。

5.对同一动态内存多次释放

错误代码演示:

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int* p = malloc(10*sizeof(int));
    if (p == NULL) {
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++) {
        p[i] = i;
    }
 
    //释放
    free(p);
    //再释放
    free(p);
  
    return 0;
}
 

解决方案:在第一次释放后紧接着将 p 置为空指针

// 释放
free(p);
p = NULL;
 
free(p); // 此时p为空,free什么也不做

6.动态开辟内存忘记释放导致内存泄露

错误代码演示:

#include <stdio.h>
#include <stdlib.h>
 
void test()
{
    int* p = (int*)malloc(100);
    if (p == NULL) {
        return;
    }

    // 此时忘记释放了
}
 
int main()
{
    test();
    
}

动态开辟的内存空间有两种回收方式:  1. 主动释放(free)      2. 程序结束 

malloc 这一系列函数 和 free 一定要成对使用,记得及时释放。

四.柔性数组

1.柔性数组的定义及特点

定义:C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

例如:

struct S {
    int n;
    int arr[0];   //柔性数组成员
};

部分编译器可能会报错,可以试着将 a [ 0 ] 改为 a [ ] : 

struct S {
    int n;
    int arr[];   //柔性数组成员
};

特点:

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

代码演示:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

struct S {
    int n;
    int arr[0];  //前面至少有一个成员
};

int main() {

    printf("%d", sizeof(struct S));   //大小为4,不包含柔性数组
    //后面+的大小就是给柔性数组准备的
    struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int));

    return 0;
}

2.柔性数组的使用

用法演示:

#include <stdio.h>
#include <stdlib.h>
 
struct S {
    int n;
    int arr[0];
};
 
int main() {
    //期望arr的大小是10个整型
    struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int));
    ps->n = 10;
 
    //使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        ps->arr[i];
    }
 
    //增容
    struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20*sizeof(int));
    if (ptr != NULL) {
        ps = ptr;
    }
 
    // 再次使用
    ………………
    …………
    ……

    // 释放
    free(ps);
    ps = NULL;
 
    return 0;
}

3.柔性数组的优势

对比下面两片代码:

1.使用柔性数组:

#include <stdio.h>
#include <stdlib.h>
 
struct S {
    int n;
    int arr[0];
};
 
int main() {
    //期望arr的大小是10个整型
    struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int));
    ps->n = 10;
 
    //使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        ps->arr[i];
    }
 
    //增容
    struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20*sizeof(int));
    if (ptr != NULL) {
        ps = ptr;
    }
 
    // 再次使用
    ………………
    …………
    ……

    // 释放
    free(ps);
    ps = NULL;
 
    return 0;
}

2.使用指针

#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)
        return 1;
    ps->n = 10;
    ps->arr = (int*)malloc(10 * sizeof(int));
    if (ps->arr == NULL)
        return 1;
 
    // 使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        ps->arr[i];
    }
 
    // 增容
    int* ptr = (struct S*)realloc(ps->arr, 20 * sizeof(int));
    if (ptr != NULL) {
        ps->arr = ptr;
    }
 
    // 再次使用 
    ………………
    …………
    ……
 
    // 释放
    free(ps->arr); // 先free第二块空间
    ps->arr = NULL;
    free(ps);
    ps = NULL;
 
    return 0;
}

 上述 代码1 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:

(1)第一个好处:有利于内存释放

代码1只需要释放一次内存,代码2需要释放两次,防止了内存释放不干净的疏忽

(2) 第二个好处:有利于访问速度

连续内存多多少少有益于提高访问速度,还能减少内存碎片。

malloc 的次数越多,产生的内存碎片就越多,这些内存碎片不大不小,再次被利用的可能性很低。内存碎片越多,内存的利用率就会降低,频繁的开辟空间效率会变低,碎片也会增加。

 


本篇到此结束,码文不易,还请多多支持哦!

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

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

相关文章

HTML实现除夕最美烟花,2023春节倒计时,新年不可没有烟花,最炫烟花代码分享

&#x1f4cb; 前言 &#x1f5b1; 博客主页&#xff1a;在下马农的碎碎念✍ 本文由在下马农原创&#xff0c;首发于CSDN&#x1f4c6; 首发时间&#xff1a;2023/01/17&#x1f4c5; 最近更新时间&#xff1a;2023/01/17&#x1f935; 此马非凡马&#xff0c;房星本是星。向前…

Android入门第58天-真机调试

开篇 随着我们的Android开始慢慢往后面涉及到设备、网络、通讯内容的开发。我们开始要把真机调试提上日程来了。 比如说&#xff1a;我们用Android扫描barcode、二维码都需要使用到Android的摄像头。而在Android Studio的AVP&#xff08;模拟器&#xff09;里&#xff0c;它的摄…

【树莓派4B】搭建HomeAssistant服务端(二)(systemd配置开机自启动,cpolar内网穿透)

设置开机自启动 创建home-assistanthomeassistant.service服务&#xff1a; sudo nano /etc/systemd/system/home-assistanthomeassistant.service复制以下内容&#xff0c;定义服务&#xff0c;其中After定义先行服务&#xff0c;ExecStart执行启动脚本&#xff1a; [Unit]…

程序跑起来数据总是关闭及丢失?保存进文件里面美滋滋

文章目录前言文件是什么&#xff1f;程序文件数据文件文件名C语言中的文件打开和关闭文件指针文件的打开和关闭fopenfclose文件的顺序读写文件的随机读写fseekftellrewind文件读取结束的判定feof&#xff1a;我们之间可能有误会文件缓冲总结前言 我们或许都有这样的苦恼&#…

【手写 Vue2.x 源码】第二十八篇 - diff算法-问题分析与patch优化

一&#xff0c;前言 首先对 6 月更文内容做一下简单的回顾&#xff1a; Vue2.x 源码环境的搭建Vue2.x 初始化流程介绍对象的单层、深层劫持数组的单层、深层劫持数据代理的实现对象、数组数据变化的观测Vue 数据渲染流程介绍模板生成 AST 语法树AST 语法树生成 render 函数re…

【Java寒假打卡】Java基础-XML文件

【Java寒假打卡】Java基础-XML文件概述标签的规则xml的语法规则解析XMLXML解析的准备工作XML解析文件的代码实现概述 标签的规则 xml的语法规则 <?xml version"1.0" encoding"UTF-8" ?> <!--本xml文件用于描述多个学生信息--> <students&…

教程: nodejs 做微信公众号开发,回复 xml 消息

教程&#xff1a; nodejs 做微信公众号开发&#xff0c;回复 xml 消息 首先需要你的后台跟服务器已经可以建立连接&#xff0c;这个不再冗述看官方教程就好 接入指南 。此篇介绍的是如何获取用户发来的信息&#xff0c;并回复它。 一、接收 xml 信息内容 我用的是 nodejs 的…

Android应用模块化开发指南

Android应用模块化开发指南 包含多个Gradle模块的项目称为多模块项目。本文包含多模块应用项目的最佳实践和推荐模式。 代码规模变大带来的问题 可扩缩性、可读性和整体代码质量会随着时间的推移而降低&#xff0c;代码维护者未采取积极的措施来保持易于维护的结构。模块化是…

【营销】uplift建模方案-专利总结

之前准备写专利的时候浏览了一下其他公司的专利&#xff0c;对于one model&#xff0c;还是two model&#xff0c;基模型是什么做了简单总结。 浦发银行&#xff08;CN 112446541 A&#xff09;——one model&#xff08;标签转换&#xff09; 基模型&#xff1a;NN分类融合m…

经济学学习(宏观)

--------------------------------------- 第8篇&#xff1a;宏观经济学的数据 --------------------------------------- 23. 一国收入的衡量(GDP&#xff0c;通胀) gdp衡量总收入和总支出&#xff0c;总收入总支出 某一既定时期&#xff0c;一个国家内生产的所有最终商品…

【科研试剂】16-Heptadecynoic acid,93813-16-2,16-庚二酸

【中文名称】16-庚二酸【英文名称】 16-Heptadecynoic acid&#xff0c;16-Heptadecynoic COOH【结 构 式】【CAS】93813-16-2【分子式】C17H30O2【分子量】266.43【纯度标准】95%【包装规格】1g&#xff0c;5g&#xff0c;10g【是否接受定制】可进行定制&#xff0c;定制时间周…

Java日志系统介绍和slf4j的使用

目录1. 日志系统介绍2. slf4j的使用2.1 slf4j的入门2.2 slf4j绑定日志框架1. 日志系统介绍 日志门面位于应用程序和日志框架之间&#xff0c;日志门面提供一个抽象的能力&#xff0c;日志框架进行具体的日志实现。可以很方便的更换日志框架。类似JDBC驱动 日志门面有&#xf…

业务逻辑漏洞

1、容易忽略的低危漏洞以及延伸利用 一、容易忽略的低危漏洞以及延伸利用 在挖洞的过程当中&#xff0c;比如我们碰到信息泄露漏洞&#xff0c;但是我们不知道这个是信息泄露&#xff1b;或者说我们碰到一个xss&#xff0c;我们不会利用&#xff0c;只能弹个窗&#xff0c;比如…

AcWing 4510. 寻宝!大冒险!(暴力枚举)

题目如下&#xff1a; 输入样例1&#xff1a; 5 100 2 0 0 1 1 2 2 3 3 4 4 0 0 1 0 1 0 1 0 0输出样例1&#xff1a; 3样例 111 解释 绿化图上 (0,0)(0,0)(0,0)、(1,1)(1,1)(1,1) 和 (2,2)(2,2)(2,2) 三处均可能埋有宝藏。 输入样例2&#xff1a; 5 4 2 0 0 1 1 2 2 3 3 …

C++入门:命名空间

目录 一.前言 C关键字(C98)总览&#xff1a; 一.作用域 二.命名冲突 三.命名空间 命名空间定义&#xff1a; 命名空间的嵌套定义&#xff1a; 四.命名空间的使用 五.命名空间的本质 一.前言 C是从C语言延伸出来的编程语言&#xff0c;C兼容了C语言百分之九十九的语法…

Lr 12 ACR 15:蒙版

Adobe Camera Raw &#xff08;简称为 ACR&#xff09;与 Lightroom Classic&#xff08;简称为 Lr 或 LrC&#xff09;使用同一引擎&#xff0c;其中的蒙版 Mask功能变得日益强大。基于人工智能技术&#xff08;AI 驱动&#xff09;&#xff0c;可快速而精准地选择主体、天空、…

Redis - Redis 6.0 新特性之多线程模型

1. Redis6.0之前的版本真的是单线程么&#xff1f; 否&#xff01;Redis 在处理客户端的请求时&#xff0c;包括获取 (socket 读)、解析、执⾏、内容返回 (socket 写) 等都由⼀个顺序串⾏的主线程处理&#xff0c;这就是所谓的「单线程」。 在执行命令阶段&#xff1a;Redis是…

【jQuery超快速入门教程】上篇

&#x1f340;作者主页&#xff1a;在下周周ovo&#x1f340;系列专栏&#xff1a;从零开始百天学习前端基础&#x1f340;其他平台&#xff1a;博客园1️⃣前言&#xff1a;jQuery必备网站jQuery下载地址jQuery中文文档jQuery插件库1️⃣一、为什么要学习jQuery&#xff1f;jQ…

蓝库云|2023年企业4个数字化转型关键,成功之路近在咫尺

数字化转型&#xff1a;由上而下的过程 企业数字化转型最主要的原因在于企业管理者的决定。数字化转型是由「上」而「下」的过程&#xff0c;如果管理层没有转型的确切目标与规划&#xff0c;与竞争者相比之下&#xff0c;经营模式将会原地踏步、无法超越。蓝库云根据最新客户…

C++:C++全局变量:看完还不懂全局变量来捶我

我们知道&#xff0c;全局变量时C语言语法和语义中一个很重要的知识点&#xff0c;首先它的存在意义需要从三个不同角度去理解。 对于程序员来说&#xff0c;它是一个记录内容的变量&#xff08;variable&#xff09;对于编译/链接器来说&#xff0c;它是一个需要解析的符号 &a…