• C语言动态内存管理、柔性数组(超详细版)


    目录

    何为动态内存管理?​​​​​​​

    动态内存函数介绍

    mallo()函数

    free()函数

    calloc()函数

    realloc()函数

    realloc()的两种执行原理

    内存池

    常见的动态内存错误

    对NULL解引用

    对开辟的空间越界访问

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

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

    对同一块内存多次释放

    内存泄漏

    经典面试题​​​​​​​

    柔性数组



     

    📌————本章重点————📌

    🔗realloc()的两种执行原理

    🔗内存池

    🔗常见的动态内存错误

    🔗经典面试题

    🔗柔性数组


     ✨————————————✨


    何为动态内存管理?

            亦称:动态内存分配。编写程序有时不能确定数组应该定义为多大,因此这时在程序运行时要根据需要从系统中动态地获得内存空间。

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

            其涉及到几种内存函数:malloc()、calloc()、realloc()、free()等;


    动态内存函数介绍

    mallo()函数

    • void* malloc (size_t size);
    • malloc向内存申请一块连续可用的空间;
      • 如果开辟成功,则返回一个指向开辟好空间的地址;
      • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查,否则会经常在vs中看到警告:取消对指针NULL的引用;
      • 该函数定义时返回值为void*,因此使用者需强制所需类型;
      • size的值不能为0,这是标准未定义的;

    使用实例:

    1. #include
    2. #include
    3. #include
    4. #include
    5. int main()
    6. {
    7. int* arr = (int*)malloc(10 * sizeof(int));
    8. if (arr == NULL)
    9. {
    10. printf("%s\n", strerror(errno));
    11. }
    12. else
    13. {
    14. for (int i = 0; i < 10; i++)
    15. {
    16. *(arr + i) = i;
    17. printf("%d ", arr[i]);
    18. }
    19. }
    20. return 0;
    21. }
    22. //输出1 2 3 4 5 6 7 8 9 10

    这里用完后虽然没有free,并不是说空间就不回收了,对于malloc,当程序退出时,系统会自动回收其开辟的空间。

    free()函数

    • void free (void* ptr);
    • free函数是专门用来做动态内存的释放和回收的;
    • 注意:
      • 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的;
      • 如果参数ptr是NULL指针,则函数什么事都不做;


    使用实例:

    1. int main()
    2. {
    3. //开辟
    4. int* p = (int*)malloc(40);
    5. //使用
    6. // ...
    7. //释放
    8. free(p);
    9. p = NULL;//让p失忆,永远不知道用过的地址
    10. return 0;
    11. }

    calloc()函数

    • void* calloc (size_t num, size_t size);
    • calloc函数也用来动态内存分配;
    • 该函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0,这里便是与malloc的不同之处;

    使用实例:

    1. #include
    2. #include
    3. #include
    4. #include
    5. int main()
    6. {
    7. int* arr = (int*)calloc(10, sizeof(int));
    8. if (arr == NULL)
    9. {
    10. printf("%s\n", strerror(errno));
    11. }
    12. else
    13. {
    14. for (int i = 0; i < 10; i++)
    15. {
    16. *(arr + i) = i;
    17. printf("%d ", arr[i]);
    18. }
    19. }
    20. //释放
    21. free(arr);
    22. arr = NULL;
    23. return 0;
    24. }

    realloc()函数

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

    • 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,realloc函数可以使分配更加灵活:

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

      • size为调整之后的总大小;

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

      • r ealloc在调整内存空间的是存在两种情况:
        • 情况1:原有空间之后有足够大的空间;
        • 情况2:原有空间之后没有足够大的空间;

    使用实例:

    1. #include
    2. #include
    3. #include
    4. #include
    5. int main()
    6. {
    7. int* arr = (int*)calloc(10, sizeof(int));
    8. if (arr == NULL)
    9. {
    10. printf("%s\n", strerror(errno));
    11. }
    12. else
    13. {
    14. for (int i = 0; i < 10; i++)
    15. {
    16. *(arr + i) = i;
    17. printf("%d ", arr[i]);
    18. }
    19. }
    20. //用完刚才40个空间后还要继续放数据——扩容
    21. //先用一个新的指针来接收,防止扩容失败返回NULL
    22. //再追加40个字节,现在总共有80个字节
    23. int* ptr = (int*)realloc(arr, 20 * sizeof(int));
    24. if (ptr != NULL)
    25. {
    26. arr = ptr;
    27. for (int i = 0; i < 10; i++)
    28. {
    29. *(ptr + i) = i + 10;
    30. printf("%d ", ptr[i]);
    31. }
    32. }
    33. //释放
    34. free(arr);
    35. arr = NULL;
    36. return 0;
    37. }
    38. //输出:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

    realloc()的两种执行原理:

    即对于上述realloc调整空间大小的两种情况:

    1.原内存之后有足够大的空间:

    2.原内存之后没有足够大的空间:

    由此我们引发思考:如果频繁的这样申请空间,不仅每次访问操作系统都会降低效率,还有可能导致空间碎片化,于是这一问题又被内存吃很好的解决了;

    内存池:

            内存池,则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升,并且一定程度上会避免空间碎片化,但这并不是绝对的,如果频繁使用内存池,也会导致同样的问题出现。


    常见的动态内存错误

    对NULL解引用:

    下面这段代码中p虽然不是NULL,但若在某些特殊情况下开辟失败,就是对NULL进行引用;

    对开辟的空间越界访问:

    该问题较为常见,无论在静态内存还是动态内存中,我们都应养成防止越界的好习惯;

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

    1. int main()
    2. {
    3. int a = 20;
    4. int* pa = &a;
    5. printf("%d\n", *pa);
    6. free(pa);
    7. pa = NULL;
    8. return 0;
    9. }

    使用free释放动态内存的一部分:

    要释放就得从开始将整个空间释放掉,否则就无法找到这块空间的起始位置;

    1. int main()
    2. {
    3. int* p = (int*)malloc(8);
    4. p += 1;//p此时已不指向起始位置
    5. free(p);
    6. p = NULL;
    7. return 0;
    8. }

    对同一块内存多次释放:

    内存泄漏:

    1. int main()
    2. {
    3. int* p = NULL;
    4. while (1)
    5. {
    6. if (p != NULL)
    7. {
    8. p = (int*)malloc(40);
    9. *p = 20;
    10. }
    11. }
    12. free(p);
    13. p = NULL;
    14. return 0;
    15. }


    经典面试题

    题一:

            p作为参数在函数内使用完后就被销毁了,此时的str依然是NULL,因此会发生内存泄露、程序崩溃。

    1. void GetMemory(char* p)
    2. {
    3. p = (char*)malloc(100);
    4. }
    5. void Test(void)
    6. {
    7. char* str = NULL;
    8. GetMemory(str);
    9. strcpy(str, "hello world");
    10. printf(str);
    11. }

    怎么写才对呢?

    题二:

            返回局部变量或临时地址,此处的p成为野指针。

            p在此处是局部变量、临时地址,它的内容会随着函数的调用而产生,随着函数的关闭而回收,但最终返回了p的地址,此时p所指向的空间已经被释放,因此成为了野指针。

    1. char* GetMemory(void)
    2. {
    3. char p[] = "hello world";
    4. return p;
    5. }
    6. void Test(void)
    7. {
    8. char* str = NULL;
    9. str = GetMemory();
    10. printf(str);
    11. }

    题三:

    warning C4172: 返回局部变量或临时变量的地址: a; 

    1. int* test()
    2. {
    3. //返回栈空间的地址的问题
    4. int a = 10;
    5. return &a;
    6. }
    7. int main()
    8. {
    9. int* p = test();
    10. printf("hehe\n");
    11. printf("%d\n", *p);
    12. return 0;
    13. }

    为什么执行打印hehe后输出不再是10了呢?

     这是因为hehe覆盖了之前一部分栈区

    题四:

            这段代码的错误出现在语句4,有的小伙伴可能会有疑惑:虽然str的空间被释放了,但是我可以拿到它的地址,既然有它的地址就可以把字符串拷贝进去啊?其实形象的比喻就像男女朋友,str变量是男生,str的内容是女生,free(str)就相当于两个人分手,既然分手了,知道人家的地址,也不能在去找她了呀。

            在程序的层面理解即为:str的变量是为Test函数创建的,str的空间也是随着str的产生而开辟的,对str进行free就,虽然str的地址依然存在,但是该程序段已经不存在属于它的空间了。

    1. void Test(void)
    2. {
    3. char* str = (char*)malloc(100); //1
    4. strcpy(str, "hello"); //2
    5. free(str); //3
    6. if (str != NULL)
    7. {
    8. strcpy(str, "world"); //4
    9. printf(str); //5
    10. }
    11. }


    柔性数组

    结构体中的最后一个元素允许是未知大小的数组,就叫柔性数组成员;

    特点:

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

    示例:下面这个结构的大小就是4个字节,只包含n的大小,不包含a的大小。

    1. #include
    2. struct S
    3. {
    4. int n;
    5. int a[];//数组a的大小未知,则a就是柔性数组成员
    6. };
    7. int main()
    8. {
    9. printf("%zd\n", sizeof(struct S));
    10. return 0;
    11. }

    使用实例:

    1. #include
    2. #include
    3. struct S
    4. {
    5. int n;
    6. int a[];//数组a的大小未知,则a就是柔性数组成员
    7. };
    8. int main()
    9. {
    10. struct S* s = (struct S*)malloc(sizeof(struct S) + 40);
    11. if (s == NULL)
    12. {
    13. //报错
    14. return;
    15. }
    16. s->n = 10;
    17. printf("%d\n", s->n);
    18. for (int i = 0; i < 10; i++)
    19. {
    20. s->a[i] = i + 1;
    21. printf("%d ", s->a[i]);
    22. }
    23. free(s);
    24. s = NULL;
    25. return 0;
    26. }
    27. //输出:
    28. //10
    29. //1 2 3 4 5 6 7 8 9 10

    当扩容时,就能真正体现其柔性的特点了:

    1. #include
    2. #include
    3. struct S
    4. {
    5. int n;
    6. int a[];//数组a的大小未知,则a就是柔性数组成员
    7. };
    8. int main()
    9. {
    10. struct S* s = (struct S*)malloc(sizeof(struct S) + 40);
    11. if (s == NULL)
    12. {
    13. //报错
    14. return;
    15. }
    16. s->n = 10;
    17. printf("%d\n", s->n);
    18. for (int i = 0; i < 10; i++)
    19. {
    20. s->a[i] = i + 1;
    21. printf("%d ", s->a[i]);
    22. }
    23. //给数组a扩容到80个字节
    24. struct S* str = (struct S*)realloc(s, sizeof(struct S) + 80);
    25. if (str == NULL)
    26. {
    27. //报错
    28. return;
    29. }
    30. s = str;
    31. str = NULL;
    32. //这里的str不能释放掉,str和p指向用一块空间,如果释放掉,p就成了野指针
    33. for (int i = 10; i < 20; i++)
    34. {
    35. s->a[i] = i + 1;
    36. printf("%d ", s->a[i]);
    37. }
    38. free(s);
    39. s = NULL;
    40. return 0;
    41. }
    42. //输出:
    43. //10
    44. //1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

    优点:

    1.方便内存释放:

            假设我们的程序交给用户使用,而用户并不知道整个结构中内存开辟了多少次,用户只会使用free,释放掉整个结构,如果结构内定义的是局部变量指针一类,那么用户并不会知道该结构内部的成员也要释放,因此当我们使用柔性数组作为结构体成员,为用户一次性开辟好空间,用户的每次释放就可以将整个空间释放掉了。

    2.有利于提高访问速度,避免连续开辟引起的空间碎片化。

  • 相关阅读:
    【正点原子Linux连载】第二十四章 智能家居物联网项目 摘自【正点原子】I.MX6U嵌入式Qt开发指南V1.0.2
    计算机毕业设计Java大学生学习时间规划平台服务端(源码+系统+mysql数据库+lw文档)
    pytorch 修改tensor数据类型
    【JavaScript】leetcode链表相关题解
    让人获益匪浅的学习网站
    什么是 DNS 泛洪攻击(DNS 泛洪)
    装饰模式(Decorator Pattern)
    《算法系列》之 数组
    Spring
    【Python日志模块全面指南】:记录每一行代码的呼吸,掌握应用程序的脉搏
  • 原文地址:https://blog.csdn.net/m0_65190367/article/details/126579665