欢迎光临
我们一直在努力

动态内存管理与柔性数组总结--超详细

在这里插入图片描述

文章目录

  • 为什么要有动态内存分配
  • malloc和free
    • malloc
    • free
  • calloc和realloc
    • calloc
    • realloc
  • 使用动态内存时的注意点
  • 柔性数组
    • 柔性数组的特点
    • 柔性数组的使用
    • 柔性数组的优势
      • 1. 便于内存管理
      • 2. 有利于提高访问效率
    • 柔性数组优点总结
  • 总结

这里是think的博客

希望可以一起交流知识,一起think

今天我们来学习动态内存管理

一起来think

为什么要有动态内存分配

举例:如果一个班级的学生的人数是36人,但是我事先不知道具体由多少人,所以提供了100个空间,所以导致空间开多了, 那么如果是数组的话,就无法修改了,但是我们需要在开辟空间开多了的情况下,修改空间大小,这个需求数组无法满足(变长数组也是无法满足的),所以动态内存分配就来了。

malloc和free

malloc

void* malloc (size_t size);

功能: malloc 用于在堆区申请一块指定大小的连续可用内存,并返回这块内存起始位置的地址。

参数: size 表示需要申请的内存大小,单位为字节。

返回值:

  • 申请成功时,返回指向这块内存起始位置的指针;
  • 申请失败时(如可用内存不足),返回 NULL,因此使用 malloc 后通常需要检查返回值是否为空;
  • 返回值类型为 void*。这是因为 malloc 只负责分配原始内存,而不知道调用者打算将这块内存解释成什么类型的数据(如 int、double、结构体等),因此返回通用指针 void*,具体的数据类型由使用者在后续使用时决定。

要注意的是:

当 size 为 0 时,malloc 的行为由实现定义,不同编译器或运行库可能返回 NULL,也可能返回一个不可解引用的非空指针。

实际使用:int* p = (int*)malloc(10 * sizeof(int));

free

void free (void* ptr);

功能: free 用于释放之前通过动态内存分配函数(如 malloc、calloc、realloc)申请的堆内存,使这块内存重新变为可用状态。

参数: ptr 是指向待释放内存块的指针,必须是动态内存分配函数返回的地址。

返回值: free 没有返回值,其返回类型为 void。

要注意的是:

  • ptr 必须指向动态分配的内存,否则调用 free 的行为未定义。
  • 同一块动态内存只能释放一次,重复释放(double free)会导致未定义行为。
  • 当 ptr 为 NULL 时,free 什么也不做,因此无需在调用前专门判断是否为 NULL。
  • free 释放的是指针所指向的内存空间,而不是指针变量本身。释放后,指针仍然存在,只是变成了悬空指针(wild pointer),通常应立即将其置为 NULL。
  • 释放成功后,程序不能再通过该指针访问原来的内存,否则会产生未定义行为。

#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int arr[num];//变长数组
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
else
{
perror("malloc");
return 1;
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;
return 0;
}

变长数组和malloc开辟的函数在现阶段好像功能是一样的,都是在运行期间才确定大小,但是在使用realloc的时候就会发现后者可以继续改变大小,而前者不可以,这就动态开辟灵活的地方。

calloc和realloc

calloc

void* calloc (size_t num, size_t size); 功能: calloc 用于在堆区动态申请内存。它会为 num 个大小为 size 字节的元素分配一块连续的内存空间,并在返回之前将这块空间的所有字节初始化为 0。

参数:

  • num:需要分配的元素个数;
  • size:每个元素的大小,单位为字节。

返回值:

  • 分配成功时,返回指向所申请内存块起始位置的指针;
  • 分配失败时,返回 NULL;
  • 返回类型为 void*,因为 calloc 只负责分配内存,而不知道这块内存将被解释为哪种数据类型,具体类型由使用者决定。

特点:

  • 实际申请的内存大小为 num × size 字节;
  • calloc 会自动将申请到的内存全部初始化为 0;
  • calloc 申请的内存同样需要使用 free 释放。

与 malloc 的区别:

  • malloc(size) 只负责分配内存,不会对内存内容进行初始化;
  • calloc(num, size) 不仅分配 num × size 字节的内存,还会将这块内存的所有字节初始化为 0。
  • 可以认为calloc==malloc+memset。

