《编程精粹:编写高质量C语言代码》,英文书名:Writing Solid Code。这本书有两个中文版本,豆瓣链接分别是:

20年前我看的是最早那个100多页的版本。作者 Steve Maguire, 1986年加入微软公司,开始为Macintosh开发软件。
主要是Excel 软件在苹果核微软两个平台上的开发工作。这本书是1993年出版的,当年就获得了Software Development Jolt Productivity Award。
这是一个很高的评价。他的另外一本书《Debugging the Development Process》,同样获得了1994年的Software Development Jolt Productivity Award。

编写无错代码的最好方法是把防上错误放在第一位

这个原则我很喜欢。开发人员调试代码的时间是比写代码的时间要长的。所以提高工作效率的最好办法是把bug消灭在调试之前。做到这一点,需要经验,作者写这本书的目的是为了介绍成熟的技巧和方法,无论经验丰富的开发者还是初学者,学习书里的方法都会受益。每隔几年我都会读一遍这本书,时刻提醒自己那些还没做到。

三原则:

  • 不要接受具有特殊意义的参数,NULL指针表示输入输出使用同一个变量。
  • 按照设计来实现而不能近似地实现
  • 努力使每个函数一次就完成住务

假想的编译程序

JavaScript代码不需要编译,但这个原则的意思是:写每行代码的时候,你要清楚代码运行的时候,会发生什么事情。 不要光依赖黑箱测试方法。 还应该试着去模仿前面所讲的假想编译程序, 来除排
运气对程序测试的影响,自动地抓住错误的每个机会。

  • 使用编译程序所有的可选警告设施
  • 增强原型的能力
1
2
void* memchr(const void* pv, int ch, int size);
void* memchr(const void* pv, unsigned char ch, size_t size);
  • 在将新做的修改并入原版源代码之前, 程序员应该实际地进行相应的单元测试

自己设计并使用断言

1
strCopy = memcpy(malloc(length), str, length);

编译程序也查不出算法的错误,无法验证程序员所作的假定。既要维护程序的交付版本,又要维护程序的调试版本.

1
2
3
4
5
6
7
void memcpy(void* pvTo, void* pvFrom, size_t size) {
void* pbTo = (byte*)pvTo;
void* pbFrom = (byte*)pvFrom;
assert(pvTo != NULL && pvFrom != NULL);
while(size-->0)
*pbTo++ == *pbFrom++; return(pvTo);
}
  • 要使用断言对函数参数进行确认
  • 要从程序中删去无定义的特性, 或者在程序中使用断言来检查出无定义特性的非法使用
  • 不要浪费别人的时间, 详细说明不清楚的断言
  • 不是用来检查错误的
  • 消除所做的隐式假定,或者利用断言检查其正确性
  • 不可能的事用也能发生, 利用断言来检查不可能发生的情况
  • 在进行防错性程序设计时,不要隐瞒错误. 在出错时,除了要恢复数据,防止崩溃,要用断言来报警。
  • 要利用不同的算法对程序的结果进行确认
  • 不要等待错误发生,要使用初始检查程序。 rubbish in, rubbish out

思考题:

  • 当程序员为枚举类型增加新元素时,有时会忘记在相应的switch 语句中增加新的 case 条件。怎样才能使用断言帮助查出这个问题?
  • 假定你必须维护一个共享库并想在其中使用断言,但又不想发行这个库的源代码, 那么怎样定义 ASSERTMSG 这个断言宏。

为子系统设防

子系统都要对其实现细节进行隐藏,所隐藏的实现细节可能相当复杂。 在进行实现细节隐藏的同时, 子系统为用户提供了一些关键的入口点。 程序员通过调用这些关键的入口点来实现不同子系统的通讯。 因此如果在程序中使用这样的子系统并且在其调用点加上调试检查,那么不用花很大力气就可以进行许多的错误检查。由于用户可能得不到子系统的源代码, 或者即使能够得到, 这些源代码的实现也未都必相同,因此可以利用所谓 “外壳” 函数把内存管理程序包装起来, 并在这层包装的内部加上相应的测试代码。

