C语言进阶——动态内存管理(上)

简介: C/C++中的内存区域大体可划分为这三个部分:栈区、堆区以及静态区,这三块区域比较重要。比如我们的 main 函数就是在栈上开辟的空间,当然我们使用的一般变量也都是存储在栈区上的,但是栈区空间有限,不能存储较大的数据,此时我们会通过动态内存管理来为这些“大数据”在堆上开辟空间供其使用,用完后记得释放内存就好了,除了储存“大数据”外,在堆区上开辟的空间还可以随意改变其大小(扩大或缩小都可以)。由此可见动态内存开辟的实用性,要想实现动态内存开辟也不难,只需要跟着本文一步一步学习就好了!


目录


🌳前言


🌳正文


🌲一、malloc


🌱声明


🌱使用


🌱注意


🌱补充例子


🌲二、free


🌱声明


🌱使用


🌱注意


🌲三、calloc


🌱声明


🌱使用


🌱注意


🌲四、realloc


🌱声明


🌱使用


🌱注意


🌲五、小结


🌲六、动态内存开辟笔试题


🌱第一题


🌱第二题


🌱第三题


🌱第四题


🌲七、C/C++中的内存区域划分


🌲八、柔性数组


🌱声明


🌱使用


🌱注意


🌱模拟实现柔性数组


🌱柔性数组的优势


🌳总结


🌳前言

 C/C++中的内存区域大体可划分为这三个部分:栈区、堆区以及静态区,这三块区域比较重要。比如我们的 main 函数就是在栈上开辟的空间,当然我们使用的一般变量也都是存储在栈区上的,但是栈区空间有限,不能存储较大的数据,此时我们会通过动态内存管理来为这些“大数据”在堆上开辟空间供其使用,用完后记得释放内存就好了,除了储存“大数据”外,在堆区上开辟的空间还可以随意改变其大小(扩大或缩小都可以)。由此可见动态内存开辟的实用性,要想实现动态内存开辟也不难,只需要跟着本文一步一步学习就好了!

db2a0b07777245e2838ef48d2bc88915.gif


🌳正文

 C语言中的动态内存开辟函数有三个:malloc、calloc 和 realloc,有开辟就要有释放,一般在使用以上三个函数时,都会配套使用一个 free 来进行内存释放。除了介绍这几个函数外,我还会介绍一下C99标准中的柔性数组,因为它也会用到动态内存管理。


🌲一、malloc

🌱声明

 malloc,是我们要学习的第一个内存开辟函数,它的作用是向堆区申请一块目标大小的连续空间,如果申请成功,会返回这块空间的首地址,失败则返回空指针(NULL)。当我们申请内存后,一般会对返回的指针进行判断,如果是空指针,就得结束程序(因为此时已经申请失败,再继续运行就会出错),虽然现在的空间都比较大,几乎不会出现申请失败的情况,但最好还是加一个判断,确保万无一失嘛,判断这个操作适用于所有动态内存申请函数。

3ae564261ee146d18571f73c2b9f43ea.png

malloc标准格式

  可以看到 malloc 格式还是比较简单的,只需要传递大小,然后准备好指针接收返回值就行了,当然我们在使用时会在此基础上进行完善,比如对返回值进行强制类型转换、传递的字节数通过sizeof(类型)*数量得出、对返回指针进行判断等


//malloc 使用方法
int main()
{
  int* p = (int*)malloc(sizeof(int) * 5);//申请五个int型的空间
  if (p == NULL)
  {
    printf("申请失败!\n");
    return 1;//结束程序
  }
  //申请成功
  //……使用……
  //释放
  free(p);
  p = NULL;//需要置空,避免野指针
  return 0;
}

a49f507922b64366987f86b8c1bb3a7f.png

🌱使用

 在有的题目中,会涉及到大量的数据,此时需要足够大的空间,此时在栈区上申请会出错,毕竟栈区空间有限,但如果改在堆区上申请,就会合适且轻松。


//malloc 的实际使用
int main()
{
  int* p = (int*)malloc(sizeof(int) * 10000);//申请40000字节的空间
  if (p == NULL)
    return 1;//这里我们直接结束程序就好了
  int i = 0;
  for (i = 0; i < 10000; i++)
    *(p + i) = i;
  printf("测试完成,无任何报错\n");
  free(p);
  p = NULL;
  return 0;
}

