动态内存申请函数 malloc_malloc函数——一个低调的C语言学习者宝藏

写在前面:我准备分次持续更新这个专栏,这个文章将包含很多块知识,所以会很长,我将在这个部分完结之后把它再整理成多个小模块,让它可读性和针对性更强,目前每次更新的内容和时间都会写在前面,并且用大写的"Chapter 1-X" 标记,希望能够坚持完成。

这篇文章,全部来自于实战和三本C语言书籍《C程序设计语言》,《C语言程序设计(现代方法)》,《C编程专家》。 同时,它将是完全0抄袭复制的文章,笔之所至,皆为自身所思所想。 如果你们能从中学到一些东西,将是我最大的荣幸。 我会尽量让我的文笔好一些,同时保留对定义严谨的措辞,就像《C程序设计语言》一样。

PS: 收藏+点赞 同时评论说出你的意见以及问题都将是我把这个文章写完的源动力,爱你们。

(双击屏幕会提高自己的阅读效率,试一下哦。)

再PS:这篇文章是基于C语言程序设计的思想以及C语言的特性来写成的,会涉及到内存,但是不会深入到Linux内核的层次,参考书目在上,望了解。


2020.8.26 首次发布,新增第一个模块,指针与malloc函数的关系。

2020.9.22 第二次发布,chapter2, 指针的指针,局部变量的生存周期,堆栈空间。

2020.9.28 第三次发布, chapter3, 重写malloc函数,单链表。

2020.10.7, 第四次发布, chapter4 , 拾遗,回答一些为什么。

2020.10.20 拾遗 : 关于在malloc中联合union的使用。

公众号: 黄桃罐头与蜘蛛 写一写可爱的事物。
为什么是黄桃罐头与蜘蛛?​mp.weixin.qq.com

b4708794-4b13-eb11-8da9-e4434bdf6706.png


Chapter 1

我知道你想要什么

我将直接告诉你,你可以从一个malloc函数中学到哪些C语言的核心内容?

而不是这样:

我知道malloc,不就是一个函数,接受以字节为单位的参数,返回一个void *类型的指针,然后这个指针指向这块申请内存的地址吗

以下的内容将让你从这个标准库中的一个malloc函数,学习且不限于学习到:

  • 指针是什么,指针类型的区别,指针的运算规则以及与其他type的区别。
  • 内存是什么,堆栈是一个东西吗,地址与内存的映射关系。
  • 局部变量和全局变量的区别
  • 链表相关的数据结构
  • 为什么链表结构的初始化要用到 “**”双重取值
  • 指针用法的注意事项,为什么要杜绝野指针
  • 为什么我们需要用malloc,而不是用全局变量
  • ...

是不是听起来些微有些不可思议,malloc()不就是一个申请内存的函数接口吗,最多我需要知道当我malloc之后不再使用之后free掉就可以了吗

但是事实是,如果你有心,你将可以拓展的更多,甚至于拓展到汇编,编译上,这是一种学习思维和方法,也就是初高中时老师常常说到的平平无奇的“举一反三”。

来吧,这个平平无奇的malloc世界欢迎你。

预告

接下来我的内容将包含以下几个哲学主题

  1. 我们从哪里来,我们将到哪里去
  2. 探索吧,迷雾迷雾快走开
  3. 看山不是山,看水不是水
  4. 如果上帝在你出生的时候告诉你1+1=3呢

我们从哪里来,我们将到哪里去

这个主题作为开始最好不过了,因为他没有那么花里胡哨,他直指人心。

malloc函数是干嘛的,我们为什么要用它?
  1. 需求

当我们需要一块内存,它可以存储我这段时间想要的数据,而且我所有的函数都可以去使用它,等我这段时间的任务完成,不需要它的时候,不需要更多的代码,我可以把它扔了,省出资源来。

总的来说,就是

