20 动态内存管理

news2024/11/25 3:02:59

目录

一、为什么要有动态内存管理

二、malloc 和 free

(一)malloc

(二)free

三、calloc 和 realloc

(一)calloc

(二)realloc

四、常见的动态内存错误

(一)对NULL指针的解引用操作

(二)对动态开辟空间的越级访问

(三)对非动态开辟内存使用free释放

(四)使用 free 释放一块动态开辟内存的一部分

(五)对同一块动态内存多次释放

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

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

(一)题目一

(二)题目二

(三)题目三

(四)题目四

六、柔性数组

(一)柔性数组的特点

(二)柔性数组的使用

(三)柔性数组的优势

        1、方便内存释放

        2、利于访问速度

七、总结C/C++中程序内存区域划分


一、为什么要有动态内存管理

        我们已经掌握的内存开辟方式有:

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

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

        • 空间开辟大小是固定的。

         • 数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整

        但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的⽅式就不能满足了。 C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。

二、malloc 和 free

(一)malloc

        malloc:

        第一个字母m是:memory,空间;

        后面的alloc:allocate:开辟;连起来就是开辟空间

        malloc 函数的一般形式如下:

void* malloc (size_t size);

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

        ① 如果开辟成功,则返回一个指向开辟好空间的指针。

        ② 如果开辟失败,则返回⼀个 NULL 指针,因此 malloc 的返回值⼀定要做检查

        ③ 返回值的类型是void* ,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候由使用者自己来决定。

        ④ 参数的单位是字节,使用 sizeof 类型进行空间计算会很方便;如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

        ⑤ 为了后续空间的方便使用,一般不直接使用void* 指针来进行返回值的接收,而是直接使用想要设计的指针来接收,等号右边把 malloc 强制转化为对应的指针类型即可。

        ⑥ 申请的空间的地址是连续的,可以类似数组一样使用该空间,但与数组有区别:

                · malloc申请的空间是动态内存,可调节,而数组不可调节;

                · 开辟空间的位置不同:malloc开辟的空间在堆区,而数组是栈区。

(二)free

        C语言提供了另外一个函数 free,专门是用来做动态内存的释放和回收的,函数原型如下:

 void free (void* ptr);

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

         • 参数是任意类型的指针,把指针指向的空间还给操作系统

         • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的,不能对固定内存进行释放操作。

         • 如果参数 ptr 是NULL指针,则函数什么事都不做。

        malloc 和 free 都声明在 stdlib.h 头文件中。

        例子:

#include<stdio.h>
#include<stdlib.h>

int main()
{
    int num = 0;
    scanf("%d", &num);
    int* ptr = (int*)malloc(num * sizeof(int));
    if (ptr == NULL)
    {
        perror("malloc:");
        return 0;
    }
    for (int i = 0; i < num; i++)
    {
        *(ptr + i) = i;
    }

    free(ptr);
    ptr = NULL;

    return 0;
}

        注意:

        free之后,参数的指针还指向被释放的空间,但这块空间不属于当前程序了,所以该指针成为了野指针,要赋值 null !

        如果不释放的话,程序结束的时候也会被操作系统自动回收,但这样内存会被浪费。  

        malloc 和 free 最好成对使用!
 

三、calloc 和 realloc

(一)calloc

        C语⾔还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:

void* calloc (size_t num, size_t size);

        • 函数的功能是为 num 个大小为 size 字节的元素开辟一块空间,并且把空间的每个字节初始化为0。

        • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0。

        使用演示如下:

int main()
{
    int* p = (int*)calloc(10, sizeof(int));
    if (p == NULL)
    {
        perror("calloc:");
        return 0;
    }
    else
    {
        for (int i = 0; i < 10; i++)
        {
            printf("%d ", *(p + i));
        }
    }

    free(p);
    p = NULL;

    return 0;
}

        输出结果为:

        所以如果我们对申请的内存空间的内容要求初始化,那么可以很⽅便的使用 calloc 函数来完成任务。