a371d15ca1714b99a029f443f5a0f2a6.png


🌱注意


注意


1.malloc 申请后要对其返回值进行强制类型转换

2.申请空间的大小不必自己进行计算,通过 sizeof 配合目标数量就好了

3.使用前要判断,使用时不要越界,使用后要释放,释放函数马上介绍

4.申请空间时,不要申请0字节大小的空间,这是标准未定义的行为,具体实现操作取决于编译器

5.申请要合理,不要无限申请,这样会造成严重的后果,比如下面这个例子


🌱补充例子

 因为申请的内存来自于我们的电脑,如果将申请空间这个操作放在一个死循环中,电脑内存就会被申请满,从而导致电脑运行奔溃,然后就会蓝屏(x64环境下会蓝屏,x86环境下有保护)

//补充示例
//注意:尝试前确保数据已保存
int main()
{
  //死循环,不断申请
  while (1)
  {
    int* p = (int*)malloc(sizeof(int) * 100);
    //申请完还不释放
  }
  return 0;
}

90867aafc2c9496aa93b43bc5af118e0.png

  造成这种现象有两个原因:1.无限申请空间   2.申请的空间不释放。x64环境内存分配更激进,运行一会内存就爆了,然后就会蓝屏(我已经试过了),如果想玩玩记得保存好数据,举出这个例子就是想让大家记住这两个重要的点:要合理、要释放,避免发生意外情况。


🌲二、free


🌱声明

 free 就是我们用来释放已申请内存的工具,有申请就要有释放,所以 free 一般都是和动态内存申请函数配套使用,可根据实际情况进行释放,但也不能随意释放。free 的形式就非常简单了,只需要在其中放入指向待释放空间的指针即可。


4770566888fc48f383d2ee73c8735a2d.png

free标准格式

  free 用起来也很简单,就是对已申请且用完的空间进行释放,值得注意的是:free 释放的空间必须是已申请的空间,释放完后要将指向这块空间的指针置空。

//free 使用方法
int main()
{
  int* p = (int*)malloc(sizeof(int));//向堆区申请1个整型的空间
  if (p == NULL)
    return 1;//申请失败的情况
  char* ptr = "123";//在栈区开辟的空间
  free(ptr);//非法释放,会报错
  ptr = NULL;
  free(p);//合理释放
  p = NULL;//置空,避免野指针
  return 0;
}

 非栈区申请的空间,不能释放,这点还是很好理解的,避免张冠李戴嘛。


🌱使用

 这里我们就沿用之前 malloc 无限申请空间的例子,说明 free 释放空间是真实存在的。

//free 实际运用
int main()
{
  //死循环,不断申请
  while (1)
  {
    int* p = (int*)malloc(sizeof(int) * 100);
    free(p);//申请完后释放
    p = NULL;//相当于没申请
  }
  return 0;
}

382264cd83404dcb87ca1c6f78e577e3.png

 当然 free 的例子得配合适合的程序才能体现其价值,这里我们就使用无限申请空间简单验证下就好了,free 虽方便,但也有使用注意事项


🌱注意

注意


1.free 的对象必须是已申请的堆区空间

2.free 完后要对目标指针手动置空

3.不可对同一对象进行连续 free


🌲三、calloc

🌱声明

 calloc,跟 malloc 很像,功能也差不多,都是向栈区申请一块目标空间,不过 calloc 有个小升级,就是 calloc 完后,它会帮忙把申请的空间初始化为0,这样就不至于申请空间中存放的都是随机数了。malloc + memset 也可以实现这一功能,但奈何别人 calloc 优秀,将二者的功能合二为一了。


2cf1b2fe98a24723bb3db46f06303d07.png

calloc标准格式

 calloc 无非就是参数部分比 malloc 多了一个参数(其实相当于没多,因为 calloc 中的两个参数,在 malloc 中被我们手动乘为一个参数了),calloc 在使用时也跟 malloc 一致,都是返回目标空间的首地址,都需要进行判断,保证不会得到一个空指针,当然肯定也少不了释放。


e765daacbc9149b2b0f64604ea0dc6f1.png7aeda7d979d44e9e8f6ccc67f75d85da.png


