C生万物 | 动态内存管理-1

简介: C生万物 | 动态内存管理

一、为什么存在动态内存分配

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

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

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

  1. 空间开辟大小是固定的
  2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配
  • 那此时呢我们就希望有一种方式,可以在程序运行的过程中动态地去开辟当前程序所需要的内存空间,此时就需要使用到我们的【动态内存函数】了

二、动态内存函数的介绍

本文我总共会介绍三种动态内存函数,分别是malloc()calloc()realloc(),与之对应内存释放函数还有free()

1、malloc和free

【函数原型】:

void* malloc (size_t size);

【函数解读】:

  • 首先我们来看一下malloc()这个函数,它会向内存申请一块连续可用的空间,并返回指向这块空间的指针

image.png【特点】:

  1. 如果开辟成功,则返回一个指向开辟好空间的指针
  2. 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
  3. 返回值的类型是void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定

这里我们来举一个例子说明一下

  • 可以看到,我在使用一个动态开辟出来的内存时分配四步走(不止四步),首先使用到malloc()函数去向内存申请大小为40的空间,由于其返回值是一个void*的指针,可以接收任何类型的指针,所以这里我去做了一个强转,将这块空间强制类型转换为int*
  • 上面说到在开辟空间的时候会有失败的可能性,所以我们要去做一个异常判断,若是这个指针为空的话,表明我们完全没有申请到相应的空间,那这个时候再去对这块地址进行操作的话就会造成==空指针异常==的问题
  • 在明确这块空间被开辟出来后,我们要先去做一个初始化操作,指针的访问这一块就不细说了,不太懂的同学可以去看看C语言指针一文。在初始化后就是将其去进行一个打印的操作
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
  // 1.开辟空间
  int* p = (int *)malloc(40);
  // 2.异常判断
  if (NULL == p)
  {
    perror("malloc fail");
    exit(-1);
  }
  // 3.初始化空间
  for (int i = 0; i < 10; ++i)
  {
    *(p + i) = i + 1;
  }
  // 4.打印观察
  for (int i = 0; i < 10; ++i)
  {
    printf("%d ", *(p + i));
  }
  return 0;
}
  • 我们通过调试来进行观察,便可以发现我们刚好将所开辟的40个空间存放了10个整型数据

  1. 如果参数size为0,malloc的行为是标准是未定义的,取决于编译器。
  • 还有一个特点,单独再说一下,看了上面的函数解读后可以知道我们需要给malloc()函数传递进去一个size大小,它便会为我们开辟出指定的空间,但若是我们传递的参数为0的话,就显得很荒唐。
  • 举个例子:就好比你向别人借钱,如果你说要借50、100那还算正常,但是说 “我要借0元”,那对方就会感觉到很奇怪,他到底要给你些什么东西呢?那编译器其实也是一样的,不过呢,既然你去要东西了,它还是会给你点什么。通过调试可以观察到虽然我们没有申请到任何的东西,但是呢却有了这么一块地址,这还是要看不同的编译器,反正在VS下还是会给你一个反应的

image.png

但是呢就上面这一些操作还是不够的,别忘了我们还有一个free()函数还没介绍呢

【函数原型】:

void free (void* ptr);

【函数解读】:

  • 然后我们来看看这个函数,它主要用来释放动态开辟的内存

image.png【特点】:

  1. 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的
  2. 如果参数 ptr 是NULL指针,则函数什么事都不做
  • 所以我们在刚才那段代码的下面应该再加上一个free(p)才行,但是这样真的就可以了吗?
free(p);
  • 我们可以通过调试来观察一下,当执行完这句代码后初始化的1 ~ 10变成了一些随机值,这也就意味着我们一开始申请的这块空间还给操作系统了,所以里面所存放的这些内容都销毁了,不过从上面对于这个函数的解读中我们可以看出即使我们将这块空间还给操作系统了,但是这块申请空间的地址还是在的
  • 那么也就意味着这个指针p现在变成【野指针】了,变得非常危险

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bcd3c28ac3864eeeb7fc4fb86b902aed~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1247&h=422&e=gif&f=78&b=f5f4f4

  • 若是我们想化解这个危机的话,可以在free(p)之后再将其置为NULL即可,此时就无法再找到之前的那块地址了

image.png


【注意实现】:

  • malloc和free都声明在 stdlib.h头文件中,记得要引头文件
  • 每次在使用【malloc】申请完一块空间后,一定要去做一个判空,预防申请失败的情况。而且在使用完这块空间后还要将其归还给操作系统,并且将指针所指向的这块地址置为空,防止野指针