我想当渣男,当我需要恋爱的时候,你就过来,你得满足我恋爱的所有需求,陪我看电影,陪我吃饭,陪我.. 当我这段时间不想恋爱了,就把你扔了。 而且下次,我想恋爱的时候,我可以换一个新的。

2. 接口思想

malloc作为一个function(之后统称函数为function)

它的输入是一个以字节位单位的size_t整数, 也就是unsigned int,无符号整数。

它的输出是一个指针,这个指针指向你传入size的内存,如果申请内存失败,空间不够等原因,它将返回一个空指针。

malloc可以理解为一个接口,我们输入它定义的参数,它返回一个我们想要的值。

其实很多时候,无论大至一个系统,还是小至一个函数,都是一个接口,首先你要明白你要做什么,还有function能提供给你什么,将他们整合起来,就是解决问题的一个不二法则。

探索吧,迷雾迷雾快走开

如果你是一个C语言的新人,你一定被它吓到过,它的样子异于常人,它善于劝退一些不思考的懒汉。

它迷人而危险,它的名字是Pointer。

就从这片迷雾开始探索吧。

  1. 指针

引子:

malloc的输出是一个指针,用来指向这片申请的内存。

(写在前面,我将不在这里复述一些简单的概念问题,相信你们能理解。)

这句话很容易理解,返回的指针指向这片内存。这片内存的大小是我们申请的size。

举个例子:

b6708794-4b13-eb11-8da9-e4434bdf6706.png

字面意思是:

p是一个指向int类型的指针,我们申请一块20bytes的内存,然后将它的返回值强制转换为int类型的指针,然后赋给p。

所以现在p是一个指向int的类型的指针,且它的值是这块内存的起始地址。

这个不难理解,让我们来看一下吧。

b8708794-4b13-eb11-8da9-e4434bdf6706.png

这里,我们看到p: 0x560ef8, 且我们的系统是将int默认为4个char。

如果我这样写:

c4708794-4b13-eb11-8da9-e4434bdf6706.png

是不是看起来很棒,我恰好申请了一个int类型的空间,且将它转成int型的指针,还要赋给一个Int类型的指针,一切看起来都那么刚好。

可是,牛郎织女的故事总是没有那么的顺利。

比如刚开始的例子,

b6708794-4b13-eb11-8da9-e4434bdf6706.png

我们申请了20个字节,但是p是一个int类型的指针,而且int在这里是4个字节,所以p指向的这个内存应该是5个int吧

让我们来试试看:

ca708794-4b13-eb11-8da9-e4434bdf6706.png
  1. 首先,我们发现p指向的地址变了,变成0x630ef8, 和之前0x560ef8不同,这说明一点,malloc是动态申请内存,当我们再次申请的时候,也就是我们抛弃旧爱,拥抱新欢的时刻,所以我们用的是一块新的内存。
  2. 这个循环要做的是将p看做一个Int的数组指针,所以p[0]代表p指向的这个Int数组的第一个Int数,我们看到这一切都那么刚好,我们赋值也完全正确。
  3. 下面注意下这些int数的地址指针,也刚好是4个,比如0x630ef8->0x630efc 刚好差4。

如果,我们干点坏事呢?

比如这样:

cd708794-4b13-eb11-8da9-e4434bdf6706.png

咦,我们申请的只是20个字节,可是我可以用更多哎,我用到了24个字节,赋值还是没问题,我是不是天才?

absolutely no !

Tips:
除了main函数,其他的function的局部变量的生命周期一般随着function结束而被抹去。

事实上,这可以说是悬崖边的罂粟花。

原因有以下几点:

1. 你动用了系统未知的内存,并且用了一些方法给它赋值,那么原来那块内存的信息就会被抹去,会对整个系统造成一个极大的危害以及隐患。
2. 值得注意的是,最恐怖的也是在此,如果你动用的不是系统内存,C语言编译甚至不会报错,那么带着这个 有着巨大隐患的系统,遗害无穷。

