手动申请内存空间 (堆空间)

手动申请内存空间 (堆空间

① 基本概念 --- 问题的引入?

之前我们用指针存放栈空间首地址,现在用指针变量存放堆空间首地址,以下只堆栈空间的基本优缺点:

    1)使用栈空间的优点和缺点:一次性筷子
        特点:栈空间变量的地址:对应函数结束返回之后,对应变量的空间会自动被释放,地址也就从合法变成非法。生命周期被函数退出影响
        优点:申请出来的空间,在对应函数退出的时候“自动”会被释放,无需程序员手动释放,方便。
        缺点:栈空间收到函数的结束影响(生命周期短),造成在开发中无法灵活使用该空间。
        
    2)使用堆空间的优点和缺点  永久性筷子
        特点:堆空间变量的地址:堆空间的生命周期比栈空间长,不会因为函数退出而被释放。
        缺点:申请出来的空间,与对应函数退出的时候无关,需程序员free手动释放。麻烦
        优点:只要在整个程序没退出,自己没有free释放的前提下,该空间可一直使用(生命周期长),在开发中能更加灵活使用该空间.
        实现数据空间在这个项目的流通很强。

② 申请堆空间对应的函数接口

1) malloc: 用于分配指定大小的内存块。
2) calloc: 用于分配并初始化指定数量的内存块。
3) realloc: 用于调整已分配内存块的大小。
4) free: 用于释放先前分配的内存块。

合理使用这些函数,可以有效地管理程序中的动态内存,确保内存的高效使用和防止内存泄漏。


学习函数的过程:为了成功调用这个函数
    第一步:知道函数的功能
    第二步:知道调用这个函数所需要的头文件
    第三步:学习该函数的参数是什么,怎么传   
    第四步:学习该函数的返回值的作用
            常见:
                 -1:函数调用失败
                 0:函数调用成功
                 malloc返回堆空间地址 
    第五步:
        写一个小练习或者例程

③ 使用malloc申请堆空间

1) malloc的用法
我怎么申请堆空间? 怎么访问这个空间? 获取其空间首地址

一开始,我们是这样的:
    int a  = 10; //栈空间
    int *p = &a; //局限性就在这里了,这个p指向的空间大小只有4个字节。
   
头文件:
        #include <stdlib.h>             

函数原型:            类型    变量名
        void * malloc(size_t size); //malloc函数的声明  水果机(投币口:形参 出币口:返回值)
    

形参1:size_t size 无符号长整形类型,这个size形参就是用来存放你传过来的数据值,就是你想申请的空间的字节大小你自己填,你自己设置。不超出 堆空间 的大小。

疑问:
    我怎么拿到这个连续空间?
        malloc函数申请内存空间成功,会把这个空间的首地址当成返回值返回给用户.
   
     你为什么要申请空间?
        为了存放数据,才申请空间的,但是数据是有类型的,所以空间的地址也是有类型的.
        
函数的返回值:
    申请成功:返回申请的空间的首地址
    申请失败: NULL   (void *)0x0
    
    
malloc具体方法:  
    int *指针变量 = (类型转换)堆空间首地址 malloc(要申请的空间的字节大小);
    if(指针变量 == (类型转换)NULL)
    {
        printf("");//标准输出 行缓存机制
        scanf("");//标准输入  行缓存机制
        perror("mallco 失败");//标准出错   无缓冲机制    自动换行 ,会输出系统错误信息机制(错误码)分析的错误信息
        return -1;
    }    
2) malloc实例--基础
#include <stdio.h>
#include <stdlib.h>


int main()
{

    int * heap_start_p = malloc(sizeof(int) * 4); //申请了16个字节的空间,所有里面可以存放4个int数据,空间是连续的
    if(heap_start_p == (int *)NULL)
    {
        printf("申请堆空间失败!\n");
        return -1;
    }
    else
    {
        printf("申请堆空间成功!\n");
    }
    memset(heap_start_p,0,20 * sizeof(char));//memset是一个字节一个字节的设置数据
     
    int a = 4;

    heap_start_p = &a; //这种操作是不允许的,刚刚申请的堆空间被你替换掉了,白申请了

    //数据存放到自己申请的4个整形堆空间中
    * heap_start_p    = 9;   //第一个整形首地址
    *(heap_start_p+1) = 19;  //第二个整形首地址
    *(heap_start_p+2) = 199; //第三个整形首地址
    *(heap_start_p+3) = 1999;//第四个整形首地址

    for(int index = 0; index<4; index++)
    {
        printf("%d\n",*(heap_start_p+index));
    }

    //heap_start_p++;

    free(heap_start_p); //free释放堆空间要求你提供的是你原有申请的堆空间首地址
    return 0;
}

malloc分析:

image

ps:注意free释放的时候提供的参数地址值不要乱写,一定要malloc的返回值(堆空间的首地址)

image

④ calloc --- 按字节块来申请堆空间的,你要确定你要多少块空间,每块空间要多少字节?

1)calloc的特点
calloc内部是调用malloc函数实现申请堆空间,所以内部原理和malloc
两个形参:
    块的个数
    块的大小
    