2、calloc

讲完【malloc】之后我们再来讲讲另一个动态内存函数【calloc】

【函数原型】:

void* calloc (size_t num, size_t size);

【函数解读】:

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

image.png【特点】:

  1. 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
  2. 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
  • 一样,我们可以通过调试来进行观察,与【malloc】不同的地方在于当我们申请到10个大小为4字节的空间后,发现这10个数据均为0,即在申请的同时就已经为初始化好了,不需要我们自己再去初始化

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2491ce6f89224c7abce040b2582a7605~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1220&h=422&e=gif&f=62&b=cee9d6

  • 如果还是觉得有点不可思议的话,我们可以再通过汇编去仔细看看

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/304a229b61ff439d8bd8d078d37dc9e2~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1179&h=467&e=gif&f=97&b=cce8d0

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

3、realloc

最后再来讲讲另一个动态内存函数【realloc】

【函数原型】:

  • ptr是要调整的内存地址、size是调整之后新大小、返回值为调整之后的内存起始位置
void* realloc (void* ptr, size_t size);

【函数解读】:

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

image.png

具体地我们来看一下要如何去使用这个realloc()进行一个扩容

  • 可以看到,在下面我首先申请了5个整型空间的大小,对其做了初始化之后就去做了一个扩容,要扩容的地址即为p,扩充后的容量便是10个整型数据
int main(void)
{
  int* p = (int*)malloc(sizeof(int) * 5);
  if (NULL == p)
  {
    perror("malloc fail");
    exit(-1);
  }
  for (int i = 0; i < 5; i++)
  {
    *(p + i) = i;
  }
  // 不够了,增加5个整型空间
  p = (int*)realloc(p, sizeof(int) * 10);
  return 0;
}

💬 不过呢,我这里还要讲一下这个realloc到底是怎么进行扩容的,因为它有一个扩容机制,分为【本地扩容】和【异地扩容】

realloc扩容机制:【本地扩容】和【异地扩容】

  • 本地扩容,即在本地就有足够的空间可以扩容,此时直接在后面续上新的空间即可
  • 异地扩容:当后边没有足够的空间可以扩容,realloc函数会找一个满足空间大小的新的连续空间。把旧的空间的数据,拷贝到新空间的前面的位置,并且把旧的空间释放掉(无需手动释放),同时返回新的空间的地址

image.png


可能这么说还不是很好理解,我这里再通过一个生活小案例来帮助理解

  • 平常我们外出旅游的时候由于比较遥远,无法一天之内回来,就会选择一些酒店或者旅馆🏠暂住一宿,那假设这个时候有一个旅行团要去住酒店,因为这家酒店只有单人间,可是呢他们有四个人,因为感情好,想住在一起,所以就让酒店前台开了一些连着的四个房间,互相之间串门方便一些,对于给出的这四个房间其实就是一开始为数组malloc空间,比较小一些
  • 然而这个时候呢,他们四个人又分别叫了自己的伙伴来,一起进行下一天的结伴旅行,想要和他们住在一起,于是问酒店前台小赵🤵可以不可以在已经为他们开的四个房间的后面再连续地开四个房间,这样他们8个人就可以住在一块了,虽然这很过分😀,但是刚好真的有连续的房间空出来,所以就又为他们开了四间房,这个时候新开的四间房就叫做==本地扩容==,就是在与上一次开辟空间后临接着开辟的

image.png

  • 这个时候这个前台小萌新就不知道怎么办了,于是去隔壁找了一个经验丰富的管理人员【老王】,老王这个时候想,既然他们是朋友,那就找一个一排空房间给到他们好了,原来的四间房还可以空出来。就在酒店的另一个大区域为他们开了八间房,然后让服务员把原来的四个人叫出来,把他们安置到新的四间房内,然后他们住过的房间就可以重新空出来为其他房客用了,接下去呢又把新来的四个人安排在他们的后面的接连房间内,于是他们8个人就并排地住在了一起,过上了幸福美满的生活。。。。哦,不对,应该是度过了一个美好的晚上🏠
  • 这里说的为他们8个人重新找一块区域安置就叫做==异地扩容==,也就是将原本开辟的空间中所存放的内容拷贝过来,然后放到新的空间中,接着把需要新放入的内容接着旧的内容之后

💬 通过上述这样一个例子,你是否理解了【本地扩容】和【异地扩容】呢 👈

