宋劲杉 老师的 Linux C 编程一站式学习 是国人当中写的非常好的计算机书籍,豆瓣评分 9.3 ,非科班出生的程序员拿来入门非常好,后面部分关于程序原理的内容也适合工作多年的人进行查缺补漏。书籍囊括了程序设计基本思想和开发调试方法,以 Linux 平台为载体介绍 C 语言基础及程序工作原理,Linux 系统编程,对计算机组成、C 语言、操作系统、编译原理等课程知识达到融汇贯通。很难得图书还是开源的,链接 http://akaedu.github.io/book/index.html, 第二版 一站式学习 C 编程 删掉了 Linux 系统编程相关的内容,其它变化不大,推荐第一版。这篇总结图书中提到的编程思想及相关思维方法。
每读一节就总结一套概念之间的关系图。比如下面的概念:
程序由指令组成,计算机只能执行低级语言的指令,高级语言执行前要先编译或解释,好处是平台无关性,平台是一种体系结构,是一种指令集,就是一种机器语言。
编程语言是一种形式语言,对应人类用的自然语言,形式语言有严格的语法 (Syntax) 规则,由符号 (Token) 和结构(Structure) 的规则组成。关于 Token 的规则称为词法 (Lexical) 规则,关于结构的规则称为语法 (Grammer)规则。分析句子结构的过程称为解析 (Parse)。解析完句子,理解句子的上下文,暗示的内容,是**语义 (Semantic) **的范畴。形式语言和自然语言的区别:歧义性、冗余性、与字面意思的一致性。
一个容易被用户接受的设计应该遵循最少例外原则。C 语言的设计非常优美,C++ 的设计非常负责,充满例外,饱受争议。
不要把必要条件当充分条件。
组合规则是理解语法规则的基础,根据语法规则任意组合,可以用简单的常量、变量、表达式、语句和声明搭建出任意复杂的程序。
如果定义一个概念要用到这个概念本身,它的定义是递归的。需要定义一个最关键的基础条件 (Base Case),比如阶乘 (Factorial) 里 0 的阶乘等于 1。如果你相信你正在写的递归函数是正确的,并调用它,然后在此基础上写完这个递归函数,那么它就会是正确的,从而值得你相信它正确。
每次都有一点区别的重复工作称为迭代。
递归和循环是等价的,用循环能做的事用递归都能做,比如 LISP 语言只有递归而没有循环,编译器的实现用了大量递归。
递归例子:两个正整数的 a 和 b 的最大公约数 (GCD, Greatest Common Divisor) 使用 Euclid 算法,如果 a 除以 b 能整除, 则最大公约数是 b,否则,最大公约数等于 b 和 a%b 的最大公约数。
无限循环 (Infinite Loop) 的例子,n 忽大忽小,著名的 3+1 问题:
while (n != 1) {
if (n % 2 == 0) {
n = n / 2;
} else {
n = n * 3 + 1;
}
}
函数式思路:整个递归调用过程中,虽然分配和释放了很多变量,但所有变量都只在初始化时赋值。 命令式思路:通过循环对变量多次赋值来达到同样的目的。C 语言主要用 Imperative 的方式。
组合使得系统可以任意复杂,而抽象使得系统的复杂性是可以控制的,任何改动都局限在某一层,而不会波及整个系统。计算机科学家 Butler Lampson 说过:"All problems in computer science can be solved by another level of indirection."
数据代替代码,以打印星期几的代码为例:通过下表访问字符串组成的数组可以代替一堆 case 分支判断,把每个 case 里重复的代码 (printf 调用)提取出来。写代码最重要的是选择正确的数据结构来组织信息,设计控制流程和算法尚在其次,只要数据结构选择正确,其它代码自然而然变得容易理解和维护。Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowcharts, they'll be obvious.
举例,归并排序,时间复杂度O(nlgn):
针对有序序列,每次把搜索范围缩小一半。
探索问题的解时走进了死胡同,则需要退回来从另一条路继续探索。举例:基于堆栈的深度优先搜索 (DFS, Depth First Search)。
编译时错误、运行时错误 (Run-time) 、逻辑和语义错误。
首先分析和分解问题,把大问题分解成小问题,再对小问题分别求解。这个过程在代码中体现函数的分层设计 (Stratify) ,底层函数解决小问题,上层函数通过调用底层函数解决更大的问题。
插入排序算法采用增量式的策略解决问题,每次添一个元素到已排序的子序列中,逐渐将整个数组排序完毕。时间复杂度O(n^2)。
函数里的 printf 语句类似脚手架 (Scaffold),验证无误后就可以撤掉。把 Scaffolding 的代码注释掉。
如果每个函数的文档都非常清楚地记录 Precondition、Maintenance、Postcondition 是什么,每个函数都可以独立编写和测试。
发布时在包含 assert.h
之前定义一个 NDEBUG
宏,就可以禁用 assert.h
中的 assert 宏定义,或者编译时加上选项 -DNDEBUG
。
switch 的语句块和循环结构的语句块没有本质区别:Duff’s Device 。
编码风格
Thus, programs must be written for people to read, and only incidentally for machines to execute.