🌱使用

 calloc 可以用于需要动态内存开辟,且开辟空间要全部初始化为0的情况,这里我想到了一个题目:小乐乐与序列,题目大概意思就是将序列去重后排序并输出,这里的解题思路是:找到与数列中的数值对应的下标(这里的下标是指申请空间中对于首地址的偏移量),再将其对应的值改为1(改的是申请空间的值),即使有重复的数字,也都只会改一次,而如果是没有出现的数字,就默认为0(根据值来判断,如果出现过,不管是否重复,都为1)。最后再弄个循环,从1下标处开始判断(题目要求,不会出现元素0),如果对应值为1,说明此处留下过标记,输出此时的下标就行了。这样一来我们就得到了一个去重且排好序的序列,可以看出我们有个硬性要求:申请空间默认为0,此时我们的 calloc 就可以派上用场了。


ba6daf6b64344d1b9dd86afd657cdd05.png

//BC118 小乐乐与序列
//calloc 实际运用
#include <stdio.h>
#include<stdlib.h>
int main() 
{
    int n = 0;
    scanf("%d", &n);
    int* pa = (int*)calloc(n + 1, sizeof(int));//申请n+1大小的空间
    if (pa == NULL)
        return 1;
    int i = 0;
    int m = 0;
    for (i = 0; i < n; i++)
    {
        scanf("%d", &m);
        *(pa + m) = 1;
    }
    for (i = 1; i <= n; i++)
    {
        if (*(pa + i) == 1)
        {
            printf("%d ", i);
        }
    }
    free(pa);//释放
    pa = NULL;//置空
    return 0;
}

583e39ea3c3f49aa80ca8e3b9e3e12a0.png

这种解法本身就很妙,再配合上 calloc 默认初始化为0的特性,就更妙了。


🌱注意

注意


1.calloc 申请后要对其返回值进行强制类型转换

2.申请空间的大小不必自己进行计算,通过 sizeof 配合目标数量就好了

3.使用前要判断,使用时不要越界,使用后要释放

4.申请要合理,不要无限申请,这样会造成严重的后果

5.calloc 会将申请的空间初始化为0

6.申请空间时,不要申请0字节大小的空间,这是标准未定义的行为,具体实现操作取决于编译器


🌲四、realloc


🌱声明

 英语中的 re 有重复、再次的意思,因此 realloc 作用是对已开辟的空间进行扩容(再申请),可以推测出 realloc 需要两个参数:待扩容空间地址、扩容后的大小。如果给 realloc 的第一个参数传递为一个空指针,那么此时的 realloc 就相当于 malloc ,仅仅是申请了一块空间。


723f93f63e8a4bc8902a0f3f5685cd23.png


 realloc 在扩容时有两种情况:1.后续空间足够大,且能够与已开辟好的空间(这里简称目标空间)相连,直接开辟就行了   2.后续空间不足,此时 realloc 会往后寻找一片足够大的空间,开辟好后会将目标空间中的元素搬过来,然后会对其旧的空间进行释放,这样就相当于增容了。当然 realloc 也需要判断、释放、置空。

//realloc 使用方法
//情况1,后续空间足够
int main()
{
  int* p = (int*)malloc(sizeof(int) * 5);//只申请了五个整型大小的空间
  if (p == NULL)
    return 1;
  int i = 0;
  int* ptr = (int*)realloc(p, sizeof(int) * 10);//扩容为十个整型大小的空间
  if (ptr == NULL)
    return 1;
  free(ptr);//释放
  ptr = p = NULL;//置空
  return 0;
}

9d96dcbca69c4f589f212ec9013c03fa.png


//realloc 使用方法
//情况2,后续空间不足
int main()
{
  int* p = (int*)malloc(sizeof(int) * 5);//只申请了五个整型大小的空间
  if (p == NULL)
    return 1;
  int i = 0;
  int* ptr = (int*)realloc(p, sizeof(int) * 100);//扩容为一百个整型大小的空间
  if (ptr == NULL)
    return 1;
  free(ptr);//释放
  ptr = p = NULL;//置空
  return 0;
}

68362460a3f2423ea68f6c1321895f8d.png

🌱使用

 realloc 可以用于需要二次申请(扩容)的场景,比如在顺序表中,如果下标等于容量,就需要扩容以确保首地址不被改变。当然因为顺序表是属于后面的知识,所以这里就用一个简单例子说明一下扩容的实际场景。

