【C进阶】——动态内存管理详解 及 经典笔试题解析(一)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【C进阶】——动态内存管理详解 及 经典笔试题解析

这篇文章,我们一起来学习C语言中的动态内存管理!!!


1.为什么存在动态内存分配

我们先来想一下,我们现在掌握的开辟内存的方式是什么:


是不是就是直接创建一个变量或者数组,然后操作系统给我们分配空间:

int main()
{
  int val = 20;//在栈空间上开辟4个字节
  int arr[10] = { 0 };//在栈空间上开辟40个字节的连续空间
  return 0;
}

大家思考一下这样的方式有没有什么弊端:


我们这样定义一个数组int arr[10],开辟的空间大小是固定的。

int arr[10]就只能存的下10个整型,我们想多存一个都不行。

我们想存11个整型,用int arr[10]这个数组就不行了,除非我们再定义一个数组。

其次:数组在声明的时候,需要指定数组的长度,它所需要的内存在编译时分配。

但是,对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道。

那这时候,这样开辟空间的方式就不行了。

这个时候就需要动态开辟内存空间了。


那怎么实现动开辟内存呢?


C语言给提供了一些函数使得我们可以实现对内存的动态开辟。


2.动态内存函数的介绍

接下来我们就来一起学习一下这些函数:


2.1 malloc

看一下它的参数:


void* malloc (size_t size);

那它是用来干嘛的呢?

5a268f78dcca431e8e634500175eb4cf.png

接下来再来给大家详细解释一下:

  1. 参数size_t size接收我们想要开辟的内存空间的大小,单位是字节,返回指向该内存块开头的指针。
int main()
{
  void* p = malloc(40);
  return 0;
}

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

malloc给我们返回的指针类型是void*,但我们知道void*是不能直接解引用的,注意使用时要先转换为我们需要的指针类型。

比如我们想再申请的空间里放整数,就应该这样搞:

int* p = (int*)malloc(40);

然后,我们就可以往里面放整型数据了。

当然,你想用来放其他数据,就转换成其它相应的类型。


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

当然用malloc开辟空间也有可能开辟失败,当请求失败的时候,它将会返回空指针(NULL)。

我们知道空指针是不能直接解引用的。

所以,对于malloc的返回值,使用之前,我们一定要检查一下。

如果为空,那就是失败了,就不能使用了。

那什么时候又可能失败呢,比如当我们开辟的空间特别大的时候,就有可能失败返回空指针。

如果开辟失败我们可以做一个相应处理,打印一下错误信息,然后return一下,让程序结束。

  int* p = (int*)malloc(40);
  if (p == NULL)
  {
    printf("%s\n", strerror(errno));
    return 1;
  }

函数strerror我们在之前的文章里介绍过。

当然我们也可以断言一下:

assert(p);

如果不为空,那就是开辟成功了。

开辟成功,我们就可以使用了。

举个例子,我们现在就在上面开辟好的P指向的40字节的空间里放一些整型数据。

  int i = 0;
  for (i = 0; i < 10; i++)
  {
    *(p + i) = i;
  }

40个字节,我们可以放10个整型,0到9。

我们也可以通过内存观察一下:

使用前:

cb6f689d9b4c4de09407ae3befde6f4c.png

这里再给大家提一点:

我们发现开辟好的空间里面放的这些其实是一些随机值

这也是malloc的一个特性:

  1. 新分配的内存块的内容不做初始化,仅保留不确定的值。

使用后:

f1855d9bf03240138b723d1d9e5ae557.png

如果参数size_t size为0,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。

此时malloc的行为是标准是未定义的,取决于编译器。

所以我们尽量不要这样试,况且这样做也没什么意义,申请一个大小为0的空间?


那申请的空间使用完之后,我们是不是什么都不用管了呢?


不是的,对于像malloc这些函数动态开辟的内存,使用完之后我们是需要将这些空间释放掉的,不及时释放,有可能会造成内存泄漏。


那怎么释放呢?


2.2 free

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


接下来我们就来一起学习一下函数free:


它的参数是这样的:

1e3e9664ca02486e9c595a2f8eba5952.png

怎么用呢?

  1. 参数void* ptr接收一个指针,这个指针指向我们使用malloc这些动态开辟内存函数分配的内存块,无返回值。

比如,上面例子中的指针P:

  int* p = (int*)malloc(20);
  /*if (p == NULL)
  {
    printf("%s\n", strerror(errno));
    return 1;
  }*/
  assert(p);
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    *(p + i) = i;
  }

在上述循环的过程中,p 的指向并没有发生改变,还是指向分配的内存块的起始地址,所以我们就可以这样做:

free(p);

这样,就把malloc申请的空间释放掉了。

那释放掉之后,是不是就万事大吉了呢?

不,我们还应该做一件事情:

p置空

p = NULL;

为什么要这样做呢?


大家想一下,我们现在虽然已经把p指向的那块空间给释放掉了。

但是,p是不是还保存着那块空间的地址啊。

那么一个指针指向了一块被释放掉的空间,那它是不是一个典型的野指针啊。

要知道如果对一个野指针解引用那程序就会出错的。


如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。

也就是说参数 ptr 指向的空间必须是动态开辟的。

如果指向其它的空间,那么free函数会怎么处理是标准未定义的。


比如:

int main()
{
  int num = 10;
  int* p = &num;
  free(p);
  p = NULL;
  return 0;
}

你写一个这样的代码,肯定是不行的,因为p指向的空间不是动态开辟的。

这里的num是一个局部变量,要知道局部变量是保存在栈区的,再来复习一下:

409f755863064d49ab692f30c4423794.png

而我们这些动态开辟的内存,是堆区分配的。

  1. 如果参数 ptr 是NULL指针,则函数不执行任何操作。
    像这样:
  int* p = NULL;
  free(p);

