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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 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;
}


写在最后


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


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


相关文章
|
2天前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
24 6
|
19天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
31 6
|
1月前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
2月前
|
存储 C语言
【c语言】字符串函数和内存函数
本文介绍了C语言中常用的字符串函数和内存函数,包括`strlen`、`strcpy`、`strcat`、`strcmp`、`strstr`、`strncpy`、`strncat`、`strncmp`、`strtok`、`memcpy`、`memmove`和`memset`等函数的使用方法及模拟实现。文章详细讲解了每个函数的功能、参数、返回值,并提供了具体的代码示例,帮助读者更好地理解和掌握这些函数的应用。
27 0
|
16天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
45 2
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
70 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
57 0
|
2月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
64 0
|
2月前
|
安全 Java 程序员
Collection-Stack&Queue源码解析
Collection-Stack&Queue源码解析
85 0
|
17天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。

推荐镜像

更多