这篇参考文章写得很详细:http://blog.codinglabs.org/articles/a-malloc-tutorial.html
本节主要讲述内存的故事。
学习C++的过程中,一般书上介绍各种对象各种用法各种注意点各种坑。然而,书上这些代码,究竟能够起到什么作用,这种程序是怎样管理内存的呢?看完书还是一知半解。
以几个例子为例:“烫烫烫”和“屯屯屯”是神马一回事?为什么总是出现Access violation?什么是野指针?堆上还是栈上傻傻分不清?内存会不会不够用啊?什么时候要释放这些空间?this指针到底是什么?为毛会有Stack overflow/Segment fault?变量、结构体、对象是内存中的东西吗?内存里面究竟是哪些妖魔鬼怪?…………
围绕着内存的问题有很多很多,上面的问题不可能去一一解答。
整个计算机系统中的各部分协调工作,才是确保电脑不会动不动就蓝屏的关键。同样的,C++乃至整个编程语言世界都是建立在计算机系统的基础上,我们看到的,也只是冰山一角。
因此,平常人看内存:“那不就是个放一堆奇奇怪怪东西的空间么”。那么这些“奇怪的东西”是什么呢?
首先,要了解这些“奇怪的东西”,就必须明白计算机的运作方式?一般是“冯*·*诺依曼”架构的,如果你对它还不熟悉,那么下面的内容理解起来需要费多一些时间。
计算机的运作方式简单起见(表述不太严谨),就是:
- CPU从内存获取指令
- CPU执行这个指令,根据这个指令做相应的事情(如读文件、打印输出等)
- CPU找到下一条指令的位置
- 回到第一步骤
一些问题:CPU读的第一条指令是什么?CPU读错了怎么办?下一条指令在哪里?CPU做什么事情都行吗,能不能和太阳肩并肩(手动滑稽)?这牵扯到操作系统的内容了,这里不展开。
现在,从计算机的运作方式中我们可以看出:
- 内存中应该存放着指令
- 内存中应该也存放着数据
- 内存中可能还有其他我们不知道的东西(咖喱棒吗)
那指令和数据在内存中是怎样存放的呢?会不会是随机打乱着放的呢?当然不会,操作系统其实是个强迫症、整理控,它对指令和数据的存放要求非常严格:
- 指令就该放在专门的存放指令的地方!
- 数据也应该放在专门的放数据的地方!
那么这个要求呢,在操作系统层次上叫作“段”/Segment。
我们的**大叔(操作系统)其实是一个疯狂的整理控,它对内存的打理是非常苛刻的。为什么这么苛刻呢?原因在于内存其实空间是有限的,如果那些小弟(程序)去外面带了一堆的杀马特(申请内存)**回来,那么大叔家就会被挤爆,到时候整个系统就挂了。
所以大叔会时刻关注家里的情况,计算着目前家里有多少小弟,有多少杀马特,有多少吃的喝的。一旦发现不够用了,大叔立马开启一级警报:谁谁谁,把你的杀马特带出去!你你你,不准再把杀马特带进屋里来!我的天哪,你们把家里搞成啥样了,快把奶嘴都放抽屉里!此时大叔变成了凶残的大妈。
上面的例子只是为了形象说明,求轻拍。
刚刚说到内存中有指令有数据,两者分别存放在特定的区域。
那么指令跟数据的关系其实很微妙:
- 如果我恶意修改了数据,那么指令的执行就会受影响
- 如果我恶意修改了指令,那么指令和数据我都可以改掉了
所以典型的思路是:我改了一点数据,结果影响了指令的正常运行,指令出现了意料之外的情况。其中一种情况是,指令会执行到数据的区域上来。这样,我更改了数据区域,相当于更改了指令区域。更改了指令区域的后果就是:可以让系统与大太阳肩并肩,你可以胡作非为了。
那么这里,有一个小小的例子可以说明:http://www.secbox.cn/hacker/program/c/2350.html 。这个例子用VC6.0进行编译,所以程序运行没有报错,若用较新的VS2015编译运行会发现程序运行会报错(它检测到指令被修改)。这个例子就是大名鼎鼎的“缓冲区溢出漏洞”。
如果我直接去修改指令区域,可能会收到一个Access Violation的手动滑稽表情。
前面简单介绍了内存的一系列知识,但内存不是只可远观不可亵玩的,相反地,我们需要自己去管理内存。
“管理”一般就是简单的CURD(增删改查)。
申请内存是门大学问。
前面说了大叔其实是整理控,如果你胡乱地申请内存,大叔便会恼怒,让你的程序越走越慢——这可是你自找的。
偌大一片内存,如何管理?常言道不扫一屋何以扫天下,就以一小块内存作为自留地,自己耕耘吧。
假设现在手头有块地,大小是1KB。
- 如果要提供1MB空间,那么不好意思,地不够,不借
- 如果要提供1KB,正好,给你
- 如果要1B,呵呵,免费赠送(为了取整),给你4B;如果要5B,同样,给8B
- 难道工作就这么简单?拿衣服
现在情况复杂了,有借有还,再借不难。
- 之前借的1KB还你了
- 一半地借人了,一半地还了,现在只剩一半地
- 借借还还越来越乱,最后是有半KB空间,但连100B也借不出去!
你火了:我要请个整理控来!我要请一堆整理控来!
- 整理控1:我每次从开头找,只要找到符合你要求大小的,就把它给你
- 整理控2:我全部找一遍,把符合你要求但是空间最小的地给你
- 整理控3:等等等等,我把这些零碎的地全合并到前面去,再把后面的空地给你,但是首先…
- 整理控4:给你零碎的地我想没问题吧……(被一脚踢飞)
- 整理控5:我全部找一遍,把符合你要求但是空间最大的地给你
- 整理控6:我全部找一遍,把符合你要求但是空间适中的地给你
最后,你非常开心,然后请了整理控1,并拒绝了后面所有人。
内存的归还还不简单,一个字,还!
但其中有玄机:两个空地被归还了,很巧的是这两块地相邻,结果当然是这两块地合并成一块空地了。但是在编程中,并没有此等好事,大叔说:“合并是神马?两个杀马特会自动合并成一个杀马特吗?当然是你来做!”
面对前面一块块零零落落的地,你感慨万千:“用什么方法来记录它们比较好呢?”
于是你买了几百本数学练习簿,打算记账。直到有一天,你惊讶地发现:
它,今天终于的确来还地了。但是,它是第一次向你申请土地的人!它的记录在前面300页!苍天!大地!我找了几宿才找到他!这可恨的!
所以,你嘀咕着:“是记载方式出问题了吗?”突然,你想到了什么。
你买了一本空白的几千页厚的书,按照土地的总量,计算出一页纸代表多少土地。谁借了,就在那部分纸上写上他的名字。但是显而易见,这种方法出了问题:别人少还了很难找出来。
后来,你想了一个办法。谁借了地,在相应的那部分纸的第一张写上他的名字。然后,将这块地相邻的上一块地和下一块地的页码也记在上面,虽然这样的话,这一页纸就没用了,不过好在可以解决问题。
最后,这种方法很久都没出问题。这种方法也是比较普遍的方法,详情请看最前面的参考部分。
每个写程序的人都是管理内存的大师,不过这大师要打引号。什么时候,自己成为了一个整理控,有着对内存要求很苛刻的独特癖好,那么这个时候——你就成了大叔。