所以我们对于malloc有几条准则:

  1. malloc 必须和 free 成对出现,如果申请了,就一定要释放,如果你一直需要它,申请一个全局变量放在ram中会是一个更好的选择。
  2. 申请的malloc一定要注意到它是否是NULL,否则后续工作都会出问题。
  3. 注意到malloc申请的内存不要越界是一个程序员的必修课。

你是不是觉得自己又可以了,准则也会了,思想也有了,参数返回值都懂了

那么,看一看这个实例:

首先你们来判断一下这两种申请内存的写法对不对

NO.1

d1708794-4b13-eb11-8da9-e4434bdf6706.png

NO.2

d4708794-4b13-eb11-8da9-e4434bdf6706.png

这里是你们思考的时间。


Chapter 2

承接上文,

第一种方法是对的,而第二种错了。

但是也不建议使用第一种方法,因为当函数复杂化,很容易混淆。

来吧,我们先花点时间了解下生命周期以及作用域。

局部变量的生命周期到函数块的结尾处就消亡。

这是一个非常非常重要的概念。

拓展:请花时间去搜索“const”,"static"这两个关键字修饰的变量的作用域以及生命周期,你将获得学会“举一反三”的奖励。

请记住它。

当我们调用很简单的函数时,我们知道,想要使主函数的一个变量数值通过一个function来改变它,那么我们需要传递这个变量的指针,也就是这个变量存储的内存块的起始地址。

函数的传参会被复制,你只能依靠指针控制他。

void 

再记住这句话,因为它告诉你:

这个简易的函数并不会改变你主main中dat这个变量,因为"a"是复制品,并不是真的dat.

这里的知识点会涉及到:

  1. 堆栈空间
  2. 汇编语言

请跟着我来。

d6708794-4b13-eb11-8da9-e4434bdf6706.png

这是一张内存分布图。

C语言内存分布图----栈空间、堆空间_Bin的博客-CSDN博客​blog.csdn.net
da708794-4b13-eb11-8da9-e4434bdf6706.png

我在这里不做过多的赘述,这里贴一个连接,有兴趣可以深入的了解它,会对了解整个计算机的内存以及运转规律有很大的帮助。

现在你只需要知道,对于c语言来说,如果在main中调用了某个函数,那么这个函数的调用地址,以及下一条语句的起始地址,以及返回值,返回地址都会被压到堆栈区,准确的说是在栈区。

当主调函数caller调用结束被调函数callee之后,所以在堆栈区的被调函数的参数将会被一一弹出。

这里同样说一句题外话,我现在是用VS来用做调试,那么熟悉GCC的命令对于理解C语言也是很有帮助的,所以这里同样是个拓展点哦。
可以在中断用gcc -S xxx.c的命令查看汇编文件。

如果不懂这些,那么先记住这句话:

局部变量的生命周期到函数块的结尾处就消亡。

所以我们很容易看出来上一节提出的两种申请动态内存的方法都是错误的。

因为无论no.1中的return p;中的指针p,还是no.2中的指针pr都会随着调用函数的结束而销毁。

正确申请动态内存的方法应该是这样:

dc708794-4b13-eb11-8da9-e4434bdf6706.png
申请一个int类型的变量的内存空间

df708794-4b13-eb11-8da9-e4434bdf6706.png

e2708794-4b13-eb11-8da9-e4434bdf6706.png
打印结果

这个地方理解起来会稍微有点抽象,那么不妨画一张图,可以帮助你理解指针和指针的指针的含义。

e5708794-4b13-eb11-8da9-e4434bdf6706.png

你只需要知道在函数结束后只是“原物奉还”。

你申请了一个指向指针的指针,那么它还是一个指针,当结束以后只是这个指针的内存空间被销毁了,可是malloc申请的内存空间的指针还好好的在那儿可以一直使用,直到你将它free掉。