函数不执行任何操作。

2.3 calloc

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。

我们一起来学习一下:

15f49e9365bb4b61acf7726e55950dc3.png

函数calloc 有两个参数,无返回值,那它的作用是什么呢?这两个参数分别接收什么呢?


函数的功能是为 num 个大小为 size 的元素开辟一块空间,同样返回指向该内存块开头的指针,类型为(void*)

参数size_t num接收我们想要分配空间的元素个数;

size_t size接收每个元素的大小,单位为字节。


那我们就可以这样用:

int main()
{
  int* p = (int*)calloc(10,sizeof(int));
  /*if (p == NULL)
  {
    printf("%s\n", strerror(errno));
    return 1;
  }*/
  assert(p);
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    *(p + i) = i;
  }
  free(p);
  p = NULL;
  return 0;
}

当然calloc分配的空间使用完也应该使用free释放并将指向空间起始地址的指针置空。


与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

对于malloc 来说,它不会对开辟好的空间初始化,里面放的是随机值。

但是,calloc 会把申请的空间的每个字节都初始化为0。


就拿上面那段代码,我们来调式看一下:

b01b1eccb36a45498781bc8b65ac8ed6.png

  1. 和malloc 一样,calloc 函数如果开辟内存块失败,则返回空指针void*。

所以对于calloc 的返回值,我们也有必要做一下检查,判断是否为空指针。

10f083ec3313480b98dad989daa9bd47.png和malloc一样,如果参数size_t size为0,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。

标准未定义的,取决于编译器。

总的来说,malloc和calloc 区别不大:


1. calloc 会在返回地址之前把申请的空间的每个字节初始化为全0,而malloc不会,里面放的是随机值。

2. 它们的参数不同。


2.4 realloc

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


有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们就要对开辟的内存的大小做出灵活的调整。

那 realloc 函数就可以做到对动态开辟的内存大小进行灵活的调整。


一起来学习一下:

eaa63407552e43199c49b9823c980811.png

两个参数分别接收什么呢?


void* ptr接收一个指针,该指针指向我们想要调整大小的内存块,当然这块内存块也应该是我们之前动态开辟的空间。

size_t size接收我们想要为内存块调整的新大小,以字节为单位。


返回值又是什么呢?


返回指向重新分配的内存块的指针


举个例子吧,我们再来看一段上面的代码:

int main()
{
  int* p = (int*)malloc(40);
  assert(p);
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    *(p + i) = i;
  }
  free(p);
  p = NULL;
  return 0;
}

还是这段代码:

我们使用malloc申请了40个字节空间,放了10个整型。

那假设我们现在想再放10个整型,那原来的空间就不够用了,那我们现在就可以使用realloc 进行扩容。

怎么搞呢?这样写:

  int* p = (int*)malloc(40);
  assert(p);
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    *(p + i) = i;
  }
  int* ptr = (int*)realloc(p, 80);
  if (ptr != NULL)
  {
    p = ptr;
    ptr = NULL;
  }
  //使用
  free(p);
  p = NULL;

变成这样,我们再中间又加了一些代码。


我们看到上面代码中我们扩容后返回的指针赋给指针变量ptr ,那为什么不直接给p呢?

因为,realloc开辟空间也有可能会失败的,它失败同样返回空指针。

所以我们先赋给ptr ,然后判断一下,不为空,再赋给p,让p继续管理扩容后的空间。

然后,不使用ptr ,最好将其也置空。


然后,没什么问题,我们就可以使用扩容后的空间了。


但是,在扩容的时候,又存在存在两种情况:


原地扩

什么时候是原地扩呢?

就还拿刚才的例子来说:

int* ptr = (int*)realloc(p, 80);

p原来指向的空间是40个字节,现在我们想要使用realloc将p指向的空间扩容为80个字节。

那这时realloc就会从原空间向后看,如果后面有足够大的空间能够再增加40个字节,那么realloc就会在原地向后扩容40个字节,使得p指向的空间变为80字节。

0ff107a384cc4043851a80fbb929f401.png

当然这样realloc返回的地址还是原来p指向的地址。


异地扩

那什么时候异地扩呢?

假设现在还是相把p指向的空间扩容为80个字节。

但是,原空间后面没有足够大的空间,那这时候怎么办?

这时候:

realloc会在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址,不再指向原空间。

而且:

realloc会将原空间的数据拷贝到新空间,并会将旧空间释放掉。然后返回指向该内存块起始地址的指针。

比如:

int* p = (int*)realloc(NULL, 40);

那这句代码就相当于:

int* p = (int*)malloc(40);

以上就是对这4个动态内存函数的介绍,它们包含的头文件都是#include

目录
相关文章
|
1月前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
38 6
|
1月前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
2月前
|
编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(上)
动态内存分配与管理详解(附加笔试题分析)
72 1
|
2月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
23天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
2月前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
2月前
|
程序员 编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(下)
动态内存分配与管理详解(附加笔试题分析)(下)
58 2
|
2月前
|
存储 安全 Java
JVM锁的膨胀过程与锁内存变化解析
在Java虚拟机(JVM)中,锁机制是确保多线程环境下数据一致性和线程安全的重要手段。随着线程对共享资源的竞争程度不同,JVM中的锁会经历从低级到高级的膨胀过程,以适应不同的并发场景。本文将深入探讨JVM锁的膨胀过程,以及锁在内存中的变化。
49 1
|
2月前
|
Java C语言 iOS开发
MacOS环境-手写操作系统-16-内存管理 解析内存状态
MacOS环境-手写操作系统-16-内存管理 解析内存状态
51 0

热门文章

最新文章

推荐镜像

更多