(二)realloc

        • realloc函数的出现让动态内存管理更加灵活。

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

        函数的原型如下:

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

        • ptr 是要调整的内存地址。

        • size 调整之后新大小,单位是字节

        • 返回值为调整之后的内存起始位置。

        • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

        • realloc在调整内存空间的是存在两种情况:

                ◦ 情况1:原有空间之后有足够大的空间;

                ◦ 情况2:原有空间之后没有足够大的空间。

                如下:

                情况1:

                原有空间之后有足够大的空间,直接把扩容的内存拼接到旧内存处,使整体内存大小达到目标内存的大小,原来空间的数据不发生变化。

                情况2:

                原有空间之后没有足够大的空间时,扩展的方法是:在堆区找一块新的满足目标大小的空间,然后把旧数据拷贝到新空间,把旧空间自动释放,最后返回新空间的起始地址。

        注意:

        最好不要用旧的指针变量进行接收,因为开辟失败就会变成空指针,弄丢旧的数据;

        需要创建新的指针进行接收,同时要进行成功或失败的判断,然后再把新创建的指针赋给旧指针。

        如下演示:

#include<stdlib.h>

int main()
{
    int* p = (int*)malloc(5*sizeof(int));
    if (NULL == p)
    {
        perror("malloc:");
        return 1;
    }
    else
    {
        //业务代码
    }

    int* p2 = NULL;
    p2 = (int*)realloc(p, 40);
    if (NULL == p2)
    {
        perror("realloc:");
        return 1;
    }
    else
    {
        p = p2;
    }
    //业务代码
    free(p); 
    p = NULL;
    
    return 0;
}

四、常见的动态内存错误

(一)对NULL指针的解引用操作

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

        解析:如果p的值是NULL,就会有问题;且最后不对p进行赋NULL操作。

(二)对动态开辟空间的越级访问

void test2()
{
    int i = 0;
    int* p = (int*)malloc(10 * sizeof(int));
    if (NULL == p)
    {
        exit(EXIT_FAILURE);
    }
    for (i = 0; i <= 10; i++)
    {
        *(p + i) = i;
    }
    free(p);
    p = NULL;
}

        解析:当i是10的时候越界访问。

(三)对非动态开辟内存使用free释放

void test3()
{
    int i = 0;
    int* p = &i;
    free(p);
}

(四)使用 free 释放一块动态开辟内存的一部分

void test4()
{
    int* p = (int*)malloc(100);
    p++;
    free(p);
}

        解析:p不再指向动态内存的起始位置。

(五)对同一块动态内存多次释放

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

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

void test6()
{
    int* p = (int*)malloc(100);
    if (NULL != p)
    {
        *p = 20;
    }
}
int main()
{
    test();
    return 0;
}

        解析:

        忘记释放不再使用的动态开辟的空间会造成内存泄漏。

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

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

(一)题目一

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

        解析:实参传给形参,形参是实参的一份拷贝,GetMemory函数创建出一个新的指针p来接收创建出来的动态内存,这样p指针可以找到该动态内存,但是str指针找不到;

        当该函数运行结束,str仍然是空指针,就会造成对空指针的解引用操作,程序就会崩溃,且并没有对指针p进行free操作,造成内存泄漏。 

        正确修改:进行地址传递给GetMemory函数,后面 printf 完 str 之后,进行free与赋NULL操作。

(二)题目二

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函数中创建的数组是在栈上创建的,出了GetMemory函数就会销毁,所以该函数会返回一个野指针。

(三)题目三

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

        解析:能正常打印出hello,但是没有对不再使用的动态开辟出来的空间进行释放,导致内存泄漏。

        正确修改是进行free与赋NULL操作。

(四)题目四

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

        解析:第一次free操作之后str指向的空间还给了操作系统,无法继续使用,但str仍然指向该空间,为野指针,若对该指针继续进行使用,就形成了非法访问

        正确修改应该是free操作之后要赋空值NULL给str。

六、柔性数组

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

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

        有些编译器会报错无法编译可以改成:

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

(一)柔性数组的特点

        • 结构中的柔性数组成员前面必须至少⼀个其他成员。

        • sizeof 返回的这种结构大小不包括柔性数组的内存。

        • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

        例如:

typedef struct st_type
{
    int i;
    int a[0];//柔性数组成员
}type_a;
int main()
{
    printf("%d\n", sizeof(type_a));
    return 0;
}

        结果为:

 

(二)柔性数组的使用

        使用例如下:

#include<stdlib.h>

typedef struct st_type
{
    int i;
    int a[0];//柔性数组成员
}type_a;
int main()
{
    type_a* p = malloc(sizeof(type_a) + 100 * sizeof(int));
    p->i = 5;
    for (int i = 0; i < 100; i++)
    {
        p->a[i] = i;
    }
    free(p);
    p = NULL;

    return 0;
}

