C语言——动态内存管理

打印 上一主题 下一主题

主题 529|帖子 529|积分 1587

引言

在编程中,动态内存管理是一个至关重要的概念,它对于开发高效、灵活且可扩展的程序至关重要。接下来我们就来学习一下C语言——动态内存管理的知识。

为什么必要动态内存分配

我们现在已经把握了两种内存开发的方法:
  1. #include<stdio.h>
  2. int main()
  3. {
  4.         int val = 20;         //在栈空间上开辟四个字节
  5.         char arr[10] = { 0 }; //在栈空间上开辟10个字节的连续空间
  6.         return 0;
  7. }
复制代码
上述的开发内存的方法为静态开发内存。
这种方法存在两个缺点:
(1)空间开发的巨细是固定的。
(2)数组在声明的时候,必须要指定命组的长度,数组的长度一旦确定就不能修改。
偶然候我们可能会遇到:必要的空间巨细不能确定的情况,此时使用静态开发内存就无法解决题目。
为相识决这类题目,C语言引进了动态内存开发,也就是我们今天要重点学习的内容。
动态内存

与动态内存相关的函数声明均在<stdlib.h>中。
1.perror函数

在学习动态内存开发函数和释放函数之前,我们可以先学习一下perror函数。
perror是C语言标准库中的一个函数,用于错误处理和调试
   perror包罗在头文件<stdio.h>中。
  函数声明为:void perror ( const char * str );
          str – 这是 C 字符串,包罗了一个自界说消息,将显示在原本的错误消息之前。
  作用:把一个形貌性错误消息输出到标准错误 stderr。起首输出字符串 str,后跟一个冒号,然后是一个空格。
  返回值:无返回值。
  2.动态内存开发函数

2.1 malloc

(1)malloc的用法

malloc函数可以向内存申请一块连续可用的空间,并返回指向这块空间的指针。
   函数原型为:
  void* malloc (size_t size);
    参数:
  size 是一个无符号整数,指定了要分配的内存块的巨细(以字节为单位)。如果参数 size 为0,那么malloc的举动是未被界说的,取决于编译器。
  返回值:
  返回一个指向分配的内存块的指针。返回值为void*,所以malloc函数并不知道开发空间的范例,具体必要根据使用者的需求去决定。
  如果开发成功,则返回一个指向开发好空间的指针。
如果开发失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
(2)malloc的使用

我们可以将malloc和perror结合使用:
  1. #include <stdio.h>  
  2. #include<stdlib.h>
  3. int main()
  4. {
  5.         int* arr = (int*)malloc(sizeof(int) * 5);
  6.     //开辟5个大小为整型的空间
  7.     //返回类型强制转换为(int*)
  8.         if (arr == NULL)
  9.         {
  10.                 perror("malloc fail:");//如果开辟失败则打印错误信息
  11.                 return 1;
  12.         }
  13.         //存储数据
  14.         for (int i = 0; i < 5; i++)
  15.         {
  16.                 arr[i] = i;
  17.         }
  18.         //打印数据
  19.         for (int i = 0; i < 5; i++)
  20.         {
  21.                 printf("%d ", arr[i]);
  22.         }
  23.         return 0;
  24. }
复制代码
输出结果为:

2.2 calloc

(1)calloc的用法

calloc函数同样可以向内存申请一块连续可用的空间,并返回指向这块空间的指针。
   函数原型为:
  void* calloc (size_t num, size_t size);
    参数:
  num:指定要分配的元素数量。
  size:指定每个元素的巨细。
  返回值:
  返回一个指向分配的内存块的指针。返回值为void*
  但与malloc函数不同的是:calloc会将申请的空间中的每个字节都初始化为0
由此我们得知:calloc不仅分配内存,还负责初始化内存,这是它与malloc的一个重要区别。
(2)calloc的使用

  1. #include <stdio.h>  
  2. #include<stdlib.h>
  3. int main()
  4. {
  5.         int* arr = (int*)calloc(5, sizeof(int));
  6.         //开辟5个大小为整型的空间
  7.         //返回类型强制转换为(int*)
  8.         if (arr == NULL)
  9.         {
  10.                 perror("calloc fail:");//打印错误信息
  11.                 return 1;
  12.         }
  13.         return 0;
  14. }
复制代码
我们通过监视窗口来观察一下:

2.3 realloc

(1)realloc的用法

realloc函数用于重新分配之前已经分配的内存块的巨细。
   函数原型为:
  void* realloc (void* ptr, size_t size);
    参数:
  ptr:指向已分配的内存块的指针。如果ptr为NULL,则realloc的举动与malloc相同,分配一块新的内存空间。
  size:新的内存块的巨细(以字节为单位)。
  返回值:
  该函数返回一个指针 ,指向重新分配巨细的内存。如果请求失败,则返回 NULL。
  这个函数在调整内存空间巨细的底子上,还会将原来内存中的数据移动到新的空间。
  realloc在调整内存空间之后存在两种情况:
情况1:本地扩容
原有空间之后有充足大的空间,直接在原有内存之后直接追加空间原有的数据不发生变革。

情况2:异地扩容
原有空间之后没有充足大的空间时,在堆空间上另外找一个合适巨细的连续空间来使用,如许函数返回的是一个新的内存地点
(2)realloc的使用

由于在扩展空间时会出现两种情况,因此我们使用realloc的时候就必要注意,避免出错。
  1. int main()
  2. {
  3.         int* arr = (int*)malloc(sizeof(int) * 10);
  4.         //开辟十个大小为整型的空间
  5.         //返回类型强转为int*
  6.         if (arr == NULL)//如果开辟失败
  7.         {
  8.                 perror("malloc fail: ");//打印错误信息
  9.                 return 1;//直接返回
  10.         }
  11.         //继续新增空间
  12.         int* tmp = (int*)realloc(arr, sizeof(int) * 15);
  13.         //不用arr是为了防止开辟失败,被至为NULL
  14.         if (tmp == NULL)//如果开辟失败
  15.         {
  16.                 perror("realloc fail: ");//打印错误信息
  17.                 return 1;//直接返回
  18.         }
  19.         arr = tmp;
  20.         return 0;
  21. }
复制代码
如果新增内存较小时一样平常是在原有底子上新增空间。两者地点相同

如果我们把开发的空间比较大:
  1. int* tmp = (int*)realloc(arr, sizeof(int) * 150);
复制代码

新增内存较大时则会重新开发一段空间,将原来的空间释放。两者地点不同。

3.动态内存释放函数

1.free函数

(1)free的用法

C语言提供了另外一个函数free,专门用于做动态内存的释放的回收。
   函数原型为:
  void free (void* ptr);
    参数:
  ptr: 指针指向一个要释放内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果传递的参数是一个空指针,则不会实验任何动作。
  作用:
  释放之前调用 calloc、malloc 或 realloc 所分配的内存空间。
  返回值:
  该函数不返回任何值。
  (2)free的使用

代码如下:
  1. int main()
  2. {
  3.         int* arr = (int*)malloc(sizeof(int) * 10);
  4.         //开辟十个大小为整型的空间
  5.         //返回类型强转为int*
  6.         if (arr == NULL)//如果开辟失败
  7.         {
  8.                 perror("malloc fail: ");//打印错误信息
  9.                 return 1;//直接返回
  10.         }
  11.         //继续新增空间
  12.         int* tmp = (int*)realloc(arr, sizeof(int) * 150);
  13.         //不用arr是为了防止开辟失败,被至为NULL
  14.         if (tmp == NULL)//如果开辟失败
  15.         {
  16.                 perror("realloc fail: ");//打印错误信息
  17.                 return 1;//直接返回
  18.         }
  19.         arr = tmp;
  20.         free(arr);
  21.         arr = NULL;
  22.         return 0;
  23. }
复制代码
常见的内存分布

C语言中的内存分布主要可以分为以下几个地区,这些地区由操纵系统进行管理,用于存储不同范例的数据和代码。
   1.栈区:用于存储函数内的局部变量、函数参数以及函数调用的返回地点等信息。
  2.堆区:用于存储程序运行时动态分配的内存。
  3.静态区:用于存储全局变量和静态变量(包括静态局部变量和全局静态变量)。
  

常见的动态内存的错误

1.对NULL指针的解引用操纵

  1. void test()
  2. {
  3.         int* p = (int*)malloc(INT_MAX / 4);
  4.         *p = 20; //如果p的值是NULL,就会有问题
  5.         free(p);
  6. }
复制代码
1.INT_MAX是一个宏界说,他表现整型的最大值,值为2147483647
2.malloc申请的空间太大导致内存开发失败,失败返回NULL
3.系统无法访问到NULL指针指向的地点,这时编译器就会报警告

我们要对代码进行修改:
  1. void test()
  2. {
  3.     int* p = (int*)malloc(INT_MAX / 4);
  4.     if (NULL == p)
  5.     {
  6.         perror("malloc fail: ");//打印错误信息
  7.         return 1;
  8.     }
  9.     *p = 20;
  10.     free(p);
  11.     p = NULL;
  12. }
