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

简介: 【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月前
|
C语言
【进阶C语言】数组笔试题解析
【进阶C语言】数组笔试题解析
17 0
|
1月前
|
C语言
c语言内存函数的深度解析
c语言内存函数的深度解析
32 3
|
2天前
|
Java 容器 Spring
Javaweb之SpringBootWeb案例之 Bean管理的Bean作用域详细的解析
Javaweb之SpringBootWeb案例之 Bean管理的Bean作用域详细的解析
9 0
|
2天前
|
JSON 前端开发 测试技术
Javaweb之SpringBootWeb案例员工管理之新增员工的详细解析
Javaweb之SpringBootWeb案例员工管理之新增员工的详细解析
13 0
|
16天前
|
存储 算法 安全
深度解析JVM世界:JVM内存分配
深度解析JVM世界:JVM内存分配
|
20天前
|
存储 缓存 监控
深入解析linux内存指标:快速定位系统内存问题的有效技巧与实用方法(free、top、ps、vmstat、cachestat、cachetop、sar、swap、动态内存、cgroops、oom)
深入解析linux内存指标:快速定位系统内存问题的有效技巧与实用方法(free、top、ps、vmstat、cachestat、cachetop、sar、swap、动态内存、cgroops、oom)
|
28天前
|
监控 Linux 调度
【Linux 应用开发 】Linux 下应用层线程优先级管理解析
【Linux 应用开发 】Linux 下应用层线程优先级管理解析
45 0
|
1月前
|
存储 编解码 Linux
深入解析Linux C/C++ 编程中的内存泄漏问题
深入解析Linux C/C++ 编程中的内存泄漏问题
112 1
|
1月前
|
存储 关系型数据库 MySQL
|
1月前
|
分布式计算 NoSQL 大数据
探索数据宇宙:深入解析大数据分析与管理技术
探索数据宇宙:深入解析大数据分析与管理技术
55 1

热门文章

最新文章

推荐镜像

更多