读书札记之 —— 《程序设计实践》

作者布拉恩.科尼汉(Brian W.Kernighan)是一座高山,难得一见的大师级的人物。C 语言圣经K&R中的 K 指的是他,awk 语言中的 K 也是他。它写过很多久负盛名 的书籍,比如《C程序设计语言》,《UNIX 编程环境》,《编程风格元素》和这本《程序 设计实践》。

大师写的书基本上都是一个基调,言简意赅,字字珠玑,简洁但不简单。这本书没有任何 的废话,从前言到附录,每一个部分都能让你受益匪浅。这本书很薄,却无所不包,分成 九个章节,每一章都讲述了程序设计环节中的重要问题。这本书可以和前阵子读的《程序 员修炼之道》相媲美,强烈推荐每个程序员认真的读一读(不管你是不是主要用C语言 做开发)

英雄所见通常略同,这本书和原来看过的很多书有重合,但是这本书讲的更加清晰明了。 这也体现了书名中的实践二字,这本书提到了很多其他书籍都泛泛而谈的金科玉律的实 践方式。

编码风格

关于菜鸟和大牛之间的差距,有人说体现在代码调试能力上,有人说体现在文档阅读能力 上,我更认同的观点是体现在代码风格之上。一段好的代码是美的,优雅的,这是每一个 程序员都应该追求的境界,代码的简单、清晰、直白远比刻意的高效重要的多。

很多人建议名字起的越长越好,但是起名好坏的关键不在于长短而在于清晰,传达它的目 的,而清晰性和作用域相关。

1
2
3
for (theElementIndex = 0; theElementIndex < numberOfElements;
theElementIndex++)
elementArray[theElementIndex] = theElementIndex

这样的代码相比于下面的代码真的没有任何值得推荐的地方

1
2
for (i = 0; i < nelement; i++)
elem[i] = i

很多人觉注释越多越好,但是代码本身的简单和清晰比冗长的注释要重要的多。你应该首 先改进自己的代码,如果代码实在比较难以做到清晰,加上适当的注释并在修改了代码的 时候保持和代码的同步更新。

算法和数据结构

对于我们大部分的人来说,学习算法和数据结构不在于自己发明新的算法和数据结构而在 于选择,让你在遇到问题、需要优化的时候能够有所选择。数据结构的四大金刚:数组、 链表、二叉树、哈希表是每程序员都应该了解的。

设计和实现

这一章给出了马可夫链文字自动生成程序的设计和 5 种不同的语言的实现,我也试过用 Python 重写了该程序。一个程序员需要掌握多门语言,不在于炫耀自己的技术而在于选 择和权衡,如果你只会 C 语言,那么即使在不需要过多考虑性能的地方你也没有机会 享受到C++、Python 带来的开发效率上的提升。

程序的设计通常和你选择的实现语言无关,但是和你选择的数据结构有关,你使用的数据 结构通常就决定了你使用的算法。你应该选择简单清晰的算法和数据结构,然后不断的迭 代的,因为没有人可以一触而就。此外产品级别的代码需要不断的打磨和测试。

接口

这一章首先展示了一项非常有用的技术——原型——一个在各种书籍中说烂了的技术,这本书 给出了它如何应用的实际例子。代码不可能一次成功,我们不应该坐在自己座位上苦思冥 想,我们应该开始写代码,建立一个原型,尝试自己的算法。你不是天才,不管你多自恋 ,你都不可能天才到一次把程序写对。

关于接口最重要的东西应该是它的接口说明(specification),在《程序员修炼之道》 中作者把它称为合约(contact),个人理解这两个概念殊途同归——告诉别人你需要什么 ,你提供什么,这是一种供求双方的协议。

好的的接口设计还要考虑很多其他的问题,比如:信息隐藏、资源管理、错误处理等等。 有些语言提供原生的支持比如C++的访问控制、RAII、异常处理等,有些语言却要设计者 自己处理和权衡这些问题,比如 C 程序员。

调试

代码预防的越好,BUG 越少,调试所花费的时间就越少,所以你应该先读一读第一章。