realloc

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

功能:

  • realloc 函数用于调整动态内存空间的大小,使动态内存管理更加灵活。

  • 在实际开发中,最初申请的空间可能过小,也可能过大。为了更合理地使用内存,可以使用 realloc 对已经申请的内存进行扩容或缩容。

参数:

  • ptr:指向之前动态分配内存块的起始地址。
    • 如果 ptr == NULL,则 realloc 的行为与 malloc(size) 类似,都可以认为是申请一块内存。
  • size:调整后的内存大小,单位为字节。
    • 当 size == 0 时,与 malloc(0)、calloc(0, size) 一样属于特殊情况,其具体行为由实现决定(实现定义)。但由于 realloc 还涉及原内存块的处理,因此通常不建议依赖 realloc(ptr, 0) 的具体行为,需要释放内存时应直接使用 free(ptr)。

返回值:

  • 调整成功:返回调整后内存块的起始地址(类型为 void*)。
  • 调整失败:返回 NULL,且原来的内存块仍然保持有效,不会被释放。

当 realloc 调整内存大小时,通常有两种情况:

情况1:原空间后面有足够的连续空闲内存 如果原内存块后方存在足够大的空闲区域,则直接在原空间基础上扩展(或缩小)内存。

特点: 原有数据保持不变; 不需要搬移数据; 返回地址与原地址相同。 原空间: [ 已使用 ][ 空闲空间 ] 扩容后: [ 已使用 ][ 新增空间 ]

情况2:原空间后面没有足够的连续空闲内存 此时无法直接扩容,realloc 会: 在堆区寻找一块满足要求的新空间; 将原空间中的数据复制到新空间; 释放原来的内存块; 返回新空间的起始地址。

特点: 原有数据会被保留; 返回地址发生变化; 应使用临时指针接收返回值,在成功后再更新原指针,否则可能丢失原内存地址。

