书 · 1978 · 布莱恩·克尼汉、丹尼斯·里奇

《C程序设计语言》

计算机与 AI书单第 14 名

一句话结论

通过一组逐步展开的小程序,理解怎样用类型、函数、指针和结构体组织计算,同时承担管理内存与处理机器差异的责任。

值不值得读完整本?

值得读完整本正文,尤其适合已经接触过一种编程语言、希望理解程序如何操作数据和存储的读者。1978 年首版也有历史价值,但今天实际编程时,需要另查现行语言规范与库文档。

  • 先读第 1 章,亲手运行并修改字符统计、温度换算和行处理程序,建立从输入到输出的完整认识。
  • 第 2、3 章连着读,重点理解类型转换、运算符、条件与循环;熟悉编程的人可以快读基础语法,但不要跳过求值顺序和边界条件。
  • 第 4—6 章值得最细读:分别解决程序怎样拆分、存储怎样访问、复杂数据怎样组织。指针章需要画内存示意并配合练习。
  • 第 7 章掌握库函数、文件访问和错误处理;第 8 章读懂库与 UNIX 系统接口的关系,目录结构等历史实现细节可以暂时跳过。
  • 附录中的语言参考手册适合随用随查。注意区分 1978 年首版与 1988 年第二版,后者面向当时正在形成的 ANSI C 规范。

核心观点

1. 从可运行的小程序进入语言

学习 C 应尽早开始读、写、运行和修改完整程序,再逐步补齐规则。一个小例子也可以同时训练算法、表达方式和程序组织。

为什么:孤立的语法规则不会告诉读者,数据怎样进入程序、经过哪些步骤、最后如何产生结果。完整程序把这些关系放在一起,使每条语句的作用可以观察。修改一个条件、一个类型或一段循环,又能让读者把自己的预期与运行结果比较。书中因此先给出入门性的整体认识,后面再展开各项机制。

2. 表达式与控制流程必须能够推演

程序的结果取决于明确的类型规则、运算规则和控制流程,不能只按数学直觉或代码外观理解。简洁表达应建立在这些规则已经清楚的基础上。

为什么:同样的除法形式,使用整数或浮点数可能得到不同结果,赋值给浮点变量也不一定能挽回先前丢失的小数。运算符优先级决定表达式如何组合,却不等于规定所有操作的执行先后。条件和循环则决定哪些操作发生、发生多少次以及何时停止。把这些规则逐一推演,才能发现类型转换、循环边界和副作用造成的问题。

3. 用函数和作用域控制复杂度

把任务拆成职责清楚的函数,并控制数据的可见范围,可以让程序更容易理解和修改。函数参数、返回值与共享状态决定各部分怎样合作。

为什么:函数把一段操作封装成有名字的步骤,调用者因而可以先理解用途,再追查实现。局部变量和受限的可见范围减少了其他代码意外干扰数据的机会。按值传参让形参具有独立的值,函数修改形参通常不会修改调用者的变量。需要共享状态时,再明确安排外部变量及其可见范围,就能把依赖关系摆到明处。

4. 指针连接数据访问与存储组织

指针使程序能够通过地址访问对象,并高效组织数组、字符串和间接引用;数组与指针密切相关,但不是同一种东西。程序员必须理解地址指向哪里,以及对应存储是否仍然有效。

为什么:把地址传给函数后,函数就能访问那个地址所指的对象,而不只是在自己的形参副本上操作。数组在多数表达式中会转换为指向首元素的指针,因此下标访问与指针运算可以描述同一种元素访问。指针运算按所指对象的类型移动,使程序可以逐项遍历数据。与此同时,指针本身通常不携带可供自动检查的数组长度,错误的边界或存储寿命判断就可能破坏数据。

5. 用结构体与链接关系表达复杂数据

结构体把属于同一个事物的数据放在一起,指针进一步把这些记录连接成树或链。算法应围绕这种数据组织方式展开。

为什么:相关字段若分散在多组变量或数组中,程序就必须额外维护它们之间的对应关系。结构体把这种关系写进数据表示,访问和传递时更容易保持一致。结构体中的指针可以连接同类对象,使数据组织不必局限于一个预先排好的连续数组。对于树这样的递归结构,递归函数又能按照相同的层次处理各个子结构。

6. 语言、库和系统接口分层合作

C 的语言核心不直接包办输入输出等环境功能,而由库提供常用接口,再由系统接口完成具体操作。理解这些层次,才能判断程序依赖了什么、能移植到哪里。

为什么:把环境相关操作放到函数接口后面,普通程序就可以通过相对稳定的调用完成读写,不必处处处理设备细节。库还可以集中管理缓冲等工作,减少频繁调用底层接口的开销。继续向下查看系统调用,则能理解库的行为、限制和错误来自哪里。不同系统可以实现相似的库接口,但直接使用某个系统的专有机制,会增加移植时需要处理的差异。

反对意见

边界安全不应主要依赖程序员自觉(C. A. R. 霍尔)

霍尔在《The Emperor's Old Clothes》中回顾 ALGOL 实现时,强调数组下标应在运行时检查,并指出正式运行中的下标错误同样会造成严重后果。这对本书所展示的、主要由程序员维护指针与数组边界的方式构成具体挑战:省去自动检查,也会把发现错误的负担转移给开发者。这是语言设计立场的对照,并非针对本书的书评。

理解 C 不等于直接理解现代硬件性能(戴维·奇斯纳尔)

奇斯纳尔在《C Is Not a Low-level Language》中指出,C 所呈现的顺序执行与内存访问模型,与现代处理器的缓存、并行和推测执行存在距离。要让 C 程序跑得快,硬件和编译器需要做大量隐藏的工作。因此,不能把书中便于理解的机器模型直接当成现代计算机的完整性能模型;这限制的是对本书的延伸解读。

许多应用更适合从组合现成组件开始(约翰·奥斯特豪特)

奥斯特豪特在《Scripting: Higher-Level Programming for the 21st Century》中区分构造底层组件与连接现成组件两类任务。他认为,对后一类工作,脚本语言能够减少代码和开发时间。这提醒读者:本书训练的、从基本类型和数据结构逐层构造程序的能力很有价值,但不必成为每个应用项目的起点。他主张两类语言互补,并未全盘否定 C。

相关推荐

导读由 AI 根据公开资料整理,没有读取原书全文,一切以原著为准。导读 Primer 首页