关于大部分人想到调试第一反应是借助调试器,打断点,单步跟踪,在作者看来,这是最 后一招。你应该先理性的分析一下BUG出现的现象,看看出错的时候的栈帧信息,查查 你最近的修改,和别人讲讲你的代码,定位最可能出错的地方,实在不行就休息休息,这 些方式比你用调试器快很多。

测试

测试的首要原则是——你得做测试。

你自己写的代码,你自己应该做测试,不要依靠测试人员帮你做测试,因为没有人比你自 己更懂你自己写的代码。测试不是为了证明你的程序是对的,而是尽可能找到你的程序中 可能出错的地方。没有人的程序是没有错的,即使是像高德纳这样的上帝级别的人物写的 程序一样会出错。

我们应该在自己写代码的同时就开始测试,写一点测试一点,而不是写完了再一次性的测 试。检查你的边界条件,测试你的先决条件和后置条件,使用断言,防御性的编程。这些 方法恰恰是《程序员修炼之道》中按合约编程的实践方式。

我们需要全面的测试,你可以使用不同的方式实现同一个问题,对比两者的输出,如果不 一样,那至少有一种方式是错误的。你可以用一种非常简单低效的方式实现同一个问题, 然后用它对比你自己的版本,如果结果不同,很大的可能是你的程序有问题。

自动测试在《程序员修炼之道》中也有提到,但是这里给出了实践方式。你需要使用到一 些脚本语言来帮助你完成自动化的操作,比如awkpython等。

性能调优

如果不必要,不要尝试调优,最简单直接的方式编写的代码最有可能接近正确。

如果你一定要做优化,先用 profiler 查一查性能的瓶颈是在什么地方,在无关紧要的 做优化只会浪费你的时间。

优化要适可而止。

可移植性

不要使用标准以外的特性,标准之内的特性使用主流的部分(那些大家都在用,确定不 会有未定义问题的特性),如果你必须问一个语言专家才能一个特性是否是可移植的, 不要用它。

对于C和C++的条件编译很多人说它是可移植性的助手,实际上他们却是可移植性的大忌, 不要使用他们。条件编译使得你看到的代码可编译器看到的代码不一致,它使得代码非常 难以理解和测试。你的程序在这个平台上可以正常运行只是说明编译器看到的那一部分代 码正常运行,你换一个平台可能立马出错。

我们应该使用抽象,把平台相关的细节隐藏在实现细节中,把和平台相关的代码移动到独 立的文件中,比如linux平台的实现放在linux.cpp,windows平台的实现放在 windows.cpp中,通过编译选项选着合适的实现。这样你的代码会清晰很多,也更容易 扩展,更容易测试。

如果你涉及到数据的交换,最好使用文本格式,你可能会丢失一些性能,但是对于可移植 性来说有非常大的好处,而且会使得你数据处理更加简单。

记法

我原来一直不太理解《程序员修炼之道》里面作者说的“领域语言”是什么意思,也不太理 解《设计模式》里面说道的解释器模式有什么作用,更不理解为什么《黑客与画家》里面 作者会鼓吹 lisp 语言中强大到可以自动编程的宏。这一切都在我看到这本书的第九章 :Notation 之后变得豁然开朗

领域语言根本就不神秘,我们大量使用的 printf 格式化记号就是其中之一,几乎每个 语言中都包含的正则表达式也是如此。在“问题域”和“解决域”之间一直都存在鸿沟,为了 让你可以更方便的跨越这条鸿沟,你可以自己指定一门靠近问题域的语言,让你可以通过 问题域中的术语来编程。这看上去很高大上,但是语言不过是记号而已,你可以指定非常 简单的记号,比如正则表达式,比如printf的格式说明符。

自动生成代码根本就没有那么神秘,编译器本身就是机器代码的自动生成工具。早期的 C++ 编译器 cfrontC++ 代码转换成 C 语言代码,从这个角度看 cfront 就是一个代码生成器。

一切的一切又回到解决问题的王道:增加一个中间层。领域语言就是这个中间层,我们不 必非得自己写一个编译器来实现我们自己的语言,我们可以通过代码转换器转换成更底层 的语言比如C++/C,我们可以使用简单的脚本语言来帮我们完成这一转换。