编程精粹—— Microsoft 编写优质无错 C 程序秘诀 01:假想的编译器

这是一本老书,作者 Steve Maguire 在微软工作期间写了这本书,英文版于 1993 年发布。2013 年推出了 20 周年纪念第二版。我们看到的标题是中译版名字,英文版的名字是《Writing Clean Code ─── Microsoft’s Techniques for Developing》,这本书主要讨论如何编写健壮、高质量的代码。作者在书中分享了许多实际编程的技巧和经验,旨在帮助开发人员避免常见的编程错误,提高代码的可靠性和可维护性。就像 20 年后 (2013 年) 作者描述的那样:
When I set out to write the first edition of Writing Solid Code twenty years ago, I had a simple thought in mind: Give programmers proven tools, techniques, and philosophies to help them write rock-solid, bug-free code.

二十年前,当我开始编写《Writing Solid Code》第一版时,我心中有一个简单想法:给程序员们提供经过验证的工具、技术和开发哲学,帮助他们编写出坚如磐石、零错误的代码。

Over the years, programmers have regularly asked me if my views have changed much since the book’s original publication.

多年来,程序员们经常问我,自从该书首次出版以来,我的观点是否发生了很大变化。

Not only have my views not changed over all those years, but I’ve embraced the concepts and philosophies expressed in the book’s pages even more staunchly.

这么多年来,我的观点不仅没有改变,反而更加坚定地支持书中阐述的概念和开发哲学.

I had no idea that the book would become a runaway best-seller, eventually being translated into more than 16 different languages. I certainly didn’t expect that so many software development companies would make the book required reading for their developers. Nor did I anticipate that Universities around the world would use the book in their computer science courses.

我完全没有想到这本书会成为畅销书,最终还被翻译成超过16种不同的语言。我当然也没有预料到会有这么多软件开发公司将这本书列为他们的开发人员的必读书目。我也没有预料到全球各地的大学会在他们的计算机科学课程中采用这本书。

作者 Steve Maguire 曾在微软担任高级项目经理和软件工程师。这是作者的第一本书,它的后续之作是《Debugging the Development Process》,中译版名字为《微软研发:致胜策略》,我会在将来介绍这本书。在这两本书中,他分享了大量关于他在微软工作期间的经验和见解。


不记录,等于没读。本文记录书中第一章内容:假想的编译器。


编写无错误代码的关键是要更加了解 错误 是如何产生的。多年以来,作者经常问自己两个关键的问题,从这两个问题中得到的答案,形成了本书介绍的诀窍。程序员遇到的每一个错误,都要问自己这两个关键的问题:

  1. 怎样才能自动地查出这个错误?

    How could I have automatically detected this bug?

  2. 怎样才能避免这个错误?

    How could I have prevented this bug?

本书提供的指南在大多数情况下你都应该遵循,但当你打破它们时可以获得更好的结果时,那么就打破它

要记住:

在任何时候,跟在大多数人的后面常常是所能选择的最坏道路。因此在成为别人的追随者之前一定要确定这样做确实有意义,而不是仅仅因为其他人如此自己也亦步亦趋。


假想的编译器

如果存在一个理想的编译器,它能检测到程序中的每个错误并给出错误信息,那么消除代码中的错误将会非常简单。问题是,这种无所不能的编译器并不存在,但是我们可以有一些方法,可以自动检测到更多的错误:

  • 启用所有可选的编译器警告
  • 使用语法和可移植性检查工具(比如 CppcheckPC-LIntSplint 等 )
  • 使用自动化单元测试

先考察一下测试人员是如何发现 BUG 的:

  1. 向程序输入数据,然后观察输出。
  2. 如果他注意到一些数字是错误的,或者某个功能没达到预期,或者程序崩溃了,那么就发现了一个BUG。

如果他输入的数据恰好无法触发 BUG 呢?如果他一时疏忽错过异常现象呢?那么这个 BUG 就会溜到正式版本中,然后在将来的某个时候被用户遇到。

所以说,测试人员发现 BUG 是靠运气吗

是的。

这并不是批判测试人员的所做所为的,这只是想指出一个事实:很难用黑盒测试一个程序。利用黑盒测试能做的只是往程序里填数据,并看它输出什么。这就好比确定一个人是不是疯子一样:你问一些问题,你倾听对方回答,然后你做出判断。但这样你还是不能确定这人是不是疯子,因为你不知道对方脑袋里在想些什么。

你永远会质疑黑盒测试的输入数据

  • 数据够吗?
  • 数据对吗?