这里是后面链表初始化的基础知识,请牢记。

“我为什么要弄那么复杂,二级指针那么复杂,我完全可以用一级指针就够了啊,还增加了代码的易读性。”

我曾经也有这样的疑问,那么请带着这样的疑问去探索吧。

我们的下一站将开始探索malloc的核心。

Chapter 3.

造轮子并不可取,但是入门的时候总是要画很多鸡蛋的。
————斯诺夫毕毕斯基.黄桃

我们将重写malloc函数。

首先回到主题来,我们需要一个什么功能,这个函数将提供什么接口。

  1. 我们有一些不连续的内存空间,我们要把它利用起来,并且用来作为提供内存的燃料的“堆空间”。
  2. 这块空间只有我们申请的时候才能够使用,当我们不需要的时候就还给“堆空间”中,可以重新被别人使用。
  3. 只需要一个可用的参数,它可以返回给我这块内存的起始地址,并且可以储存任何类型的数据。
  4. 我需要这段空间是可控的,如果不匹配将返回空指针。

如果大家有空的话,我推荐一本书,闲暇之余可以一观。

书名是《高效程序员的45个习惯:敏捷开发修炼之道(苏帕拉马尼亚姆)》

如果你刚入门,看这本书会觉得浮躁且浅薄,没有关系,就当做小时候学古文一样去读一遍,然后记住其中的建议,如果你以后从事软件开发,一定会大有裨益。

如果你已经修炼得道,想必也不需要我的建议,读起来自然有自己的感悟。

PS: 请点击关注和收藏本篇文章,在这个专题写完之后我会在最后附上各类资源的链接,不要吝啬你们的点赞,谢谢。

为什么要突然说起“敏捷开发”的问题?

这是因为当我们写代码的时候,一定要胸有成竹,框架优先是前提。

这是提高我们代码的可读性以及提高效率的不二法则。

下面开始吧。

  1. 我们申请几块动态内存用作我们自己的pool。
  2. 创建一个单链表,将这些内存串联起来。
  3. 针对这些内存和我们的需求写出malloc函数。

如果用代码来看的话,那会是这样。

申请内存

e8708794-4b13-eb11-8da9-e4434bdf6706.png
申请内存

为了模拟出堆空间,我申请了5块动态内存

其中disturb1-2是用来将内存块割裂,然后将分别申请了20,40, 60个Int类型大小的地址存在了数组p_heap[3]中,这也是为了将来表现出当申请不同大小的动态内存时,我们的malloc函数将如何反应。

链表模拟构建堆pool

这一步是非常重要的,并且在这一步中非常有助于提高你对数据结构的理解。

先提出几个问题吧,当做思考,然后我会在下一节逐步解答。

为什么选择链表而不是其他数据结构?
可以使用typedef struct 的标识符来代替struct node吗?
链表的数据结构怎么样去构建才算最优化?
我所写的代码中哪里还可以优化?

由于时间有限,代码没有最优化,不过结构一定要清晰。

  1. 存储数据

我们看到,当我们申请到内存的时候,往往是成对的,比如一块内存对应一个size和一个指向这块内存的地址addr,所以在C语言中选择合适的结构体就比较重要,虽然这里很简单,但是不妨碍我们来定义结构体来存储这段数据。

eb708794-4b13-eb11-8da9-e4434bdf6706.png
存储结构体

ee708794-4b13-eb11-8da9-e4434bdf6706.png
赋值到结构体中

2. 链表的初始化

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

f0708794-4b13-eb11-8da9-e4434bdf6706.png

这就是我们将使用的数据结构——单链表。

同样这里,如果你不了解数据结构的话,可以参照我给的链接学习一下基础。

【C语言】链表及单链表基本操作_swag_wg的博客-CSDN博客​blog.csdn.net
f3708794-4b13-eb11-8da9-e4434bdf6706.png