calloc(int nmemb, int size)
{
    malloc(nmemb*size);
    memset();
}

它calloc虽然是按照块来申请空间的,但是每一块空间的地址都是连续的,那多块和一块是没有区别的.只是在敲代码的时候
”比较形象一点"

calloc: 5块  每块4个字节   20个字节  calloc(5,4) //
malloc: 1块  20个字节              malloc(20);

calloc函数会自动清空申请的堆空间
2)calloc的用法
头文件:
    #include <stdlib.h>
    
void * calloc(size_t nmemb, size_t size);

 nmemb:块的个数
 size:每一块的大小(字节数)
 
使用calloc申请10块,每块10个字节的堆空间,验证空间是不是被清0
#include <stdlib.h>
#include <stdio.h>

int main()
{
     char * p = (char *)calloc(10,10);//100个字节
     if(p==(char *)NULL)//Or Error, These funaction returned NULL
     {
           perror("calloc");
           return -1;   
     }   
     
     for(int lp=0; lp<100; lp++)
     {
         printf("%d\n",*(p+lp));     
     }
     
     //printf("%d\n",(int)((p+2)-p)));
      
     
判断题:
     p+2跳2块,总共跳20个字节。  错!因为是字符指针只跳2个字节
    free(p);
    return 0;
}  

image

3) calloc 进阶 申请20块,每块4个字节,的int类型空间,后面再低8个int类型的堆空间存放字符数据SOS

image
)

⑤ C语言程序申请堆空间的意义和优势

在C语言中,申请堆空间(Heap Memory)是一种动态内存管理方法,允许程序在”运行时“动态地分配和释放内存。相比于静态分配的栈内存(Stack Memory),堆内存具有许多重要的意义和优势:

    1. 动态内存分配
        堆内存允许程序在运行时根据实际需求动态地分配内存,而不是在编译时确定内存大小。这使得程序更灵活,可以处理各种不同大小的数据集。
        数据集:数据的集合
        简单点:数组  结构体 联合体
       
    2. 管理大型数据结构  复杂点:链表、二叉树、链式栈、链式队列、图
        堆内存使得管理大型数据结构变得更加容易。对于需要存储大量数据或需要复杂数据结构(如链表、树、图等)的应用程序,堆内存提供了必要的灵活性。栈内存通常有限,不能满足大型数据结构的需求。
   
    3. 持久化存储  --- 生命周期长
        在函数调用过程中,函数的局部变量存储在栈上,函数返回后这些变量会被销毁。而堆内存可以在函数返回之后继续存在,因此可以用于需要跨越多个函数调用的数据。堆空间存放的数据体现再程序中数据再各个函数之间的流程性强
        
    4. 防止栈溢出
        对于需要大量内存的情况,如果全部使用栈内存,可能会导致栈(栈暴)溢出错误。堆内存则提供了一个更大、更灵活的内存区域,防止栈溢出的问题。
    

总结:

申请堆空间在C语言程序中具有重要的意义,主要包括以下几点:
    灵活性:允许程序在运行时动态分配和释放内存,适应变化的需求。
    处理大型数据结构:堆内存适合存储大型和复杂的数据结构。
    持久性:堆内存可以跨越多个函数调用存在。
    防止栈溢出:避免因大量内存需求而导致的栈溢出。
    
通过合理使用堆内存,可以编写更高效、灵活和可靠的程序。然而,使用堆内存时也要注意管理内存,确保正确释放不再需要的内存,以避免内存泄漏。

⑥malloc的系统内部原理

内存管理的基本概念
    堆(Heap):堆是动态分配内存的区域,由操作系统或运行时程序C语言库管理。
    空闲链表(Free List):维护已释放但尚未重新分配的内存块链表。
    元数据(Metadata):每个内存块前通常有一些元数据,用来记录内存块的大小和状态(已分配或空闲 -- 标记法)。

malloc 的基本步骤
① 初始化:
    程序启动时,堆通常是空的。 是动态加载的,需要运行到对应的代码才能会使用堆空间
    第一次调用 malloc 时,堆管理器可能会从”操作系统“请求一大块内存(如通过 sbrk 或 mmap)。这段内存将用于后续的动态分配。

② 寻找适合的空闲块:
        malloc 在空闲链表中查找一个足够大的空闲块。
        常见策略包括首次适配(first fit)、最佳适配(best fit)和最坏适配(worst fit)。

分割空闲块:
    找到合适的空闲块后,如果该块比请求的大小大得多,则会将其分割成两个部分:一部分分配给请求者,另一部分仍保持空闲并插入空闲链表。

更新元数据:
    修改分配出去的块的元数据,标记为已分配。
    返回指向该块数据部分的指针(跳过元数据部分)。

释放内存(free(堆空间首地址))
    当使用完动态分配的内存,需要调用 free 将其归还给堆管理器。free 的工作原理如下:
    
    标记为空闲:
        根据传入的指针找到相应的内存块,并通过其元数据确定块的大小。
        修改元数据,标记为空闲。
    合并相邻空闲块:
        为了减少内存碎片化,free 尝试将相邻的空闲块合并成一个更大的块。

    更新空闲链表:
        将新的空闲块插入空闲链表。