(三)柔性数组的优势

        1、方便内存释放

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

        2、利于访问速度

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

七、总结C/C++中程序内存区域划分

        内存中的分区如下所示:

 

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

        以上内容仅供分享,若有错误,请多指正

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

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

相关文章

【初阶数据结构】顺序表与链表的比较(附题)

目录 一、顺序表和链表的区别&#xff08;其他链表存在缺陷&#xff0c;比较意义不大&#xff0c;这里用带头双向循环链表与顺序表进行比较&#xff09; 1.1插入、扩容与随机访问 二、缓存利用率的比较 2.1前置知识 详解及补充知识&#xff08;本文仅为比较顺序表及链表&am…

照片怎么转jpg格式?这些照片格式转换方法简单又高效

图片已成为我们日常生活与工作中不可或缺的一部分。然而&#xff0c;面对多种多样的图片格式&#xff0c;如何高效地将它们转换为最常用的JPG格式&#xff0c;成为了许多人面临的难题。下面给大家分享常用的几种照片格式转换方法&#xff0c;一起来了解下吧。 方法一&#xff1…

数据结构之树体系:二叉树、平衡二叉树、红黑树、AVL树、B树、B+树、最小生成树、哈夫曼树、决策树、LSM树、后缀树、R树

概述 数据结构与算法 二叉树 其中每个结点都不能有多于两个子结点&#xff1a; 满二叉树&#xff1a;若设二叉树的高度为 h h h&#xff0c;除第 h h h层外&#xff0c;其它各层(1&#xff5e;h-1) 的结点数都达到最大个数&#xff0c;最后一层都是叶子结点&#xff0c;且叶…

CICD持续集成持续交付部署

一、CICD概念 1、什么是CI/CD&#xff1f; 通俗来说就是启动一个服务&#xff0c;能够监听代码变化&#xff0c;然后自动执行构建、测试、打包、发布等流程&#xff1b; 2、CI 持续集成 指在开发人员频繁地提交新代码&#xff0c;都会自动执行构建、测试。根据测试结果&…

WPS回应“崩了”:提供15天会员补偿,另有新羊毛,你还不来薅?

近期&#xff0c;“WPS崩了”这一话题在时隔两个月后&#xff0c;再次因多名用户反馈软件使用问题而登上微博热搜。 WPS官方微博随后发布消息称&#xff0c;经过工程师的紧急修复&#xff0c;WPS服务已经恢复正常。 为了补偿用户&#xff0c;在8月22日0点至24点期间&#xff…

视频插帧—— RIFE 和 IFNet 的机制和应用

介绍 最近&#xff0c;数字和模拟技术开始加速融合。我们生活在一个人工智能技术能够显著提高质量的时代&#xff0c;只要模拟材料能够数字化。 例如&#xff0c;讨论中涉及到的纸艺软件&#xff0c;纸龙的移动模型被时间锁定&#xff0c;以大约 3 fps&#xff08;每秒帧数&a…

vm 虚拟机无法调用摄像头(亲测有效)

-- 前言1 报错说明1.1 opencv调用摄像头失败&#xff0c;画面窗口无法显示1.2 选择连接摄像头出现失败&#xff1a;桌面右下角出现【USB 设备“Acer Integrated RGB Camera"的连接失败】连接摄像头方法 2 解决方法步骤一步骤二步骤三 补充 前言 网上找的很多方法都是无效…

2024年游泳耳机哪个牌子好?四大热门游泳耳机多维测评盘点!

游泳不仅是夏日里最受欢迎的消暑方式&#xff0c;也是全年无休的一项全身性运动。在水中畅游时&#xff0c;若能伴随着喜爱的音乐&#xff0c;无疑能让体验更上一层楼。近年来&#xff0c;随着骨传导技术和防水材料的进步&#xff0c;游泳耳机已经成为许多游泳爱好者的必备装备…

网络编程,网络协议,UDP编程

网络&#xff1a; 1.协议&#xff1a;通信双方约定的一套标准 2.国际网络通信协议标准&#xff1a; 1.OSI协议&#xff1a; 应用层 发送的数据内容 表示层 数据是否加密 会话层 是否建立会话连接 传输层 …

electron仿微信,新建贴合窗口