f6708794-4b13-eb11-8da9-e4434bdf6706.png
定义一个全局的头指针并初始化

fa708794-4b13-eb11-8da9-e4434bdf6706.png
将之前申请的内存信息存在链表中

00718794-4b13-eb11-8da9-e4434bdf6706.png
调用函数来进行初始化。

对于这里,我有几点想说的:

  • 如果你不想断点调试,那么合适的Printf函数将是你最好的搭档。
  • 一定要对有指针传入的函数进行check,杜绝野指针的出现是所有程序员的必修课,assert将是一种好办法,你要是问我那个是什么,请学会自行拓展。

重写malloc函数

现在重头戏来啦,我想要申请33个int类型大小的内存空间,请批准。

我以re_malloc命名:

03718794-4b13-eb11-8da9-e4434bdf6706.png
re_malloc函数

08718794-4b13-eb11-8da9-e4434bdf6706.png
申请33个int大小的内存

根据上面的设定,我们知道我们有三块内存现在是可以用的,分别是20,40,60个int大小,那么如果p指针的值是第二块内存地址就表明我们申请成功了。

下面来看我们的打印。

0a718794-4b13-eb11-8da9-e4434bdf6706.png
打印内容

可以看到,0x3c0fb8 使我们申请到的内容,刚好是第二块。

其实,虽然说到了malloc这个函数的由来,可是也能看出来它并不是特别的复杂,看似写起来也很简单,可是当你真正动手去写的时候会遇到很多问题。

那些问题不是拦路虎,对于C语言编程来说,只有1或者0,万物都遵循一个规律。

这是我刚入行的时候,我一个同事告诉我的。

“计算机只有1或者0,它不会欺骗,没有人那么复杂。”
https://zhuanlan.zhihu.com/p/201496904​zhuanlan.zhihu.com

之前颇多感慨,写在上面那篇文章里。

物是人非,诚如所言。

Chapter 3 完。


Chapter 4 拾遗

这个章节会逐一解答之前埋下的坑

也就是那些“为什么”

弄懂他们,或者说学会思考某种程度上是更重要的学习。

指针部分

  • 为什么在链表的操作中需要使用二级指针?

举一个例子,当我们全局定义一个结构体类型的变量时

  • 我们没有使用指针,那么系统将直接开辟一个和这个结构体大小一致的内存空间。
  • 如果我们使用指针,并且初始化定义这个结构体的指针是NULL,空指针时,那么它只占4个字节。

所以初始化链表的时候,用指针会更节省空间,并且可以减少我们初始化的工作量,当我们使用它的时候,因为初始是NULL,所以会更加小心的使用它,而不会用很多的缺省值导致之后很多难以查询的问题。

那么,当我们的初始化是指针时

0e718794-4b13-eb11-8da9-e4434bdf6706.png

那么,当我们需要对这个链表进行操作的时候,就需要用到二级指针。

比如这样,我需要在链表的头插入一个新的内存区。

11718794-4b13-eb11-8da9-e4434bdf6706.png

14718794-4b13-eb11-8da9-e4434bdf6706.png
insert fucntion

这里我的传参就是一个二级指针,至于为什么,请参照chapter 2中指针的指针概念。

现在你应该已经了解了为什么会使用二级指针。

“我为什么要弄那么复杂,二级指针那么复杂,我完全可以用一级指针就够了啊,还增加了代码的易读性。”

主要的原因就是:

  • 节省空间
  • 减少初始化的工作量
  • 可以更好的警醒我们的操作

有时候复杂意味着效率更高,有时候复杂意味着空间更大。

如何平衡他们是一个软件设计异常重要的课题。


我在写这个函数的时候,看起来非常的简单,但是编译过也遇到了两个错误,非常典型,特拿出来作为例子。如果你看到了这里,不妨去找一下是哪里出了错误。

  1. insert fucntion 中可以编译过,但是并不能准确的插入到列表中。