旧空间到新空间的步骤:

  • 申请新空间
  • 复制数据
  • 释放旧空间
  • 注意点: 由于 realloc 可能返回新的地址,因此不要直接将返回值赋给原指针: ptr = realloc(ptr, new_size); // 不推荐 如果扩容失败,返回 NULL,原地址会丢失,导致内存泄漏。

    更安全的写法:

    int* tmp = realloc(ptr, new_size);
    if (tmp != NULL)
    {
    ptr = tmp;
    }

    这样即使 realloc 失败,原来的内存空间仍然能够通过 ptr 访问和释放。

    使用动态内存时的注意点

  • 对NULL指针的解引用操作
  • void test()
    {
    int *p = (int *)malloc(INT_MAX);
    *p = 20;//如果p的值是NULL,就会有问题
    free(p);
    p = NULL;
    }

    改法:在动态内存申请完成以后加上判断即可。

    void test()
    {
    int* p = (int*)malloc(INT_MAX);
    if (p == NULL)
    {
    perror("malloc");
    exit(1);
    }
    *p = 20;
    free(p);
    p = NULL;
    }

  • 对动态开辟空间的越界访问
  • void test()
    {
    int i = 0;
    int *p = (int *)malloc(10*sizeof(int));
    if(p == NULL)
    {
    exit(1);
    }
    for(i = 0; i <= 10; i++)
    {
    *(p+i) = i;//当i是10的时候越界访问
    }
    free(p);
    p = NULL;
    }

  • 对非动态开辟内存使用free释放
  • void test()
    {
    int a = 10;
    int *p = &a;
    free(p);
    }

  • 使用free释放一块动态开辟内存的一部分
  • void test()
    {
    int *p = (int *)malloc(100);
    p++;
    free(p);//p不再指向动态内存的起始位置
    }

  • 对同一块动态内存多次释放
  • void test()
    {
    int *p = (int *)malloc(100);
    free(p);
    free(p);//重复释放
    }

  • 动态开辟内存忘记释放(内存泄漏)
  • void test()
    {
    int *p = (int *)malloc(100);
    if(NULL != p)
    {
    *p = 20;
    }
    }
    int main()
    {
    test();
    while(1);
    return 0;
    }

    动态开辟的内存最终会被释放,释放方式通常有两种:

  • 程序员主动调用 free 释放;
  • 程序结束时,由操作系统回收该进程占用的全部内存资源。
    • 对于一些运行时间较短的程序,即使忘记释放动态内存,程序结束后操作系统也会将其回收。但在实际开发中,很多程序(例如服务器程序、数据库程序等)需要长期甚至持续运行,不会轻易退出。

    • 因此,当一块动态申请的内存不再使用时,应及时调用 free 将其释放。否则,这部分内存会一直被当前进程占用,无法被程序中的其他功能再次利用,从而造成内存泄漏(Memory Leak)。随着泄漏的内存越来越多,程序可用内存会不断减少,严重时甚至可能导致程序运行缓慢、申请内存失败,甚至崩溃。

    柔性数组

    你可能从没听过柔性数组(flexible array),但它是真实存在的语法特性。 在 C99 标准里,结构体的最后一个成员允许定义长度未知的数组,这种数组就被称作柔性数组成员。

    示例写法一:

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

    部分编译器使用 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));//输出的是4
    return 0;
    }

    柔性数组的使用

    //代码1
    #include <stdio.h>
    #include <stdlib.h>

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

    int main()
    {
    int i = 0;
    type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
    p->i = 100;
    for(i=0; i<100; i++)
    {
    p->a[i] = i;
    }
    free(p);
    return 0;
    }

    这样柔性数组成员a,相当于获得了100个整型元素的连续空间。 在这里插入图片描述

    柔性数组的优势

    前面介绍的柔性数组结构(代码1)与下面这种“结构体 + 指针成员”的写法(代码2)都能够实现动态存储数据的功能。

    //代码2
    #include <stdio.h>
    #include <stdlib.h>
    typedef struct st_type
    {
    int i;
    int *p_a;
    }type_a;
    int main()
    {
    type_a *p = (type_a *)malloc(sizeof(type_a));
    p->i = 100;
    p->p_a = (int *)malloc(p->i*sizeof(int));
    //业务处理
    for(i=0; i<100; i++)
    {
    p->p_a[i] = i;
    }
    //释放空间
    free(p->p_a);
    p->p_a = NULL;
    free(p);
    p = NULL;
    return 0;
    }

    1. 便于内存管理

    对于代码2:

    type_a* p = malloc(sizeof(type_a));
    p->p_a = malloc(100 * sizeof(int));

    实际上进行了两次动态内存申请: 一次为结构体对象申请空间; 一次为数据成员申请空间。 因此释放时也必须对应地进行两次释放: free(p->p_a); free(p); 在这里插入图片描述 要注意的是如果该结构体是在某个库函数内部创建并返回给用户的,用户可能只知道释放结构体本身: free(p); 而忽略释放 p->p_a 指向的内存,从而造成内存泄漏。

    而柔性数组通常采用一次性分配: struct S* p = malloc(sizeof(struct S) + n * sizeof(int)); 柔性数组是结构体的一个成员,柔性数组元素与其他结构体成员共同存储在同一块连续内存空间中。因此整个结构体对象只需一次动态内存申请,也只需一次 free 即可完成释放,因此释放时只需: free(p); 即可释放全部空间,降低了内存泄漏的风险,也使内存管理更加简单。

    2. 有利于提高访问效率

    • 柔性数组的数据与其他结构体成员存放在同一块连续内存中。

    • 而指针方案通常需要分别申请内存: 结构体 ——-> 数据区 两块内存的位置可能相距较远。

    连续存储具有以下优点:

    • 更符合 CPU 缓存(Cache)的工作方式;
    • 减少缓存未命中的概率;
    • 提高数据访问效率;
    • 减少内存碎片的产生。

    因此,在需要频繁访问大量数据时,柔性数组通常具有更好的性能表现。

    柔性数组优点总结

    柔性数组相比于“结构体 + 指针成员”的方案,主要有两个优点:

    • 一次申请、一次释放,内存管理更加简单。
    • 数据连续存储,缓存友好,访问效率更高,同时能减少内存碎片。

    因此,在需要存储变长数据且数据生命周期与结构体一致时,柔性数组往往是更优的选择。

    总结

    谢谢观看!

    赞(0)
    未经允许不得转载:171主机测评 » 动态内存管理与柔性数组总结--超详细
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址