编程精粹—— Microsoft 编写优质无错 C 程序秘诀 02:设计并使用断言

这是一本老书,作者 Steve Maguire 在微软工作期间写了这本书,英文版于 1993 年发布。2013 年推出了 20 周年纪念第二版。我们看到的标题是中译版名字,英文版的名字是《Writing Clean Code ─── Microsoft’s Techniques for Developing》,这本书主要讨论如何编写健壮、高质量的代码。作者在书中分享了许多实际编程的技巧和经验,旨在帮助开发人员避免常见的编程错误,提高代码的可靠性和可维护性。


不记录,等于没读。本文记录书中第二章内容:设计并使用断言。


一个好的开发策略是维护程序的两个版本:发布版本调试版本。通过在调试版本中使用 断言 语句,你可以检测以下错误:

  • 错误的函数参数
  • 未定义行为
  • 其他程序员做出的错误假设
  • 不知何故出现的不可能条件

仅在调试模式下启用的备用算法有助于验证函数结果和函数中所使用算法的正确性。

第一章介绍了一些方法,以便检测到更多的错误,比如启用所有可选的编译器警告、使用语法和可移植性检查工具等。这些措施当然是好的,只是这些措施只能查出所有错误的一小部分。举一个例子:

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

这句代码在多数情况下都会工作良好,直到 malloc 函数调用失败。当 malloc 失败时,就会给 memcpy 函数返回 NULL 指针,从而出错。

编译器检查不出这种错误,同样,编译器也检查不出算法错误,无法验证程序员所作的假设。寻找这种错误非常艰苦,但如果在实现 memcpy 函数时,检查一下参数是否有效,就可以自动捕获这个错误。

聪明的程序员将调试代码隐藏在断言 assert。断言的好处是用户在错误发生时,可以自动地把它们检查出来。

assert 是一个宏,如果其参数的计算结果为假,就中止调用程序的执行。

assert不应该产生其它副作用。因此assert应该为宏而不是函数,因为函数调用会引起不期望的内存或代码交换。

使用断言对函数参数进行确认

比如前面提到的 memcpy 函数,我们可以使用断言进行参数确认,代码如下:

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);
}

这样,当给参数 pvTopvFrom 传递 NULL 时,就会触发断言,从而自动的发现程序代码错误。

使用断言避免未定义行为

要从程序中删除未定义的特性或者在程序中使用断言来检查出未定义特性的非法使用

使用库函数memcpy时,如果在存储空间相互重叠的对象之间进行了拷贝,结果是未定义的。

可以使用断言来进行重叠检查:

ASSERT(pbTo >= pbFrom + size || pbFrom >= pbTo + size);

假如你之前从来没有见过重叠检查,当这个断言捕获到错误时,你能看出来它是什么意思吗?

断言捕获了一个错误,我们却不知道断言的作用,没有什么比这件事更令人沮丧了。

如果搞不清楚相应断言检查的是什么,就很难知道错误是出现在程序中,还是出现在断言中。

解决这个问题的方法是:给不够清晰的断言加上注解

不要浪费别人的时间——详细说明不清晰的断言

一个人在穿过森林时,看到树上钉着一块大牌子,上面写着两个红色大字:“危险”。但危险到底是什么?

除非告诉人们危险是什么或者危险非常明显,否则这个牌子起不到提高人们警觉的作用,人们会忽视牌子上的警告。

同样,程序员不理解的断言也会被忽视。

利用断言消除隐式假设

如果假设long占用4个字节,可以使用以下断言来检查假设是否正确:

ASSERT(sizeof(long) == 4);

在编写函数时,要进行反复的思考,并且自问:“我打算做哪些假设?”

一旦确定了相应的假设,就要使用断言对所做的假设进行检验,或者重新编写代码去掉相应的假设。

另外,还要问:“这个程序中最可能出错的是什么,怎样才能自动地查出相应的错误?”努力编写出能够尽早查出错误的测试程序。

利用断言检查不可能发生的情况

断言是检查那些在正常情况下绝不应该发生的情况,而不是用于检查错误。换句话说,断言是防止程序员失误的一种手段,绝不要把必须执行的代码放入断言中

比如函数参数不可能超过某个范围,可使用断言来验证;malloc 函数返回是否为 NULL 就不能使用断言来检查,判断是否为 NULL 是错误检查的一部分;

比如一个解压缩协议规定:如果遇到0xA5,那么0xA5后面必须是 字符压缩序列,字符压缩序列占用两个字节,第一个字节表示 压缩的字符,第二个字节表示 字符重复的个数

我们认为 字符序列 只能有两种情况:

  1. 压缩的字符是 0xA5,字符重复个数固定为1
  2. 压缩的字符不是 0xA5,字符重复个数必须大于等于4

则可以使用以下断言对规则进行验证:

#define REPEAT_CODE    0xA5
//...
ASSERT( size>=4 || (size==1 && b==bRepeatCode) )

如果这一断言失败说明内容不对或者字符压缩程序中有错误。无论哪种情况都是错误,而且是不用断言就很难发现的错误。

利用断言发现静默的异常行为

防错性程序设计常常被誉为有较好的编码风格,但它却隐瞒了绝不应该发生的错误。

核反应堆堆芯过热,程序自动地向堆芯灌水、插入冷却棒或者做其它能让堆芯冷却下来的方法。只要程序已经控制了事态,就不会向有关人员报警。

这和我们写的防错程序很像,当某些意料不到的事情发生时,程序只进行静默处理。

但是堆芯不会无缘无故地出现过热现象,一定是发生了某种不同寻常的事情,才会引起这一故障。但在值班人员眼里,核反应堆一切正常,错误被隐瞒了。

即便如此,防错性程序仍然有它的价值。我们希望在进行防错性程序设计时,不要隐瞒错误

一方面一如既往地利用防错性程序设计进行编码,另一方面在事情变糟的情况下利用断言进行报警

核反应堆堆芯过热,立即向工作人员发出警报。同时,程序自动地向堆芯灌水、插入冷却棒或者做其它能让堆芯冷却下来的方法。

如果使用到防错性程序设计,在编码之前都要问自己:**“在进行防错性程序设计时,程序中隐瞒错误了吗?”**如果答案是肯定的,就要在程序中加上相应的断言,以对这些错误进行报警。


有很多书都有讲解断言,有很多开源代码都在使用断言。在工作了几年之后,我对断言也有了一些见解,我将它们写在了《随想012:断言》 这篇文章中,推荐阅读。






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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值