【注意事项】:

  • 这里我还要讲一个注意点,如果仔细一点点学习下来的同学一定会想到一个问题,如果在扩容的时候失败了怎么办呢?此时realloc就会返回一个空指针
  • 但是当我们上面对这个指针p所指向的地址进行扩充后,又将其赋值给了自己,若真像我们上面所扩容失败返回空指针的情况,此时再去使用p的时候就会出现【空指针异常】的问题
p = (int*)realloc(p, sizeof(int) * 10);

💬 那有同学说:这该怎么办呀🤔

  • 对于这个问题,我们的解决办法一般是这样的,定义一个新的指针tmp去指向这块空间,再扩容结束后再去判断一下这个指针是否NULL,若是为NULL的话代表扩容失败,此时应该打印错误信息然后结束程序,不要再往下执行了,而是当这个地址不为空的时候再将让原先的指针p指向它,让我们从头至尾都在维护同一个指针
  • 因此我们在扩容之后应该再去加上这么一个判断才行,在赋值完后别忘了把临时的tmp指针置为空,防止其变为【野指针】
// 不够了,增加5个整型空间
int* tmp = (int*)realloc(p, sizeof(int) * 10);
if (tmp == NULL)
{
  perror("fail realloc");
  exit(-1);
}
p = tmp;
tmp = NULL;   // 这个指针不用了,记得置为空

当代码补充完整后,我们再通过调试来观察一下本地扩容和异地扩容

  • 首先是本地扩容,可以看到realloc返回的地址就是原先开辟出来那块空间的首地址

  • 然后是异地扩容,我们可以将需要扩充后的容量调大,这样后续的容量就会不够了,此时编译器便会在内存中再去找一块合适大小的空间,然后将原先的5个整型数据先拷贝过去,然后再在其后开辟出剩余的空间,最后再释放掉原先的那块空间

实际应用:数据结构之【顺序表】与【顺序栈】

对于这个【realloc】,它是有实际的应用场景的

  1. 首先第一个就是我们在数据结构之顺序表中在进行【尾插】的时候所做的扩容检查工作
  • 以下具体的代码实现,对于顺序表来说,我在一开始是没有给他分配任何空间的,因此在进行第一次尾插的时候就会进入到下面这段扩容机制中,首先就判断当前顺序表的容量是多少,再来决定需要扩容的大小,这里就很好地利用了realloc的一个机制:当传递的指针为空的时候,其所表现得行为就和malloc是一样的
//检查是否需要扩容
void SLCheckCapacity(SL* ps)
{
  if (ps->size == ps->capacity)
  {
    int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
    SLDataType* tmp = (SLDataType*)realloc(ps->a, newCapacity * sizeof(SLDataType*));
    //判断是否开辟空间成功【失败会返回空指针null pointer】
    if (tmp == NULL)
    {
      perror("realloc fail\n");
      exit(-1); //结束掉程序【程序异常结束】
    }
    //扩容成功
    ps->a = tmp;
    ps->capacity = newCapacity;
  }
}
  1. 我们在数据结构之顺序栈中在讲入栈操作的时候也有使用到它,因为对于顺序栈来说会出现空间不够的情况,所以我们也需要去实现一个扩容的机制,
  • 以下就是具体的入栈代码实现,判断当前栈顶指针是否达到了栈的容量大小,如果是的话就找执行扩容逻辑,每次扩容的大小为原先的2倍,也是使用到了临时的指针tmp去做一个接受,判断其不为空后再去使用扩容之后的这块空间
/*入栈*/
void PushStack(ST* st, STDataType x)
{
  //栈满扩容逻辑
  if (st->top == st->capacity)
  {
    //初始化时已经malloc开辟过空间了,因此无需考虑容量为空的情况
    STDataType* tmp = (STDataType*)realloc(st->a, st->capacity * 2 * sizeof(STDataType));
    if (tmp == NULL)
    {
      perror("fail realloc");
      exit(-1);
    }
    st->a = tmp;
    st->capacity *= 2;
  }
  st->a[st->top] = x;   //top指向栈顶元素的后一元素,因此直接入栈即可
  st->top++;    //然后栈顶指针后移,为下一次入栈做准备
}
相关文章
|
C语言 C++
【C生万物】 字符串&内存函数篇 (上)(一)
【C生万物】 字符串&内存函数篇 (上)
94 0
|
1月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
291 1
|
22天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
1月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
1月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
22 3
|
1月前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
50 1
|
1月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
89 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
2月前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
2月前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
63 2

热门文章

最新文章