第四章:漫游“Bug谷”与“变量海”

一件新工具,一种新恐惧——欢迎来到C语言

旅行者,欢迎回来。深呼吸,稳住你的神经。在上一章1,我们刚刚结束了一场关于 GOTO 语句是否“有害”的高雅哲学辩论1

现在,我得告诉你一个小秘密:那场辩论,在我们即将前往的地方,根本无足重轻。我们要去的地方,比“意大利面条式代码”1的逻辑迷宫要危险一千倍。我们要去的地方,会让 GOTO 语句看起来像是在公园里牵着贵宾犬安全地散步。

我们要去的,是你亲口命名的“Bug谷”1

你的新工具,是C语言2

它的名字只有一个字母,谦逊得近乎傲慢。它在20世纪70年代初的贝尔实验室2诞生,由两位神级人物——Dennis Ritchie和Ken Thompson——共同创造3。他们当时正在逃离一个过于庞大、过于复杂、注定要失败的乌托邦项目,名叫Multics4。他们渴望简单,于是决定创造一个属于他们自己的、小而美的操作系统——UNIX3

他们最初犯了一个你(作为一名来自80年代的专家)非常熟悉的“错误”:他们用汇编语言编写了UNIX4。正如我们在第二章1探讨过的,汇编语言是你和CPU之间的私密情话。它快如闪电,但也“一次性”得令人心碎——为一个CPU写的代码,换到另一个CPU上就全废了。

1973年,他们做出了一个在当时看来惊天动地的决定:用他们刚刚发明的新语言C,重写整个UNIX内核4

为什么?C语言的“杀手锏”究竟是什么?

它提供了凡人梦寐以求的完美平衡:它既是一种结构化的高级语言(让我们彻底告别 GOTO 噩梦),又保留了低级语言的原始威力(你好,硬件!)1。但最、最、最关键的是——它是可移植的(Portable)5

C语言的真正革命不是性能(汇编更快),也不是结构(Dijkstra的理论早就有了),而是可移植性。这是软件第一次可以从特定的硬件中“解放”出来。它不是一种更好的“编写”方式,而是一种更好的“分发”和“扩展”方式。它使得UNIX(以及后来的Linux)成为可能,让一套操作系统几乎可以运行在任何机器上。

直到今天,当你为你的智能烤面包机、汽车的ECU或火星探测器编程时,C语言仍然是无可争议的王者。为什么?因为:

  1. 支持它的编译器无处不在;
  2. 它允许你进行精细的手动内存管理和直接的硬件交互。

但这就是浮士德的交易。

为了获得这种可移植性的力量和对硬件的绝对控制,C语言拿走了你所有的“安全网”。它给了你一把上膛的M1911手枪,解开了保险,然后蒙上了你的眼睛,对你说:“我相信你,程序员。你知道你在做什么,对吧?”

这种“信任”,就是“Bug谷”的入口。

欢迎来到“变量海”:指针与手动挡的F1赛车

你所知的世界观,即将被彻底颠覆。

在你的BASIC世界里,变量是一个“名字”。你写下 A$ = "HELLO"1,解释器会像个贴心的管家,为你找个地方放好"HELLO"这个字符串,然后把 A$ 这个标签贴上去。你从不需要知道它在哪儿。

在C语言的世界里,变量是一个“内存地址”1

欢迎来到“变量海”1,这里波涛汹涌,我们用来航行的工具,名叫“指针”(Pointer)。

什么是指针?它是一种特殊的变量。它的值不是数据本身,而是另一个变量的内存地址1

这是一个你必须立刻掌握的比喻:指针就是一张“藏宝图”6

假设你有一个变量 int x = 100;。那么,x 就是“100块黄金”。

现在你创建了一个指针 int *p = &x;p 本身不是黄金,p 是一张“藏宝图”,&x(“x的地址”)就是地图上的坐标。

如果你想“顺着藏宝图找到黄金”,你必须“解引用”(Dereference)这个指针,即使用星号:*p7*p 的值,才是那100块黄金。

这种能力赋予了你无与伦比的权力1。你可以把这张“藏宝图”传来传去,让程序的不同部分高效地访问同一块数据,而不用把沉重的“黄金”复制得到处都是。

但C语言(这位信任你的“魔鬼”)说:“既然我把藏宝图交给你了,那么‘寻宝’(分配内存)和‘销毁地图’(释放内存)就都是你的责任了。”

于是,你迎来了C语言中最神圣,也最被诅咒的两个命令:malloc()free()1

  • malloc (Memory Allocate):你(程序员)主动向操作系统“申请”一块内存。
  • free:你(程序员)在用完这块内存后,必须,一定,切记,要手动“归还”它。