1
2
3
4
5
6
7
/* fNewMemory ─── 分配一个内存块 */
bool fNewMemory(void** pv, size_t size)
{
byte** ppb = (byte**)ppv;
*ppb = (byte*)malloc(size);
return(*ppb != NULL);
}
  • 请求malloc分配长度为零的内存块时,其结果无定义, assert宏可以解决问题。
  • 如果malloc 分配成功,那么它返回的内存块的内容无定义。 不能简单填充为0,MFC。

重要的原则:

  • 要消除随机特性,使错误可再现。
  • 冲掉无用的信息,以免被错误地使用
  • 如果某件事甚少发生的话,设法使其经常发生
  • 保存调试信息,以便进行更强的错误检查
  • 建立详尽的子系统检查并且经常地进行这些检查
  • 努力做到透明的一致性检查
  • 要用大小和速度来换取错误检查能力

所加入的调试代码会引起程序交付版本和调试版本之间的区别。但只要在加入调试代码时十分谨慎,并没有改变原有程序的内部行为,那么这种区别就不应该有什么问题。

对程序进行逐条跟踪

程序员测试其程序的最好办法是对其进行逐条跟踪。

  • 要构造出合适的参数,对每一条代码路径进行逐条的跟踪。
  • 当对代码进行逐条跟踪时,要密切注视数据流,观察变量的值和变化趋势。
  • 编译器优化程序可能会隐瞒执行的细节,对关键部分的代码要进行汇编指令级的逐条跟踪。

糖果机界面

getchar函数的返回值是int,不只是要返回一个char,同时也可以返回EOF,表示结束。如果用char型变量来接受返回值,则会出错。因此,函数的名字有误导性。
另外的选择是,定义为:bool getchar(char* pch),

  • 不要在正常地返回值中隐藏错误代码。
  • 要编写功能单一的函数,反例就是realloc函数
  • 编写函数使其在给定有效的输入情况下不会失败。tolow函数,或者断言检查非字母输入,或者原值返回,但是不能出错。减少检查函数返回值的机会。
    在设计函数时尽量避免返回错误值,以免程序员错误地处理或漏掉这些返回值。

风险事业

讨论在某些普通的编码实践中所存在的一些风险, 以及如何做才能减少甚至消除这些风险。

  • 使用有严格定义的数据类型,尽量用可移植的数据类型。stdint.h
  • 经常反问: “这个变量表达式会上溢或下溢吗?”
  • 尽可能精确地实现设计,近似地实现设计就可能出错
  • 避免无关紧要地 if 语句,避免使用嵌套的“?:“运算符
  • 每种特殊情况只能处理一次
  • 避免使用有风险的语言惯用语,移位操作代替除法等

在某些情况下, 取消一般的错误处理代码是有可能的, 但要保证所做的事情不会失败。这就意味着在初始化时要对错误进行一次性处理或是从根本上改变设计。

编码中的假象

  • 只引用属于你自己的存储空间,对象空间的申请,释放要么对象本身自己来负责,要么是由确定的管理着来负责。
  • 不要利用静态(或全局)量存储区传递数据。尤其是在多线程环境下。
  • 为一般水平的程序员编写代码,代码尽量简单。

态度问题

错误不会消失,要么被掩盖了,要么被悄悄的改正了。需要有探究精神,错误要及时检查,搞清楚啊原因,随着系统的复杂,修复错误的代价越来越高。

  • 不要通过把改正错误移置产品开发周期的最后阶段来节省时间
  • “一次性”地修改错误会带来许多问题
  • 错误是一种负反馈, 程序开发倒是快了, 却使程序员疏于检查。可以尝试在修改完错误之后再增加新特性。
  • 若把错误数保持在近乎于 0 的数量上, 就可以很容易地预言产品的完成时间。
  • 除非关系产品的成败,否则不要整理代码。停止无意义的重构。
  • 不要实现没有战略意义的特征
  • 不允许没有必要的灵活性,过度灵活的函数,
  • 在找到正确的解法之前,不要一味地“试”,要花时间寻求正确的解。看文档,系统学习会节省后续的大量的无意义的尝试时间。