18718794-4b13-eb11-8da9-e4434bdf6706.png

2. debug会报错,Segmentation Fault

Segmentation Fault 是如何被检测到的?​www.zhihu.com

1c718794-4b13-eb11-8da9-e4434bdf6706.png

具体为什么,我会在下一节中指出。


  • 为什么选择链表而不是其他数据结构?

这是我刚学习的时候,最感兴趣的问题。

为什么不用好用的数组,而要选择看起来复杂多了的链表呢。

我们先来分析一下我们储存的堆内存吧。

  • 非连续,非线性
  • 可随时存取
  • 遍历方便
  • 操作方便,比如插入,删除

如果我们使用数组来储存这些数据,那么对应的我们需要一个二维数组,或者直接用结构体的数组,还是成对储存。

那么我们会遇到一些问题:

  1. 需要预设非常大的空间来储存,我们没办法来自动控制数组的大小。

2. 数组不好做删除,插入的操作,代码量将变得非常巨大。

而链表的特点,刚好和我们的需求契合。

自然,使用链表将是我们最好的选择。

  • 可以使用typedef struct 的标识符来代替struct node吗?

typedef 是一个非常好用的标识符。

比如

20718794-4b13-eb11-8da9-e4434bdf6706.png

23718794-4b13-eb11-8da9-e4434bdf6706.png

如果我们正常去声明和定义一个结构体,应该是这样。

如果我们使用typedef将变得非常简洁和明快。

27718794-4b13-eb11-8da9-e4434bdf6706.png

但是我们能不能在链表这里也这样使用呢。

比如改成:

2b718794-4b13-eb11-8da9-e4434bdf6706.png

答案是不行。

在结构体中有一个指向相同结构类型的指针成员时,要求使用结构标记。

也就是说,没有struct node ,而只是node_t ,是没有办法在结构体里继续声明的。

关于联合

联合的作用,一般来说是为了拓展同一块内存空间的利用率。

比如:

31718794-4b13-eb11-8da9-e4434bdf6706.png

如果我需要传输一块8bit图像调色板的数据,同时每次只能传输1个byte,那么如果用上图

这个联合的话,就可以直接存入不做其他的转换,当我想用的时候直接用g_color_table.color_tab就好了.

  1. 节省了多余转换代码的开支。
  2. 节省了缓存区的空间。

那么,是不是还有别的作用呢?

它容易被忽视的在于它常常可以拯救一些难以捉摸的bug。

比如有些情况的突然死机,突然数据不正常,往往你把代码翻个底朝天也难以找到原因。

这里可能的原因之一就是你没有做到系统所要求的对齐水平。

union的特点之一是它将严格按照对它定义的最小单位的变量类型对齐,同时它所占的空间是它定义的最大单元的变量大小。

在malloc函数中,我们一样可以用到它。

34718794-4b13-eb11-8da9-e4434bdf6706.png

我们将上文中定义的内存链表结构改成以上形式。

这样做的目的:

使我们定义的链表结构头永远符合Int类型的对齐,有的机器是4字节对齐,有的机器是2字节对齐,这样在机器进行读写的时候不会出错。

注意:
虽然我们定义了align_x,但是我们将永远不会对它进行赋值,它存在的意义就是对以上的struct进行强制的int对齐。

chapter4 完

后续拾遗会继续添加在这个chapter。

如果你看到了这里,那么我将非常荣幸,希望可以看到你们对于我的提问有一些答案,写在评论区。

爱你们。

2020.8.26 首次发布,新增第一个模块,指针与malloc函数的关系。

2020.9.22 第二次发布,chapter2, 指针的指针,局部变量的生存周期,堆栈空间。

2020.9.28 第三次发布, chapter3, 重写malloc函数,单链表。

2020.10.7, 第四次发布, chapter4 , 拾遗,回答一些为什么。

2020.10.20 拾遗 : 关于在malloc中联合union的使用。

未完。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值