这个比喻更直白:malloc() 就像你去一家爆满的餐厅领号排队;free() 就像你吃完饭后,举手告诉服务员:“嗨!这桌可以收了!”1

如果你只管领号(malloc),吃完就走,从不告诉服务员你走了(忘记 free),那么这张桌子将永远被标记为“占用”。餐厅(你的计算机系统)很快就会因为没有空桌(没有内存)而崩溃。

为了让你(这位“受信任”的程序员)的管理工作更“轻松”(哈!),C语言的内存被分成了两个截然不同的世界:“栈”(The Stack)和“堆”(The Heap)。

“栈”(The Stack):整齐的盘子

“栈”这个名字极具描述性。它是一个LIFO(Last-In, First-Out,后进先出)结构8

比喻:一摞自助餐盘子8

你只能从顶部放一个新盘子(在编程中称为 push),也只能从顶部拿一个盘子(称为 pop8。你绝对不可能(也不应该)试图从这一摞盘子的中间抽一个出来。

“栈”是你程序的“便签本”或“草稿空间”9。它的工作是全自动的。当你调用一个函数时,操作系统会自动在“栈”的顶部给你分配一个“楼层”10。这个函数所有的局部变量(local variables)都会被 push 到这个楼层上。

当函数执行完毕并返回时,整个“楼层”10(连同它上面的所有局部变量)会被瞬间 pop 掉并销毁。

优点: 极快,全自动管理。你永远不需要(也不能)在“栈”上使用 free()

“堆”(The Heap):杂乱的衣服

“堆”这个名字同样极具描述性,而且在英语中,它与“栈”的对比更加强烈。

“栈”(Stack)是一摞整齐的东西。而“堆”(Heap)……它更像是“你堆(a heap of)在衣柜地板上那堆脏衣服”11。它是一片广阔、混乱、无组织的内存区域8

用途: 当你需要一块“持久”的内存时——比如,你希望某个数据在你当前的函数返回之后,它仍然存在——你就必须在“堆”上手动申请。

流程与比喻:
你调用 malloc()(“服务员,我需要一张能坐10个人的桌子!”)。

内存分配器(Allocator)就像那个忙得焦头烂额的服务员,它必须在“堆”这片混乱的餐厅大堂里8寻找一块足够大的、尚未被占用的空地8

找到后,它会标记这块地为“正在使用”,然后返回给你一个指针(那张写着桌子位置的“藏宝图”)8

缺点: 速度慢(因为服务员需要“寻找”空地)8,而且最重要的是:它是纯手动的。服务员(系统)绝对不会知道你什么时候吃完。你必须在用完后,明确调用 free() 来归还这块内存。

现在你看清了。

“栈”是安全的“新手村”,由系统自动管理。“堆”(即“变量海”)是你必须去征服的“狂野西部”,充满了需要你手动管理的“藏宝图”。

而“Bug谷”1里的几乎所有怪物,都生活在这片狂野的“堆”上。

“Bug谷”探源:谁放出了这些怪物?

在我们进入“Bug谷”的动物园,开始我们的狩猎之前,我们必须先搞清楚:我们到底在猎杀什么?“Bug”这个词,究竟是哪儿来的?

你可能听过那个最著名的故事:格雷斯·霍珀(Grace Hopper)与飞蛾。

神话:格雷斯·霍珀(1947)

1947年9月9日,哈佛大学Mark II计算机的工程师们又一次遇到了故障12。在那个继电器和真空管的时代,这意味着你得真的“打开引擎盖”检查。他们打开了一个继电器面板,发现了一只“货真价实”的飞蛾,它的尸体卡在了触点之间,导致了短路12

一位工程师(不是Hopper本人,尽管她让这个故事名垂青史13)小心翼翼地把这只可怜的飞蛾用透明胶带粘在了团队的日志本上(这本传奇的日志本,连同那只蛾子,至今仍保存在史密森尼国家美国历史博物馆13)。

在这只飞蛾旁边,工程师写下了一句不朽的话:“First actual case of bug being found.”(发现的第一个“真正”的bug案例。)12

真相:托马斯·爱迪生(1878)

这是一个伟大的故事,但它(很遗憾)是“Bug”一词的错误起源故事。

请注意日志本上那个关键的形容词:“真正的”(actual)13。工程师之所以这么写,恰恰是因为他们已经在口头上使用“bug”(虫子)这个词来形容那些看不见摸不着的技术故障了13。他们只是在惊叹,这一次,居然是字面意义上的虫子。

这个词的起源,至少要再往前追溯70年。

1878年,托马斯·爱迪生(Thomas Edison)在一封信中抱怨道:“‘Bugs’——正如这些小故障和困难所称呼的那样……”13

1889年的《帕尔马尔公报》(Pall Mall Gazette)也报道了爱迪生“过去两个晚上都在忙着发现他留声机里的一个‘bug’”13

异端:迪杰斯特拉(Dijkstra)的愤怒

我们的老朋友,在第三章1中与GOTO激烈作战的Edsger Dijkstra,极其憎恶“Bug”这个词14

他认为这个词不仅仅是词不达意,它简直是“知识上不诚实的”(intellectually dishonest)14

在他的著名论文EWD1036中15,他愤怒地写道:

我们可以,举个例子,从清理我们的语言开始,不再称一个bug为‘bug’,而是称它为‘error’(错误)... 这更诚实,因为它直接将责任归咎于它所属的人,即犯下错误的程序员。14

他认为,“‘bug’这个拟人化的比喻,暗示着当程序员没在看的时候,某个恶意的虫子潜了进来。这是知识上不诚实的,因为它掩盖了错误是程序员自己的创造这一事实。”14

Dijkstra的这番抱怨,完美地概括了从BASIC到C的哲学转变。

在你的BASIC世界里,很多问题(比如内存管理)被解释器隐藏了,所以当问题发生时,它感觉像是“系统”的错(一个“bug”潜入了)。

但在C语言中,一切都是裸露的,一切都是手动的16。正如Dijkstra所说14,C语言中的任何“bug”都不是“潜入”的;它是你(程序员)亲手(通过错误的指针或free)制造的“Error”。

C语言编程,就是一场关于“知识上的诚实”的残酷试炼14。你不再是“用户”;你是“创造者”,你必须为你所有的“创造”负全责。

现在,让我们见见你亲手创造的怪物们。


“Bug谷”怪物手册(速查表)

怪物名称 (外号) 技术定义 通俗比喻
段错误 (Segfault) (“暴君”) 试图访问你无权访问的内存区域17 操作系统给了你一记响亮的耳光。
空指针 (Null Pointer) (“十亿美元的错误”18) 一个指向“无”或地址0的指针19 一张指向“世界尽头”的空白藏宝图。
悬空指针 (Dangling Pointer) (“房间里的幽灵”) 一个指向已被free()(释放)的内存的指针20 你卖掉房子后还偷偷留着的旧钥匙。
缓冲区溢出 (Buffer Overflow) (“大坝决堤”) 往一个容器里塞了比它容量更多的东西,导致数据“溢出”并覆盖了邻居16 往8盎司的杯子里倒10盎司的水。
内存泄漏 (Memory Leak) (“温水煮青蛙”21) 你申请了内存(malloc),但弄丢了地址,导致无法归还(free22 你买了房子,但把房产证和地址都忘了。

“Bug谷”怪物图鉴(上):突然死亡与物理灾难

欢迎来到动物园。这里的怪物都是你亲手创造的。我们将从那些最“吵闹”、最“血腥”的开始。

怪物一:段错误(Segmentation Fault)——“我听到了噪音,并选择了暴力”

这是你在C语言中最常见的死亡方式。当你看到那行冰冷的文字:

Segmentation fault (core dumped)

你(的程序)已经死了。

重要的是要理解:这不是C语言的“错误”。这是硬件(你CPU里的内存保护单元)17发现你的程序在“耍流氓”,它触发了警报,并通知操作系统(OS)来“清理门户”。

触发机制: 你的程序试图访问一个“不属于你”的内存地址19。也许是你试图闯入操作系统的“圣地”(比如地址0),也许是你试图闯入另一个进程的私有领地。

后果: 操作系统是社区的保安。它会立即向你的进程发送一个“死亡信号”(SIGSEGV),导致你的程序异常终止17。它不能容忍你这种试图破坏社区安全(篡改其他程序内存)的行为。

幽默的比喻:
一个Segfault是你的计算机的“数字版存在主义危机”23。它是“计算机在说‘我听到了噪音,并选择了暴力’”23

黑客帝国版比喻24
你(程序员)就像《黑客帝国》里的尼奥,自信地宣称“我知道C++”。然后你运行程序,屏幕上打出 Segmentation fault (core dumped)

这就像墨菲斯在道场里把你一拳打倒在地板上,告诉你:“你还不是‘天选之子’(The One),你只是又一个忘了检查指针的菜鸟。”

xkcd版比喻25
那个著名的xkcd漫画25把Segfault比作“睡眠惊跳”(Hypnic Jerk)——就是那种你刚要睡着时,突然感觉从高处坠落而猛地抽搐一下。

漫画里,电脑对程序员抱怨说:“当你的程序segfault时,我就有那种感觉。拜托,双重检查你该死的指针。”

怪物二:空指针(Null Pointer)——“十亿美元的错误”

这个怪物是导致“段错误”的最常见元凶。

它是什么? NULL是一个特殊的指针,它被标准定义为“不指向任何东西”。在实践中,它通常(但不总是)就是地址 0x00000000。它是一张“空白的藏宝图”19

问题在哪? 拥有一张空白的藏宝图(即 int *p = NULL;)是完全合法的。

犯罪发生在“解引用”(Dereferencing)的那一刻。当你试图“顺着那张空白的藏宝图去找黄金”(即 *p = 1;)时19,你实际上是在试图访问内存地址0。

而地址0,几乎在所有现代操作系统上,都是“神圣不可侵犯的领土”。你无权访问19

结果? 砰!保安(OS)开枪了。Segmentation fault。

托尼·霍尔(Tony Hoare)爵士的忏悔:
这个“方便”的发明,是计算机科学家托尼·霍尔爵士在1965年为ALGOL W语言设计的18

2009年,他在一次演讲中,公开称其为“我十亿美元的错误”(my billion-dollar mistake)18

为什么(这个洞察令人发指)?他坦白,他当年加入NULL的唯一原因,不是因为它有多好,而是……

仅仅因为它太容易实现了(simply because it was so easy to implement)18

就是这样。一个图省事的“小发明”,导致了“无数的错误、漏洞和系统崩溃”18,在之后的40年里造成了(据他估计)数十亿美元的损失。这是C语言世界观的第一个警示:程序员的便利(或者说懒惰),往往是系统灾难的根源。

怪物三:悬空指针(Dangling Pointer)——“你房间里的幽灵”

如果你觉得NULL指针很糟糕,那么欢迎来到真正的噩梦。

这个怪物比NULL指针阴险一万倍。

NULL指针是“一张空白的地图”。你知道它哪儿也不指。

而“悬空指针”是“一张过时的地图”。

比喻:你卖掉房子后还留着的旧钥匙19

  1. 你调用 malloc() 在“堆”上建了一座房子,并拿到了钥匙(指针 p)。
  2. 你用完了房子,于是调用 free(p)。你“卖掉”了房子,归还了土地。
  3. 但是: 你忘了销毁你的钥匙副本(指针 p 仍然指向那个地址)。
  4. 现在,p 就是一个“悬空指针”26

为什么它很恐怖? 当你(在程序的其他地方,也许是很久以后)再次使用这个“悬空钥匙”时,会发生什么?

  • 情况A(你很幸运): 那块地还没被别人用。你用钥匙开门,走进一片废墟(垃圾数据)。程序可能会立即崩溃(Segfault)。谢天谢地。
  • 情况B(你极其不幸): 操作系统动作很快。它已经把那块地重新分配给了别人(比如,你的密码管理器刚刚申请了内存,用来临时存放你的主密码)。

你现在用你的“旧钥匙”闯入了“新房客”的家。你(的程序)以为你还在自己的房子里,你开始往“你”的变量里写数据,实际上,你正在篡改你的密码管理器的内存。你无意中读取或覆盖了完全无关的、高度敏感的数据。

为什么它极度危险(安全漏洞):
这不仅仅是“代码质量问题”,它和缓冲区溢出一样危险20

攻击者可以利用这一点。他们可以故意触发你的程序去 free() 某个对象,然后,攻击者立即申请一大块新内存(malloc),把他们自己的恶意Shellcode(攻击代码)像喷漆一样“喷”进你刚刚释放的那块地20

当你的程序“无辜地”使用那个“悬空指针”时,它不会崩溃。它会以为自己还在访问旧数据,但实际上,它会一头扎进攻击者的陷阱,执行攻击者的代码。

这就是“任意代码执行”(Arbitrary Code Execution)20

小结: Segfault和NULL是“物理攻击”,它们很吵闹,会当场爆炸。悬空指针是“幽灵攻击”,它在你背后捅刀子,你甚至都不知道自己是怎么死的。

“Bug谷”怪物图鉴(下):缓慢窒息与化学灾难

有些怪物不会立刻杀死你。它们会慢慢地折磨你,直到你的系统窒息而死。

怪物四:缓冲区溢出(Buffer Overflow)——“杯子满了,水(数据)洒在了电线上”

在C语言中,缓冲区(Buffer,通常是一个数组)只是内存中连续的一块地。它们最美妙(也最可怕)的特性是:它们没有内置的“边界检查”16

比喻:8盎司的杯子16
你声明了一个8字节的缓冲区(char name[8];),这就像你从柜子里拿了一个标着“8盎司”的杯子。

然后,你(天真地)允许用户输入他们的名字。

用户恶意地输入了10字节,比如“Maximilian”16

溢出: C语言不会阻止你!它会说:“好的,老板!” 然后把10字节的数据全塞进去。

前8字节("Maximili")填满了杯子(name缓冲区)。

多出来的2字节("an")就“溢出”16,覆盖了内存中紧邻name变量的任何东西27

灾难: 如果被覆盖的“邻居”恰好是“栈”上的函数返回地址28,会发生什么?
(“返回地址”是CPU存放的“藏宝图”,告诉它这个函数执行完后应该跳回哪里去继续执行)。

攻击者就可以通过这个“溢出”,把那个合法的“返回地址”改成他们自己恶意代码的地址28

当你的函数“正常”返回时,CPU会查看那个(已被篡改的)“返回地址”,然后“礼貌地”一头“跳”进攻击者的陷阱里,开始执行恶意代码。

“老大哥”级的黑客攻击:莫里斯蠕虫(Morris Worm, 1988)
这是“Bug谷”的第一个超级巨星。在1988年,一个名叫Robert Tappan Morris的康奈尔大学研究生29,利用了UNIX中一个叫做 finger 的网络服务中的缓冲区溢出漏洞29

他通过发送“畸形”数据包(malformed packets)29来溢出目标服务器上的缓冲区,注入他的蠕虫代码,并使其在受害机器上执行29

影响: 在那个互联网还处于田园牧歌时代的1988年,这只蠕虫感染了大约6000台机器——这在当时占了整个互联网的10%29。互联网第一次(但绝不是最后一次)瘫痪了。

技术宅的吹牛资本:为什么“心脏出血”(Heartbleed)不是一个“经典”的溢出
你可能听说过2014年的“心脏出血”(Heartbleed)漏洞30,它重创了全球的服务器。很多人误以为它也是缓冲区溢出。

不完全是。如果你想在技术聚会上显得很懂,你可以这样说:
Heartbleed 不是一个经典的“缓冲区写溢出”31。它是一个“缓冲区读溢出”(更准确地说是“信息泄露”)31

  • 经典溢出16(如莫里斯蠕虫): 攻击者写入的数据太多,溢出并覆盖了内存。
  • 心脏出血31: 攻击者请求的数据太多。
    • 攻击流程: 攻击者向服务器发送一个1字节的“心跳”请求包,但他谎报这个包的长度为64KB。
    • 漏洞: 有漏洞的服务器(OpenSSL)没有检查“1字节”和“64KB”是否匹配31。它天真地读取了那1字节的请求,以及内存中跟在那1字节后面的64KB数据(这可能是其他用户的密码、私钥、银行账户信息……)32
    • 结果: 服务器把这64KB的“内存泄漏”32全部“礼貌地”发回给了攻击者。它不是“写”溢出,而是“读”溢出。这是一个更诡异、更隐蔽的怪物。

怪物五:内存泄漏(Memory Leak)——“温水煮青蛙”

这是最阴险、最缓慢的怪物。它不会让你的程序崩溃(至少一开始不会)。它只会让你的程序变慢...变慢...然后窒息而死。

它是什么? 当你使用 malloc() 从“堆”33申请了内存,但再也没有调用 free() 来归还它33

关键: 你不仅没有归还,你还把指向它的唯一的指针(那张藏宝图)给弄丢了(比如,覆盖了那个指针变量,或者变量超出了作用域)22

后果: 这块内存现在成了“孤儿”。你的程序无法再访问它,也无法再释放它。它在程序的整个生命周期中都会被“占用”,但“毫无用处”33

比喻一:温水煮青蛙(The Boiling Frog)
这是对内存泄漏最完美的比喻21

  • Segfault(段错误): 就像把一只青蛙扔进滚烫的水里。它会立刻察觉到危险并跳出来(程序崩溃并重启)34
  • Memory Leak(内存泄漏): 就像把青蛙放进常温的水里,然后慢慢地、逐渐地加热21

过程: 你的程序启动了(水是凉的)。它在一个循环中泄漏了1KB内存(水温升高了0.1度)。它继续运行,又泄漏了1KB(又升高了0.1度)。这个变化是如此渐进(gradual)21,以至于你(和青蛙)根本没有察觉到危险。你只是觉得程序“好像变慢了一点”。

结局: 几小时或几天后,所有可用内存都被耗尽(水终于开了)。操作系统最终介入,杀死了你的进程(Out-of-Memory, OOM)22。青蛙被煮熟了34

比喻二:健忘的房产大亨22
22 提供了一个绝妙的类比:你的程序就像一个房产大亨。

  • malloc() = 你买了一栋房子。
  • 指针 = 房子的地址和钥匙。
  • free() = 你卖掉了房子。
  • 内存泄漏 = “你买了一栋房子,然后把地址和钥匙都给忘了”22

你无法居住(访问),也无法出售(释放)。你只能继续买(malloc),继续忘...直到你买下了整个城市(耗尽内存),然后市政府(OS)就会因为你占用了所有资源而“杀死你”(OOM Killer)22

真实世界的灾难:Cloudflare事件 (2017)
内存泄漏不仅仅是让服务器变慢。它们可能导致灾难性的数据泄露。

2017年,Cloudflare32发现他们的一个HTML解析器中存在一个bug(类似于缓冲区溢出)。这个bug导致他们的边缘服务器在处理某些HTTP请求时,会泄露内存中的其他数据。

泄露了什么? “HTTP cookies、身份验证令牌、HTTP POST正文和其他敏感数据”32。这些“泄露”的数据(来自其他完全无关的用户)被返回给了随机的访问者,甚至被搜索引擎缓存了32

这证明了在“Bug谷”中,内存管理不善33不仅会导致你的程序死亡,还可能导致你的客户数据“社会性死亡”。

结论:“Bug谷”的伟大遗产——我们为何必须创造“清洁工”和“独裁者”

“旅行者,你还活着吗?恭喜你。你已经穿越了‘Bug谷’1。这很可怕,对吧?”

C语言给了我们力量,但代价是让我们生活在对这些怪物的持续恐惧中。我们成了“全能”的程序员,但也成了(正如Dijkstra指出的14)要为所有“错误”负全责的人。

正是这种可怕的、令人难以承受的责任,催生了C语言之后的整个编程语言史1

我们之后的每一次伟大创新,几乎都是对“Bug谷”恐怖现状的直接回应。它们的核心卖点之一,就是自动化内存管理,从而将那个危险的 free 命令从我们这些不完美的人类手中夺走1

为了逃离“Bug谷”,人类开辟了三条主要的道路。


逃离“Bug谷”的三种路径

解决方案 (语言) 核心哲学 幽默比喻
Java 运行时自动化(垃圾回收)35 雇一个“自助餐厅清洁工”36
Python 包装与抽象(GC + C绑定)37 “穿上C语言的燕尾服”37
Rust 编译时验证(所有权)38 雇一个“严厉的编译时独裁者”23

解决方案 A (Java):雇佣一个“垃圾回收器”(GC)

哲学: Java的设计者们看着C++(C的超集)1说:“够了!人类不应该被信任去处理free()。”

机制: 他们彻底移除了手动内存释放。取而代之的是一个内置的、自动的“垃圾回收器”(Garbage Collection, GC)35

比喻:自助餐厅的清洁工36
在Java中,你只管使用 new 关键字(就像在自助餐厅疯狂拿新盘子36)。

GC就像一个“餐厅清洁工”36,在后台默默巡视。它使用一种“标记-清除”(Mark-and-Sweep)算法35:它会检查哪些“盘子”(对象)你还在用(即还有指针/引用指向它们),并“标记”它们。

对于那些你已经“吃完”(不再有任何引用)的盘子,GC会自动“清扫”(Sweep)它们,回收内存36

Java的“新”泄漏:
但这不完美。Java仍然可能发生内存泄漏39。为什么?

因为清洁工很懂礼貌,他不会把你还端在手里的盘子收走36。如果你(程序员)出于某种原因,把一个早已不再需要的对象(比如一个装满数据的超大列表)的引用保存在一个全局变量中,GC会认为它“仍在使用”36,因此永远不会回收它。这就是“逻辑上的”内存泄漏39

解决方案 B (Python):成为“C语言的伪装者”(新的BASIC)

哲学: Python说:“编程应该像BASIC一样简单1,但运行起来应该像C一样快40。”

机制: 它做到了。Python本身(像Java一样)有垃圾回收。但是,对于所有性能密集型任务(如图形、AI、数据科学),它都“作弊”了。

旅程的圆环:
还记得吗,旅行者?在第二章1,你的BASIC程序使用PEEKPOKECALL来运行汇编代码以获得速度。

看看今天的Python。当一个数据科学家需要处理十亿行数据时,他会写:import numpy1

Python是“C语言的伪装”(C in disguise)37

你猜 numpy 是用什么写的?它的核心是高度优化的C++和C代码1

Python的模式,与你所熟知的“BASIC外壳 + 汇编内核”1在精神上完全相同。Python通过“包装”C代码,为你隐藏了整个“Bug谷”,同时又利用了C的全部性能37

解决方案 C (Rust):创造一个“编译时独裁者”

哲学: Rust说:“GC(Java)太慢,C太危险。我两个都不要。我要求零成本的绝对安全。我要在编译时就解决所有问题。”

机制: Rust引入了一个革命性的、极其严厉的系统,称为“所有权”(Ownership)和“借用检查器”(Borrow Checker)38

它如何工作(简而言之):

  1. 防止悬空指针38: 编译器在编译时就跟踪每一个引用的“生命周期”(Lifetime)。如果你试图返回一个指向局部变量的引用(在C中是灾难性的38),Rust编译器会拒绝编译你的程序38。它在“Bug”诞生之前就杀死了它。
  2. 防止数据竞争38: 如果你对某块数据有一个引用(指针),Rust会“冻结”(freeze)那块数据,禁止你修改它38。这从根本上杜绝了“释放后使用”(use-after-free)的可能。

比喻:浮士德的交易(新版)23
Rust的编译器(“借用检查器”)是出了名的“严厉”、“反人类”。
23的比喻非常贴切:你(程序员)与Rust的编译器达成了“浮士德式的交易”。

  • 你付出的: “你把你的精神健康交给了借用检查器之神”23。编译器会“对你大喊大叫八个小时,直到你开始质疑自己的职业选择”23
  • 你得到的: “你的代码永远不会在凌晨2点的生产环境中Segfault”23

你用“预付的治疗费”(编译时的痛苦)取代了“事后的应急响应费”(运行时的灾难)23


本章结语:

旅行者,我们必须穿越这片“Bug谷”,因为它是我们这个行业的“成年礼”。

在BASIC的世界里,我们是“用户”。

在C的世界里,我们被迫成为“神”——全知全能,也(正如Dijkstra指出的14)要为所有的“错误”负全责。

正是这种可怕的责任,催生了我们今天拥有的一切自动化和安全工具。现在,你理解了这些怪物的长相。你明白了为什么现代语言如此“臃肿”——它们都是建立在C语言留下的“伤疤”之上的。

在下一章,我们将讨论我们为了管理这些用C语言建造的、日益庞大的“代码山”1而发明的第一个工业化流程:欢迎来到面向对象(OOP)的“对象”工厂。


引用的著作


  1. 程序员复活手册:从BASIC到AI(大纲) ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Chistory - Nokia, 访问时间为 十月 27, 2025, https://www.nokia.com/bell-labs/about/dennis-m-ritchie/chist.html ↩︎ ↩︎

  3. Unix - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Unix ↩︎ ↩︎

  4. Kenneth Thompson & Dennis Ritchie Develop UNIX, Making Open Systems Possible, 访问时间为 十月 27, 2025, https://www.historyofinformation.com/detail.php?id=872 ↩︎ ↩︎ ↩︎

  5. Why is C (or other similar programming languages) called portable if we need to code compilers for all platforms? : r/learnprogramming - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/learnprogramming/comments/11m3wtu/why_is_c_or_other_similar_programming_languages/ ↩︎

  6. Pointers in C Programming - Lay Man's Analogy - DEV Community, 访问时间为 十月 27, 2025, https://dev.to/ogagacodes/c-pointers-lay-mans-analogy-1nf9 ↩︎

  7. Pointers (&memes explained) - Malware Guy, 访问时间为 十月 27, 2025, https://www.malwareguy.tech/RE-Notes/pointers.html ↩︎

  8. A nice explanation of memory stack vs. heap - Offtopic - Julia Programming Language, 访问时间为 十月 27, 2025, https://discourse.julialang.org/t/a-nice-explanation-of-memory-stack-vs-heap/53915 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  9. What and where are the stack and heap?, 访问时间为 十月 27, 2025, https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap ↩︎

  10. Help me understand "stack" and "heap" concept : r/C_Programming - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/C_Programming/comments/1kefmgn/help_me_understand_stack_and_heap_concept/ ↩︎ ↩︎

  11. The linguistics of "stack" and "heap" : r/AskProgramming - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/AskProgramming/comments/135k694/the_linguistics_of_stack_and_heap/ ↩︎

  12. September 9: First Instance of Actual Computer Bug Being Found | This Day in History, 访问时间为 十月 27, 2025, https://www.computerhistory.org/tdih/september/9/ ↩︎ ↩︎ ↩︎

  13. The Bug in the Computer Bug Story - JSTOR Daily, 访问时间为 十月 27, 2025, https://daily.jstor.org/the-bug-in-the-computer-bug-story/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  14. Bugs – Clayton Cafiero, 访问时间为 十月 27, 2025, https://www.uvm.edu/~cbcafier/cs1210/book/09_structure,_development,_and_testing/bugs.html ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  15. I like the term "defect" it's more accurate than "bug." | Hacker News, 访问时间为 十月 27, 2025, https://news.ycombinator.com/item?id=36948220 ↩︎

  16. Buffer overflow - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Buffer_overflow ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  17. Segmentation fault - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Segmentation_fault ↩︎ ↩︎ ↩︎

  18. Tony Hoare - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Tony_Hoare ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  19. c++ - What is a segmentation fault? - Stack Overflow, 访问时间为 十月 27, 2025, https://stackoverflow.com/questions/2346806/what-is-a-segmentation-fault ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  20. DANGLING POINTER - Black Hat, 访问时间为 十月 27, 2025, https://www.blackhat.com/presentations/bh-usa-07/Afek/Whitepaper/bh-usa-07-afek-WP.pdf ↩︎ ↩︎ ↩︎ ↩︎

  21. Boiling Frogs - about lowering your standards to mediocrity - No Kill Switch, 访问时间为 十月 27, 2025, https://no-kill-switch.ghost.io/boiling-frogs-about-lowering-your-standards-to-mediocrity/ ↩︎ ↩︎ ↩︎ ↩︎

  22. ELI5: What is a memory leak, how are they caused, and how are they usually fixed? - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/eli5_programming/comments/14ggxwr/eli5_what_is_a_memory_leak_how_are_they_caused/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  23. segfault Memes - ProgrammerHumor.io, 访问时间为 十月 27, 2025, https://programmerhumor.io/memes/segfault ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  24. I Am The One (Until Segmentation Fault) - ProgrammerHumor.io, 访问时间为 十月 27, 2025, https://programmerhumor.io/cpp-memes/i-am-the-one-until-segmentation-fault-fl16 ↩︎

  25. 371: Compiler Complaint - explain xkcd, 访问时间为 十月 27, 2025, https://www.explainxkcd.com/wiki/index.php/371:_Compiler_Complaint ↩︎ ↩︎

  26. danglingPointer - ProgrammerHumor.io, 访问时间为 十月 27, 2025, https://programmerhumor.io/programming-memes/danglingpointer/ ↩︎

  27. What is a Buffer Overflow | Attack Types and Prevention Methods - Imperva, 访问时间为 十月 27, 2025, https://www.imperva.com/learn/application-security/buffer-overflow/ ↩︎

  28. Stack buffer overflow - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Stack_buffer_overflow ↩︎ ↩︎

  29. What is a Buffer Overflow? - Portnox, 访问时间为 十月 27, 2025, https://www.portnox.com/cybersecurity-101/what-is-a-buffer-overflow/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  30. Buffer Overflow Attacks: Detection, Prevention & Mitigation | Black Duck Blog, 访问时间为 十月 27, 2025, https://www.blackduck.com/blog/detect-prevent-and-mitigate-buffer-overflow-attacks.html ↩︎

  31. I've always wondered - what are the most (in)famous buffer overflow ..., 访问时间为 十月 27, 2025, https://news.ycombinator.com/item?id=12742521 ↩︎ ↩︎ ↩︎ ↩︎

  32. Incident report on memory leak caused by Cloudflare parser bug, 访问时间为 十月 27, 2025, https://blog.cloudflare.com/incident-report-on-memory-leak-caused-by-cloudflare-parser-bug/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  33. Memory leak - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Memory_leak ↩︎ ↩︎ ↩︎ ↩︎

  34. Boiling frog - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Boiling_frog ↩︎ ↩︎

  35. Java garbage collection: What is it and how does it work? - New Relic, 访问时间为 十月 27, 2025, https://newrelic.com/blog/best-practices/java-garbage-collection ↩︎ ↩︎ ↩︎

  36. Understanding Memory Leaks in Java and the Magic of Garbage ..., 访问时间为 十月 27, 2025, https://medium.com/@pkgmalinda/understanding-memory-leaks-in-java-and-the-magic-of-garbage-collection-%EF%B8%8F-af6b7d412ef6 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  37. There's a joke that is roughly "Python is not the best language for ..., 访问时间为 十月 27, 2025, https://news.ycombinator.com/item?id=37313403 ↩︎ ↩︎ ↩︎ ↩︎

  38. Ownership - The Rustonomicon, 访问时间为 十月 27, 2025, https://doc.rust-lang.org/nomicon/ownership.html ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  39. The Best Memory Leak Definition [closed] - Stack Overflow, 访问时间为 十月 27, 2025, https://stackoverflow.com/questions/312069/the-best-memory-leak-definition ↩︎ ↩︎

  40. C vs Python devs : r/ProgrammerHumor - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/ProgrammerHumor/comments/w00mxs/c_vs_python_devs/ ↩︎