不要依赖黑盒测试。去做一些事情,抓住每一个机会 自动地 捕获BUG,而不是靠运气。这些事情包括:

  • 启用所有可选的编译器警告
    好的编译器能够把屡次出错的合法 C 习惯用法看成是拼写错误。比如某些编译器可以将警告级别设置为 MISRA C 2004 或者 MISRA C 2012,而 MISRA C 是一套用于嵌入式系统中编写高可靠性和高安全性软件的编码准则,最初是为汽车行业制定。它实际上是标准 C 的一个子集,对标准 C 附加了一系列规则和指导。使用 MISRA C 会更严格的检测代码,有些标准 C 允许的用法会被警告。本文附录 1 给出一些这样的例子。
  • 使用语法和可移植性检查工具(比如 CppcheckPC-LintSplint 等 )
    通常这些检查器检查的错误更详细、更彻底。一旦原程序变成了没有 PC-Lint 错误的形式,继续保持就变得很简单了。
  • 如果有单元测试,请使用它们
    不要过高估计自己编写正确代码的能力,要做测试!即使没有新增代码,只是调整代码;或者做很小的修改都要运行单元测试。
    有时,似乎可以跳过一些安全措施,这些安全措施用来避免程序出错。但走捷径之时,就是麻烦将近之日

消除代码中 BUG 的最好方法是找到它们,越早越好。寻找自动捕获 BUG 的方法。

努力减少程序员迫不得已的排错,优先让编译器和工具(比如 PC-Lint)指明错误。

附录 1

1 while 循环错放了一个分号

如下代码给出了一个复制内存代码,但 while 表达式后误写了一个分号。

void* memcpy(void *pvTo, void *pvFrom, size_t size) {
	byte *pbTo = (byte *)pvTo; 
	byte *pbFrom = (byte *)pvFrom; 
	while(size-->0); 					//<--- 这里错误的键入了分号
		*pbTo++ = *pbFrom++; 
	return(pvTo); 
}

对于这个代码,逻辑上是错误的,但编译器认为这是一个完全合法的 while 语句,其循环体为空语句。由于使用空语句的场景比较少,所以编译器常常在遇到循环体为空语句时给出一条可选的警告信息,如果你选择启用这个警告信息,编译器就会自动提醒你注意这样的错误。如果你真正要使用循环体为空语句的时候,可以使用编译手册建议的解决方法,比如使用一个将被优化掉的常量表达式 ( NULL ),或者使用一个空块 ({ })。这里使用空块举例:

char *strcpy(char *pchTo, char *pchFrom) {
	char *pchStart = pchTo;
	while(*pchTo++ = *pchFrom++)
		{}								//<--- 这里使用了空块
	
	return (pchStart);
}

2 错误的赋值

C 语言允许在编写表达式的任何地方使用赋值 (=) ,但如果不小心,这种额外的灵活性可能带来错误。比如:

if(ch = '\t')							//<--- 本想与制表符比较,但实际是赋值
	ExpandTab();

这种代码编译器不会产生错误,因为代码是合法的。现代的编译器一般能检查到这种可能的错误,并给出警告信息。除了 if 控制表达式外,还有:

  • forwhile 控制表达式
  • &&|| 表达式

在以上表达式中,如果确实需要使用赋值,比如上面例子的拷贝代码:

while(*pchTo++ = *pchFrom++)			//<--- 这里的赋值可能会产生编译警告
	{}

为了不让编译器产生警告信息,可以改写成:

while(()*pchTo++ = *pchFrom++) != '\0')
	{}






每一份打赏,都是对创作者劳动的肯定与回报。
千金难买知识,但可以买好多奶粉

相关推荐

  1. Android 编译C程序APP

    2024-06-16 12:06:04       15 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-06-16 12:06:04       17 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-16 12:06:04       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-16 12:06:04       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-16 12:06:04       18 阅读

热门阅读

  1. KeyedProcessFunction 在 Flink项目中的应用实战

    2024-06-16 12:06:04       10 阅读
  2. 【LeetCode最详尽解答】15-三数之和 3sum

    2024-06-16 12:06:04       10 阅读
  3. LNMP网站架构

    2024-06-16 12:06:04       8 阅读
  4. C语言——文件

    2024-06-16 12:06:04       7 阅读
  5. 【网络安全产品】---下一代防火墙

    2024-06-16 12:06:04       10 阅读
  6. 一个有用的docker entrypoint脚本的范例

    2024-06-16 12:06:04       7 阅读
  7. 【轮询负载均衡规则算法设计题】

    2024-06-16 12:06:04       7 阅读