内存碎片化
    长期大量的动态内存分配和释放会导致内存碎片化,这是指堆中的可用空闲块被分割成许多小块,使得尽管总的空闲内存足够,但没有连续的大块可供分配。
    更高级的内存管理算法
现代的标准库实现(如 GNU C Library 中的 ptmalloc 和 jemalloc 等)引入了更复杂的算法和数据结构来提高内存分配和释放的效率,减少碎片化,并支持多线程环境下的高效操作。
总结来说,malloc 实现涉及堆管理、空闲块查找和管理、以及内存碎片问题处理等多个方面。不同的实现可能在这些方面有所不同,但基本原理大致相同。

image

⑦ 内存碎片化

多次使用 malloc 和 free 可能会导致内存碎片化问题。这种碎片化是指堆中的可用内存被分割成许多小块,虽然总的空闲内存足够,但没有足够大的连续内存块可供分配某些较大的请求。

原因和影响
1)分配和释放不均衡:
    如果程序频繁地进行大小不同的内存分配和释放操作,堆中会出现许多不同大小的空闲内存块。
    这样就可能出现某些空闲块虽然大小总和足够,但无法满足某些大内存分配请求,因为它们不是连续的。

2)内存碎片化:
    内存碎片化使得分配器需要更多的时间和工作来寻找合适的空闲块,可能导致性能下降。
    还可能导致程序在运行时因为无法找到足够大的连续内存块而申请失败。

3)堆管理策略:
    堆管理器会尝试通过合并相邻的小内存块或者采用更复杂的内存分配算法(如伙伴分配算法、slab 分配器等)来减少内存碎片化问题。
    然而,如果程序频繁地进行不规则的内存分配和释放,这些策略也可能不足以完全避免碎片化。

如何减少内存碎片化

1)合理的内存使用:
    尽量避免频繁地进行小块内存的分配和释放,而是尽量使用较大的内存块,减少内存碎片化的可能性。

2)复用和池化:
    对于生命周期较长的对象,可以考虑使用对象池或者内存池来复用内存,而不是频繁地分配和释放。

3)使用内存分配器的最佳实践:
    根据具体的应用场景选择合适的内存分配器,如使用专门针对多线程或特定内存使用模式优化过的分配器(如 jemalloc、tcmalloc 等)。
    根据项目的申请内存的特点来选则合适的分配器
    
4)通过工具定期进行内存碎片化分析:
    可以使用工具来分析程序的内存分配模式和内存碎片情况,及时发现和解决潜在的碎片化问题。

⑧ 嵌入式常见的内存分析调试工具 -- 拓展

在嵌入式Linux环境中进行C和C++程序的内存分析时,需要考虑工具的资源消耗、可移植性和嵌入式系统的特定需求。以下是一些常用且适合嵌入式Linux开发的内存分析工具:

1. Valgrind (包括 Massif 和 Memcheck)
    Valgrind 是一个多功能的内存调试和分析工具,虽然它可能消耗较多的资源,但在嵌入式开发中仍然非常有用。
        Memcheck 可以检测内存错误和内存泄漏。
        Massif 可以分析堆内存使用情况,帮助识别内存占用高峰和碎片化问题。

2. Heaptrack
    Heaptrack 是一个开源的内存跟踪工具,性能相对较好,适合在嵌入式系统中使用。
    可以记录所有的动态内存分配和释放操作,并生成详细的分析报告。

3. AddressSanitizer (ASan)
    AddressSanitizer 是一个快速内存错误检测工具,集成在 Clang 和 GCC 编译器中。
    对嵌入式系统也很有帮助,能够检测各种内存错误(如越界访问、Use-After-Free 等),并提供内存泄漏报告。

4. Dmalloc
    Dmalloc 是一个调试内存分配库,特别适用于嵌入式系统。
    提供内存泄漏检测、未初始化内存读取检测和越界访问检测等功能。
    相比其他工具,它更轻量级,适合资源受限的环境。

5. mtrace
    mtrace 是 GNU C 库中的一个轻量级内存泄漏检测工具。
    通过 mtrace() 和 muntrace() 函数,可以记录 malloc 和 free 操作,生成内存分配记录,适合嵌入式系统使用。

6. GDB 最常用的嵌入式Linux内存调试工具
    GDB (GNU 调试器)不仅仅是一个调试器,它还可以用来监控内存使用。
    通过设置断点和观察变量变化,可以手动检查内存分配和释放的正确性。

7. Custom Instrumentation
    在嵌入式开发中,有时编写自定义的内存管理代码和日志记录可以更加直接和有效。
    开发者可以使用宏和函数包装器来记录内存分配和释放操作,从而生成内存使用报告。

这些工具在嵌入式Linux开发中都具有一定的适用性,选择时应根据具体的资源限制和需求进行权衡。例如,Valgrind提供了全面的内存分析功能,但在资源受限的设备上运行可能会比较缓慢;而像Dmalloc和mtrace这样的工具则更轻量级,更适合嵌入式系统。
posted @ 2024-06-25 14:12  WJnuHhail  阅读(19)  评论(0编辑  收藏  举报