C语言 动态内存管理函数的 深度解析 #是不是对数组不能变大变小而烦恼呢?学会动态内存管理函数,消去数组耿直的烦恼#

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: C语言 动态内存管理函数的 深度解析 #是不是对数组不能变大变小而烦恼呢?学会动态内存管理函数,消去数组耿直的烦恼#

前言


动态内存管理函数可以说很好用,但是有些小危险。

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

动态内存函数的头文件都是:<stdlib.h>


为什么存在动态内存分配


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

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


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


空间开辟大小是固定的。

数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这时候就只能试试动态存开辟了。


此外:在后面的通讯录的完整实现,以及数据结构的完整实现大都是需要动态内存来实现的。


mallocfree


1.malloc


malloc是C语言提供的一个内存开辟函数,该函数的参数如下:


d666dd3c2d41480999e5cb157f660e8a.png


c213fcb3cdca47d3b59810bfdd438a25.png


返回值:

264edf14b96746caa573116a43eede32.png


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

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

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

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

如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。


malloc开辟的内存空间都是每有初始化的,观察内存如下:


0c9160dea3c44bf384b89f75eff4053f.png

2.free

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

8531ef4b3420412e8cc8652db2c9ee74.png

dbf473bb77f548e5890408c80a3a3a5d.png


  • free函数用来释放动态开辟的内存。
  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptrNULL指针,则函数什么事都不做。

注意:任何只要是开辟的动态内存空间(堆上的),都要free释放返还给操作系统。


3.使用


mallocfree是要共同使用的,有malloc开辟空间就一定要有free释放空间,通过上面的函数介绍,接下来结合使用。

例如,这里动态开辟一个能够存放10个整型的数组:


#include <stdio.h>
#include <stdlib.h> // 对应头文件
int main()
{
    // 因为返回的是void*,最好强转以下
  int* tmp = (int*)malloc(sizeof(int) * 10); // 也可以直接放一个40(要40字节)
  // 一定要检查开辟成功没有
  if (tmp == NULL)
  {
    perror("malloc fail");  // 这里打印错误“开辟失败”
    exit(-1);  // 这里可以理解为直接退出程序
  }
  // 开辟没问题,进行以下操作
  // 给开辟的数组赋值
  for (int i = 0; i < 10; ++i)
  {
    tmp[i] = i + 1;
  }
  // 打印
  for (int i = 0; i < 10; ++i)
  {
    printf("%d ", tmp[i]);
  }
  // 操作完后一定要释放空间
  // 传递指向那段空间起始位置的指针
  free(tmp);
  // 释放后要把该指针置为空,不然后面一不小心又使用该指针找到那块空间,属于非法访问了
  tmp = NULL;  
  return 0;
}


如果后面不释放,虽然现在的机器大都会自动返还给操作系统,但是出于严谨和安全,一定要记得free,不然会造成内存泄露问题,这是很严重的。


calloc


calloc也是动态内存分配函数

695953d051dd48a7b4ba5fec61e4d86e.png

// 例如这里开辟一个有十个整型元素的数组
int* arr = (int*)calloc(10, sizeof(int));

b9ea3edc0d254c86ab41e4792cf7429c.png


7a85aed6caad40c2a4c39c6088ee1bb7.png


通过上面的介绍,可以发现,calloc的功能与malloc几乎相同,其有两点不同之处:

  1. callocmalloc的函数参数不同;
  2. calloc开辟的空间会将全部元素初始化0,而malloc则是随机值。

如:


#include <stdio.h>
#include <stdlib.h> // 对应头文件
int main()
{
  //               个数     一个元素的大小
  int* tmp = calloc(10, sizeof(int)); 
  // 一定要检查开辟成功没有
  if (tmp == NULL)
  {
    perror("calloc fail");  // 这里打印错误“开辟失败”
    exit(-1);  // 这里可以理解为直接退出程序
  }
  // 开辟没问题,进行以下操作
  // 给开辟的数组赋值
  for (int i = 0; i < 10; ++i)
  {
    tmp[i] = i + 1;
  }
  // 打印
  for (int i = 0; i < 10; ++i)
  {
    printf("%d ", tmp[i]);
  }
  // 操作完后一定要释放空间
  // 传递指向那段空间起始位置的指针
  free(tmp);
  // 释放后要把该指针置为空,不然后面一不小心又使用该指针找到那块空间,属于非法访问了
  tmp = NULL;
  return 0;
}