//realloc 的实际使用
int main()
{
  int* p = (int*)malloc(sizeof(int) * 5);//先申请五个整型大小的空间
  if (p == NULL)
    return 1;
  int* ptr = (int*)realloc(p, sizeof(int) * 10);//再扩容为十个整型大小
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    *(p + i) = i;
    printf("%d ", *(p + i));
  }
  free(ptr);//释放
  //free(p);//释放 ptr 就够了,因为 p 包含于 ptr
  ptr = p = NULL;//都要置空
  return 0;
}

c4504c4f160d4ace9c67083cd0fb50c1.png


🌱注意

注意


1.realloc 申请后要对其返回值进行强制类型转换

2.申请空间的大小不必自己进行计算,通过 sizeof 配合目标数量就好了,realloc 申请的空间大小至少要大于原空间大小,不然没意义

3.使用前要判断,使用时不要越界,使用后要释放

4.申请要合理,不要无限申请,这样会造成严重的后果

5.realloc 对参数1传递空指针,等价于 malloc

6.申请空间时,不要申请0字节大小的空间,这是标准未定义的行为,具体实现操作取决于编译器


🌲五、小结


 不难发现这几个动态内存管理都有相似之处,比如需要对返回地址进行判断、使用完后对开辟空间进行释放等。于是我们可以把动态内存开辟的常见错误总结为以下几点:


1.对空指针进行解引用(开辟后没有进行判断)

2.对开辟空间的越界访问(使用空间与开辟空间不匹配)

3.对非动态内存开辟的空间进行释放(比如在栈区上开辟的空间是不能释放的)

4.释放空间与申请空间不匹配(跟第2点很像,使用这些空间时要注意!)

5.对同一块动态开辟空间进行多次释放(开辟的空间只能释放一次,释放过后的空间不能再释放)

6.动态开辟的空间忘记释放(内存泄漏问题,这个问题比较严重)

7.通过传值函数调用开辟空间(形参的改变并不会影响实参,此时通过函数开辟的空间处于无人认领的情况,而主函数中释放的空间也并非在堆区上开辟的空间)

关于以上错误的详情可以参考这篇文章:常见的动态内存的错误 和 柔性数组


目录
相关文章
|
30天前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
|
3月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
14天前
|
存储 大数据 C语言
C语言 内存管理
本文详细介绍了内存管理和相关操作函数。首先讲解了进程与程序的区别及进程空间的概念,接着深入探讨了栈内存和堆内存的特点、大小及其管理方法。在堆内存部分,具体分析了 `malloc()`、`calloc()`、`realloc()` 和 `free()` 等函数的功能和用法。最后介绍了 `memcpy`、`memmove`、`memcmp`、`memchr` 和 `memset` 等内存操作函数,并提供了示例代码。通过这些内容,读者可以全面了解内存管理的基本原理和实践技巧。
|
14天前
|
缓存 Linux C语言
C语言 多进程编程(六)共享内存
本文介绍了Linux系统下的多进程通信机制——共享内存的使用方法。首先详细讲解了如何通过`shmget()`函数创建共享内存,并提供了示例代码。接着介绍了如何利用`shmctl()`函数删除共享内存。随后,文章解释了共享内存映射的概念及其实现方法,包括使用`shmat()`函数进行映射以及使用`shmdt()`函数解除映射,并给出了相应的示例代码。最后,展示了如何在共享内存中读写数据的具体操作流程。
|
1月前
|
存储 程序员 C语言
【C语言】动态内存管理
【C语言】动态内存管理
|
1月前
|
存储 编译器 C语言
C++内存管理(区别C语言)深度对比
C++内存管理(区别C语言)深度对比
60 5
|
1月前
|
C语言
C语言动态内存管理
C语言动态内存管理
26 4
|
26天前
|
存储 NoSQL 程序员
C语言中的内存布局
C语言中的内存布局
27 0
|
30天前
|
C语言
【C语言篇】字符和字符串以及内存函数详细介绍与模拟实现(下篇)
perror函数打印完参数部分的字符串后,再打印⼀个冒号和⼀个空格,再打印错误信息。
|
30天前
|
存储 安全 编译器
【C语言篇】字符和字符串以及内存函数的详细介绍与模拟实现(上篇)
当然可以用scanf和printf输入输出,这里在之前【C语言篇】scanf和printf万字超详细介绍(基本加拓展用法)已经讲过了,这里就不再赘述,主要介绍只针对字符的函数.