说明 在写electron项目时&#xff0c;只有一个主窗口不足以满足需求&#xff0c;我们通常还会打开很多个窗口。 怎么打开一个子窗口像微信的聊天界面一样&#xff0c;全贴合在一起&#xff0c;看起来像一个整体呢&#xff1a; 分析 这个窗口有点像element ui中的抽屉(drawe…

回归预测|基于北方苍鹰优化NGO-Transformer-BiLSTM组合模型的数据预测Matlab程序多特征输入单输出

回归预测|基于北方苍鹰优化NGO-Transformer-BiLSTM组合模型的数据预测Matlab程序多特征输入单输出 文章目录 前言回归预测|基于北方苍鹰优化NGO-Transformer-BiLSTM组合模型的数据预测Matlab程序多特征输入单输出 一、NGO-Transformer-BiLSTM模型回归预测&#xff1a;NGO-Trans…

VUE学习笔记 2

条件渲染 v-if : 若不频繁切换&#xff0c;会删除DOM节点&#xff0c;再重复添加&#xff1b; template只能配合v-if v-show: 适用于频繁切换&#xff0c;动态变换但不删除DOM节点 列表渲染 方式&#xff1a; v-for"p in person" :key"yyy" 遍历数组…

详讲C#中如何存储当前项目的设置-超级简单省事

我们在编写软件的时候总有一些配置数据需要保存&#xff0c;比如用户选择的偏好设置&#xff0c;又如软件所用到的数据库文件等。我们有很多中方式都可以保存&#xff0c;比如直接保存在某个文本文件&#xff0c;或者ini文件中&#xff0c;其实最简单的办法是保存在项目的资源文…

File的常见成员方法(创建,删除)

一.File的常见成员方法&#xff08;创建&#xff0c;删除&#xff09;&#xff1a; 二.方法的代码实现&#xff1a; 1.createNewFile方法->创建一个新的空的文件&#xff1a; 如上图直接调用createNewFile方法会报错&#xff0c;处理方案如下&#xff1a; 本例选择第一个&a…

Kotlin OpenCV 图像图像51图片轮廓获取

Kotlin OpenCV 图像图像51图片轮廓获取 在OpenCV库中&#xff0c;Imgproc.adaptiveThreshold、Imgproc.findContours 和 Imgproc.boundingRect 这三个方法在图像处理和分析中非常有用。以下是它们的详细作用&#xff1a; Imgproc.adaptiveThreshold解释作用该方法用于将灰度图…

工业排污检测算法的算法样本及工业排污检测的源码展示

工业排污检测算法是一种结合了先进人工智能、图像识别、声音识别、数据分析等技术的解决方案&#xff0c;专门用于实时监测和评估工业排放的污染物。这种算法在环境保护、合规管理、数据支持等方面具有显著的优势&#xff0c;并且可以广泛应用于各种工业场景。 主要作用 1、实…

宝塔面板本地搭建Typecho博客结合内网穿透实现远程访问本地站点

文章目录 前言1. 安装环境2. 下载Typecho3. 创建站点4. 访问Typecho5. 安装cpolar6. 远程访问Typecho7. 固定远程访问地址8. 配置typecho 前言 本文主要介绍如何通过Linux centOS8宝塔面板工具&#xff0c;本地搭建一个Typecho个人博客网站。并结合cpolar内网穿透工具配置公网…

关于Arrays.asList返回List无法新增和删除?

这个是在写项目的时候发现的&#xff0c;然后就分析了一下源码&#xff0c;得其内部原理 复现代码示例&#xff1a; public class ArraysAsList {public static void main(String[] args) {Integer[] array {1, 2, 3, 4, 5};List<Integer> list Arrays.asList(array);…

html+css+js网页制作 电商小米商城7个页面

htmlcssjs网页制作 电商小米商城7个页面 ui还原度百分之99 网页作品代码简单&#xff0c;可使用任意HTML编辑软件&#xff08;如&#xff1a;Dreamweaver、HBuilder、Vscode 、Sublime 、Webstorm、Text 、Notepad 等任意html编辑软件进行运行及修改编辑等操作&#xff09;。 …

uniapp/uniapp x总结

uni-app组成和跨端原理 上图所诉 App的渲染引擎&#xff1a;同时提供了2套渲染引擎&#xff0c;.vue页面文件由webview渲染&#xff0c;原理与小程序相同&#xff1b;.nvue页面文件由原生渲染&#xff0c;原理与react native相同。开发者可以根据需要自主选择渲染引擎。 uniapp…