af0d5b96460344de86dc454448973f3f.png

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


realloc


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


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


40416037061c4d0b84f8aa1c95657339.png

6766f7c88c684faf8ca18bf6be4b6a9a.pngfb14ed7fc1624a2d9e652f1cc3d5e726.png

5a2038120e1d4fa8b75059755f4c91a0.png


基础点:

  1. ptr 是要调整的内存地址。
  2. size 调整之后新大小。
  3. 返回值为调整之后的内存起始位置。
  4. 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。


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

情况1: 内存中原有的空间之后有足够的空间来存放重新开辟的新大小的空间,这时直接在原有的空间之后追加空间。

情况2: 内存中原有的空间之后没有足够的空间来存放重新开辟的新大小的空间,这时在堆上另找一个合适大小的连续空间来使用。

8a139270502a4adea4e01396e7e25d51.png

那么我们如何来写代码呢?

#include <stdio.h>
#include <stdlib.h>
int main()
{
  int* tmp = (int*)malloc(100);
  if (tmp == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  //扩展容量
  //代码1
  tmp = (int*)realloc(tmp, 1000);//这样可以吗?(如果申请失败会如何?)
///
  //代码2
  int* p = realloc(tmp, 1000);
  if (p == NULL)
  {
    perror("realloc fail");
    exit(-1);
  }
  tmp = p;
  // 释放
  free(tmp);
  tmp = NULL;
  return 0;
}


  • 上面有两种写法,代码1跟代码2


分析代码1:如果重新开辟的空间开辟成功,并且是在原空间上做修改,那么这是可行的;如果原空间后面没有足够空间来开辟,另寻找到一份空间来存放,此时的地址空间的起始地址发生了改变,如果空间申请失败,而此时又将该空间的起始地址给了原有的指针变量tmp,这时原有空间就找不到了,并且会出现错误,所以还是不严谨的;


分析代码2:代码2是先将重新开辟的空间的起始地址交给一个临时变量,在判断这份空间的有效性,最后才赋值给原有的指针变量,这样做才是最安全且不会亏损原有空间的,所以,根据代码2的严谨性强的特点,以后realloc一定要写代码2这种样式。


有了reallocbuff的加持,我们想让数组变他就嘚变,哈哈哈


常见的动态内存错误

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


void test()
{
   int *p = (int *)malloc(INT_MAX/4);
   // 这里没有判断是否开辟成功
   *p = 20;  //如果p的值是NULL,就会有问题
   free(p);
   p = NULL;
}


2.对动态开辟空间的越界访问

void test()
{
   int i = 0;
   int *p = (int *)malloc(10*sizeof(int));
   if(NULL == p)
   {
       exit(-1);
   }
   for(i = 0; i <= 10; i++)
   {
       *(p+i) = i;//当i是10的时候越界访问
   }
   free(p);
   p = NULL;
}


i10就越界访问了,越界访问的后果就不用多说了把(哈哈哈哈哈,非法闯入)。

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

void test()
{
   int a = 10;
   int *p = &a;
   free(p);  //ok?
   p = NULL;
}


free是不能释放除动态开辟的内存以外的内存的,只适用于堆上。

4.使用free释放一块动态开辟内存的一部分

void test()
{
   int *p = (int *)malloc(100);
   p++;
   free(p);//p不再指向动态内存的起始位置
   p = NULL;
}


free这样子释放相当于拦腰截断,会存在内存泄漏的问题。

5.对同一块动态内存多次释放

void test()
{
   int *p = (int *)malloc(100);
   free(p);
   free(p);//重复释放
   p = NULL;
}


对同一块空间多次释放,这当然是不行的。


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


这样是绝对不行的,内存泄漏迟早会吃光你的内存。

例如:

void test()
{
   int *p = (int *)malloc(100);
   if(NULL != p)
   {
    *p = 20;
   }
}
int main()
{
   test();
   // p指向的动态内存空间没有释放,虽然p变量销毁了,但申请的空间还在
   return 0;
}


写在最后


动态内存分配是不是很容易就学会了,接下来就可以 ”肆无忌惮“ 的玩弄 ”数组“ 了,不过要小心内存泄漏噢!


感谢阅读本小白的博客,错误的地方请严厉指出噢!


相关文章
|
1月前
|
存储 网络协议 编译器
【C语言】深入解析C语言结构体:定义、声明与高级应用实践
通过根据需求合理选择结构体定义和声明的放置位置,并灵活结合动态内存分配、内存优化和数据结构设计,可以显著提高代码的可维护性和运行效率。在实际开发中,建议遵循以下原则: - **模块化设计**:尽可能封装实现细节,减少模块间的耦合。 - **内存管理**:明确动态分配与释放的责任,防止资源泄漏。 - **优化顺序**:合理排列结构体成员以减少内存占用。
153 14
|
1月前
|
存储 算法 C语言
【C语言】深入浅出:C语言链表的全面解析
链表是一种重要的基础数据结构,适用于频繁的插入和删除操作。通过本篇详细讲解了单链表、双向链表和循环链表的概念和实现,以及各类常用操作的示例代码。掌握链表的使用对于理解更复杂的数据结构和算法具有重要意义。
548 6
|
1月前
|
存储 网络协议 算法
【C语言】进制转换无难事:二进制、十进制、八进制与十六进制的全解析与实例
进制转换是计算机编程中常见的操作。在C语言中,了解如何在不同进制之间转换数据对于处理和显示数据非常重要。本文将详细介绍如何在二进制、十进制、八进制和十六进制之间进行转换。
44 5
|
2天前
|
算法 C语言
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
本文档介绍了如何编写两个子函数,分别求任意两个整数的最大公约数和最小公倍数。内容涵盖循环控制与跳转语句的使用、最大公约数的求法(包括辗转相除法和更相减损术),以及基于最大公约数求最小公倍数的方法。通过示例代码和测试说明,帮助读者理解和实现相关算法。最终提供了完整的通关代码及测试结果,确保编程任务的成功完成。
25 15
|
2天前
|
C语言
【C语言程序设计——函数】亲密数判定(头歌实践教学平台习题)【合集】
本文介绍了通过编程实现打印3000以内的全部亲密数的任务。主要内容包括: 1. **任务描述**:实现函数打印3000以内的全部亲密数。 2. **相关知识**: - 循环控制和跳转语句(for、while循环,break、continue语句)的使用。 - 亲密数的概念及历史背景。 - 判断亲密数的方法:计算数A的因子和存于B,再计算B的因子和存于sum,最后比较sum与A是否相等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台对代码进行测试,预期输出如220和284是一组亲密数。 5. **通关代码**:提供了完整的C语言代码实现
38 24
|
2天前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
38 23
|
1天前
|
存储 编译器 C语言
【C语言程序设计——函数】回文数判定(头歌实践教学平台习题)【合集】
算术运算于 C 语言仿若精密 “齿轮组”,驱动着数值处理流程。编写函数求区间[100,500]中所有的回文数,要求每行打印10个数。根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码。如果操作数是浮点数,在 C 语言中是不允许直接进行。的结果是 -1,因为 -7 除以 3 商为 -2,余数为 -1;注意:每一个数据输出格式为 printf("%4d", i);的结果是 1,因为 7 除以 -3 商为 -2,余数为 1。取余运算要求两个操作数必须是整数类型,包括。开始你的任务吧,祝你成功!
15 1
|
1月前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
75 10
|
1月前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
58 9
|
1月前
|
存储 Unix Serverless
【C语言】常用函数汇总表
本文总结了C语言中常用的函数,涵盖输入/输出、字符串操作、内存管理、数学运算、时间处理、文件操作及布尔类型等多个方面。每类函数均以表格形式列出其功能和使用示例,便于快速查阅和学习。通过综合示例代码,展示了这些函数的实际应用,帮助读者更好地理解和掌握C语言的基本功能和标准库函数的使用方法。感谢阅读,希望对你有所帮助!
45 8

推荐镜像

更多