复制代码
这就表现出判断空指针的重要性了
2.对动态开发空间的越界访问

  1. void test()
  2. {
  3.         int i = 0;
  4.         int* p = (int*)malloc(10 * sizeof(int));
  5.         if (NULL == p)
  6.         {
  7.                 perror("malloc fail: ");//打印错误信息
  8.                 return 1;//直接返回
  9.         }
  10.         for (i = 0; i <= 10; i++)
  11.         {
  12.                 *(p + i) = i; //当i是10的时候越界访问
  13.         }
  14.         free(p);
  15.     p=NULL;
  16. }
复制代码
malloc开发的空间只有10个,但是for循环循环了十一次,这时就会越界访问。
修改方法:
  1. void test()
  2. {
  3.         int i = 0;
  4.         int* p = (int*)malloc(10 * sizeof(int));
  5.         if (NULL == p)
  6.         {
  7.                 perror("malloc fail: ");//打印错误信息
  8.                 return 1;//直接返回
  9.         }
  10.         for (i = 0; i < 10; i++)
  11.         {
  12.                 *(p + i) = i;
  13.         }
  14.         free(p);
  15.         p = NULL;
  16. }
复制代码
3.对非动态开发内存使用free释放

  1. void test()
  2. {
  3.         int a = 10;
  4.         int* p = &a;
  5.         free(p);
  6.     p=NULL;
  7. }
复制代码
1.free函数只能释放有动态内存开发在堆上的空间。
2.p开发的空间在静态区,free函数无法释放。
修改方法:
  1. void test()
  2. {
  3.         int a = 10;
  4.         int* p = &a;
  5. }
复制代码
动态内存开发的空间不必要释放
4.使用free函数释放一块动态开发内存的一部分

  1. void test()
  2. {
  3.         int* p = (int*)malloc(100);
  4.         p++;
  5.         free(p); //p不再指向动态内存的起始位置
  6.         p = NULL;
  7. }
复制代码
1.p++跳过了一个整型巨细的空间。
2.free函数释放p只会释放当前位置开始之后的空间,这导致有一个整型巨细的空间未被释放,导致内存泄漏。
修改方法:
  1. void test()
  2. {
  3.         int* p = (int*)malloc(100);
  4.         free(p);
  5.         p = NULL;
  6. }
复制代码
我们要注意不能随意改变p指向的位置,开发多少内存就释放多少
5.对同一块动态内存多次释放

  1. void test()
  2. {
  3.         int* p = (int*)malloc(100);
  4.         free(p);
  5.         free(p); //重复释放
  6. }
复制代码
1.p已经被释放归还给操纵系统,但是此时p还指向该内存,是一个野指针。
2.再次释放p就会出现内存出错题目。
修改方法:
  1. void test()
  2. {
  3.         int* p = (int*)malloc(100);
  4.         free(p);
  5.         p = NULL;
  6. }
复制代码
6.动态开发内存没有释放

  1. void test()
  2. {
  3.         int* p = (int*)malloc(100);
  4.         if (NULL != p)
  5.         {
  6.                 *p = 20;
  7.         }//内存泄漏
  8. }
复制代码
忘记释放不再使用的动态开发的空间会导致内存泄漏
修改方法:
  1. void test()
  2. {
  3.         int* p = (int*)malloc(100);
  4.         if (NULL != p)
  5.         {
  6.                 *p = 20;
  7.         }
  8.        
  9.         free(p);
  10.         p = NULL;
  11. }
复制代码
动态开发的内存一定要释放,并且精确释放
动态内存经典笔试题分析

1.题目一

  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("%s\n",str);
  11. }
复制代码
我们来分析一下:
1.空间非法访问:
使用传值调用时,形参只是实参的暂时拷贝,对形参的改变无法影响实参,这时str还是空指针,而strcpy拷贝会对空指针进行解引用操纵,对NULL指针解引用会出错。
2.空间内存泄漏:
在GetMemory()函数内部动态申请了100字节的空间,因为p随着函数竣事而被烧毁,所以已经再也找不到该空间,会造成内存泄漏。
修改方法:
1.我们要使用传址调用,str自己是指针变量,传递指针变量的地点必要使用二级指针。
2.释放申请的空间
代码如下:
  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("%s\n",str);
  11.         free(str);
  12.         str = NULL;
  13. }
复制代码
2.题目二

  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("%s\n",str);
  11. }
复制代码
我们来分析一下:
等GetMemory函数返回后,使用str访问p数组,这是非法访问:局部变量脱离作用域就会返还给操纵系统,此时str就会酿成野指针。
修改方法:
我们可以把p的值放在静态区,可以使用static或者常量字符串。
代码如下:
  1. const char* GetMemory1(void)
  2. {
  3.         const char* p = "hello world";
  4.         return p;
  5. }
  6. char* GetMemory2(void)
  7. {
  8.         static char p[] = "hello world";
  9.         return p;
  10. }
  11. void Test(void)
  12. {
  13.     const char* str = NULL;
  14.     str = GetMemory1();
  15.     printf("%s\n", str);
  16.     str = GetMemory2();
  17.     printf("%s\n", str);
  18. }
复制代码
3.题目三

  1. void GetMemory(char** p, int num)
  2. {
  3.         *p = (char*)malloc(num);
  4. }
  5. void Test(void)
  6. {
  7.         char* str = NULL;
  8.         GetMemory(&str, 100);
  9.         strcpy(str, "hello");
  10.         printf("%s\n",str);
复制代码
分析:
又是十分经典的题目:内存没有释放。
修改方法:
把开发的空间及时释放。
代码如下:
  1. void Test(void)
  2. {
  3.         char* str = NULL;
  4.         GetMemory(&str, 100);
  5.         strcpy(str, "hello");
  6.         printf("%s\n",str);
  7.         free(str);
  8.         str = NULL;
  9. }
复制代码
4.题目四

  1. void Test(void)
  2. {
  3.         char* str = (char*)malloc(100);
  4.         strcpy(str, "hello");
  5.         free(str);
  6.         if (str != NULL)
  7.         {
  8.                 strcpy(str, "world");
  9.                 printf("%s\n",str);
  10.         }
  11. }
复制代码
分析:
str所开发的空间已经归还给了操纵系统,这时再将world拷贝进str就会出错。
修改方法:
归还内存之后顺手将其值为NULL指针。
代码如下:
  1. void Test(void)
  2. {
  3.         char* str = (char*)malloc(100);
  4.         strcpy(str, "hello");
  5.         free(str);
  6.         str = NULL;
  7.         if (str != NULL)
  8.         {
  9.                 strcpy(str, "world");
  10.                 printf("%s\n",str);
  11.         }
  12. }
复制代码
柔性数组

1.什么是柔性数组

在C99中,结构体中的最后一个元素答应是位置巨细的数组,这就是【柔性数组】成员
举个例子:
  1. typedef struct st_type
  2. {
  3.         int i;
  4.         int a[0];        //柔性数组成员
  5. }type_a;
复制代码
有些编译器会报错,可以改成:
  1. typedef struct st_type
  2. {
  3.         int i;
  4.         int a[];        //柔性数组成员
  5. }type_a;
复制代码
2.柔性数组的特点

   1.结构中的柔性数组成员前面必须至少一个其他成员。
2.sizeof 返回的这种结构巨细不包括柔性数组的内存。
3.包罗柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的巨细,以顺应柔性数组的预期巨细。
  比方:
  1. typedef struct st_type
  2. {
  3.         int i;
  4.         int a[0];        //柔性数组成员
  5. }type_a;int main(){        printf("%d\n", sizeof(type_a));        return 0;}
复制代码
输出结果为:4
3.柔性数组的使用

  1. // 代码1
  2. typedef struct st_type
  3. {
  4.     int i;
  5.     int a[];
  6. } type_a;
  7. int main()
  8. {
  9.     int i = 0;
  10.     type_a* p = malloc(sizeof(type_a) + 100 * sizeof(int));
  11.     if (p == NULL)
  12.     {
  13.         return;
  14.     }
  15.     p->i = 100;
  16.     for (i = 0; i < 100; i++)
  17.     {
  18.         p->a[i] = i;
  19.     }
  20.     free(p);
  21.     return 0;
  22. }
复制代码
如许柔性数组成员a,相称于获得了100个整型元素的连续空间。
4.柔性数组的优势

我们来看这段代码:
  1. // 代码2
  2. typedef struct st_type
  3. {
  4.     int i;
  5.     int* p_a;
  6. } type_a;
  7. int main()
  8. {
  9.     type_a* p = (type_a*)malloc(sizeof(type_a));
  10.     if (p == NULL)
  11.     {
  12.         perror("malloc fail:");
  13.         return;
  14.     }
  15.     p->i = 100;
  16.     p->p_a = (int*)malloc(p->i * sizeof(int));
  17.     if (p->p_a == NULL)
  18.     {
  19.         perror("malloc fail:");
  20.         return;
  21.     }
  22.     for (int i = 0; i < 100; i++)
  23.     {
  24.         p->p_a[i] = i;
  25.     }
  26.     free(p->p_a);
  27.     p->p_a = NULL;
  28.     p = NULL;
  29.     return 0;
  30. }
复制代码
代码1 和 代码2 都能实现同样的功能,但是 代码1 的实现有两个利益:
   1.方便内存释放
  如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也必要free,容易造成内存泄漏。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
  2.有利于访问速率
  连续的内存有利于提高访问速率,也有利于淘汰内存碎片。
  
竣事语

求点赞收藏加关注!!!十分感谢各位的支持!!!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

没腿的鸟

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表