文章吧-经典好文章在线阅读:《编程精粹》读后感精选10篇

当前的位置:文章吧 > 经典文章 > 读后感 >

《编程精粹》读后感精选10篇

2018-02-10 20:27:02 来源:文章吧 阅读:载入中…

《编程精粹》读后感精选10篇

  《编程精粹》是一本由Steve Maguire著作,人民邮电出版社出版的256图书,本书定价:45.00元,页数:2009.2,文章吧小编精心整理的一些读者的读后感,希望对大家能有帮助

  《编程精粹》读后感(一):我读过的第一本原版技术书籍

  《代码大全》也在我的当前阅读列表中停留了三四年了。天幸最近两年的阅读速度有了很大的改进。终于把这本书先pass掉了——其实加起来可能就用了有30个小时

  这本书和《代码大全》的定位一致的。但是感觉它并没有非常系统地写完美。比如前几章还能寻出脉络,但是后几章完全是各种知识的杂烩。没有明显逻辑关系

  不过,在这本书前,我曾经一度觉得我自己已经可以很完美地翻译一本技术类的专业书,但是,这本书中,有某些地方,我觉得还是有一定的小困难

  对于求职和在校的学生,或者阅读能力不太好的同志们。建议把重心仅仅放在代码大全上即可。这本书并非非读不可。我觉得代码大全的覆盖面会更广。

  但是,仍然要说但是,这本书对于某些类别的bug的分析有价值的。但凡有能力读,都可以用心把它读过来一遍。

  最后,我想说,读英文原版是有价值的。有些描述,中文里看起来可能是套话,但是英文的描述,可能会激发你思考。这一点在我现在看的英文版格林童话体现地尤其明显。中文的一个故事,读过了,那些句子你很可能会从脑海里滑过去。

  《编程精粹》读后感(二):《Writing clean code》读书笔记

  写在前面的话:

  这两天看了《Writing Clean Code》,很受启发,感觉值得再读,于是整理了一点笔记,作为checklist,以备速查。

  原书共8章,每章都举一些例子,指出不足,再用通用的规则改写,每章结束时会总结一下要点,其中覆盖了比较重要的规则。附录A是作者整理的编码检查表。本笔记前8章和原书前8章对应,列出了所有的规则,对比较特别或者比较难理解的规则还附上了书中的例子,偶尔加一两句个人想法。第9章是原书各章末尾要点的汇总。第10章是原书的编码检查表。

  本笔记只作为原书的一个速查手册,详细内容请看原书。

  中译本:

  《编程精粹 ─── Microsoft 编写优质无错C 程序秘诀》Steve Maguire 著,姜静波 佟金荣 译,麦中凡 校,电子工业出版社

  英文版:

  《Writing Clean Code ── Microsoft Techniques for Developing Bug-free C Programs》Steve maguire, Microsoft Press

  英文版原名:

  《Writing Solid Code ── Microsoft Techniques for Developing Bug-free C Programs》Steve maguire, Microsoft Press

  1 假想的编译程序

  1.1 使用编译程序所有的可选警告设施

  1.2 使用lint 来查出编译程序漏掉的错误

  1.3 如果有单元测试,就进行单元测试

  1.4 Tips

  C 的预处理程序也可能引起某些意想不到结果。例如,宏UINT_MAX 定义在limit.h

  中,但假如在程序中忘了include 这个头文件,下面的伪指令就会无声无息失败

  因为预处理程序会把预定义的UINT_MAX 替换成0:

  #if UINT_MAX > 65535u

  …

  #endif

  怎样使预处理程序报告出这一错误?

  2 构造自己的断言

  2.1 既要维护程序的交付版本,又要维护程序的调试版本

  少用预处理程序,那样会喧宾夺主,尝试用断言

  2.2 断言是进行调试检查的简单方法。要使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是在最终产品中必须处理的。

  这是断言和错误处理的区别

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

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

  这个对C/C++很适用

  2.5 不要浪费别人的时间 ─── 详细说明不清楚的断言

  森林中只标有“危险”,而没指出具体是什么危险的指示牌将会被忽略。

  2.6 断言不是用来检查错误的

  当程序员刚开始使用断言时,有时会错误地利用断言去检查真正地错误,而不去检查非

  法的况。看看在下面的函数strdup 中的两个断言:

  char* strdup(char* str)

  {

  char* strNew;

  ASSERT(str != NULL);

  trNew = (char*)malloc(strlen(str)+1);

  ASSERT(strNew != NULL);

  trcpy(strNew, str);

  return(strNew);

  }

  第一个断言的用法是正确的,因为它被用来检查在该程序正常工作时绝不应该发生的非

  法情况。第二个断言的用法相当不同,它所测试的是错误情况,是在其最终产品中肯定会出

  现并且必须对其进行处理的错误情况。

  2.7 用断言消除所做的隐式假定,或者利用断言检查其正确性

  Eg. 对于和机器相关的内存填充程序,不必也无法将其写成可移植的。可以用条件编译。但其中应该对某种机器的隐含假设做检查。

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

  压缩程序的例子:正常情况和特殊情况,重复次数>=4或者就等于1

  2.9 在进行防错性程序设计时,不要隐瞒错误

  2.10 要利用不同的算法对程序的结果进行确认

  2.11 不要等待错误发生,要使用初始检查程序

  2.12 Tips

  不要把真正需要执行的语句放在断言里

  3 为子系统设防

  3.1 要消除随机特性 ─── 使错误可再现

  3.2 冲掉无用的信息,以免被错误地使用

  分配内存时填充上非法值:eg. 68000 用0xA3,Intel X86系列用0xCC

  释放内存时立刻填上非法值

  引申:这个和《代码大全》中讲的进攻式编程观点类似

  3.3 如果某件事甚少发生的话,设法使其经常发生

  eg. 让realloc函数中移动内存块这种比较少发生的事情经常发生--自己包装一个relloc。

  3.4 保存调试信息到日志,以便进行更强的错误检查

  这里的日志信息相当于一个簿记功能的信息,写到内存链表中。

  168代码有错:

  if( pbiPrev == NULL )

  iHead = pbi->pbiHead;

  3.5 建立详尽的子系统检查并且经常地进行这些检查--调试检查

  eg。利用簿记和‘是否被引用’的标志,检查是否有内存泄漏和悬挂指针

  3.6 仔细设计程序的测试代码,任何选择都应该经过考虑

  eg. 先后顺序是有讲究的:先看500元的套装,再看80元的毛衣

  3.7 努力做到透明的一致性检查

  不要影响代码的使用者的使用方式

  3.8 不要把对交付版本的约束应用到相应的调试版本上

  要用大小和速度来换取错误检查能力

  3.9 每一个设计都要考虑如何确认正确性

  如果可能的话,把测试代码放到所编写的子系统中,而不要把它放到所编写子系统的外层。不要等到进行了系统编码时,才考虑其确认方法。在子系统设计的每一步,都要考虑“如何对这一实现进行详尽的确认”这一问题

  引申:回忆高中时检查结果:如果是解方程,则代入数值验算就可;如果是计算题,换一个方法再算一遍。总之,要有方法确认其正确性。

  3.10 “调试代码时附加了额外信息的代码,而不是不同的代码”

  加调试代码时要保证产品代码一定也要运行,这样才能测试到真正的产品代码。

  3.11 在自己包装的内存函数中加上允许注入错误的机制

  eg. 定义一个failure结构,在NewMemory中测试这个结构,如果为真,则返回false,表示内存分配失败。

  这样,开发者和测试者都能利用这个机制,人为的注入错误。

  4 对程序进行逐条跟踪

  4.1 代码中不会自己生出错误来,错误是程序员编写新代码或者修改现有代码的产物。如果你想发现代码中的错误,没有哪个办法比在对代码进行编译时对其进行逐条跟踪更好。

  这个如果用个“完美的”编译器就更好。

  4.2 不要等到出了错误再对程序进行逐条的跟踪

  而是把对程序逐条跟踪看成是一个必要过程。这可以保证程序按你预想的方式工作。

  引申:可以和代码走查结合在一起。或者先进行代码走查,再逐条跟踪,共两遍检查代码。

  4.3 对每一条代码路径进行逐条的跟踪

  注意覆盖率问题:语句覆盖or分支覆盖

  4.4 当对代码进行逐条跟踪时,要密切注视数据

  这样有助于发现以下错误:

  上溢和下溢错误;

  数据转换错误;

  差1 错误;

  ULL 指针错误;

  使用废料内存单元错误(0xA3 类错误);

  用 = 代替 == 的赋值错误;

  运算优先级错误;

  逻辑错误。

  4.5 源级调试程序可能会隐瞒执行的细节,对关键部分的代码要进行汇编指令级的逐条跟综

  对条件语句的各个子条件,不要一次越过,而要看每个子条件的值。

  5 糖果机界面

  作者以糖果机的糟糕的界面设计导致人犯错讲起,阐述界面设计应该指导程序员少犯错误。

  5.1 要使用户容易忽视错误情况,不要在正常地返回值中隐藏错误代码

  作者以getchar函数为例:这个函数返回一个char或者是-1,由此要求使用getchar的程序员必须用int来接收getchar的返回值,但肯定会有很多程序员忘记这一点,由此可能会引发难以捕捉的错误。

  作者设计了另一个函数界面来处理这种情况:int fGetChar(char*),返回值存入char*所指位置,而int返回flag,为true表示正确。这样,由于划分了正常的返回值和错误代码,避免了getchar的返回值要用int接收的问题。

  5.2 要不遗余力地寻找并消除函数界面中的缺陷

  Eg. 下述代码隐含着一个错误

  uf = (byte*)realloc( pbBuf, sizeNew );

  if( pbBuf != NULL )

  使用初始化这个更大的缓冲区

  如果realloc分配内存时失败,返回NULL,则pbBuf为NULL,它原来指向的内存将会丢失。

  如果界面是flag fResizeMemory( void** ppv, size _t sizeNew )则好得多

  5.3 不要编写多种功能集于一身的函数,为了对参数进行更强的确认,要编写功能单一的函数

  以realloc为例,它接受的指针为NULL但size大于0时相当于malloc,指针不为NULL但size为0时相当于free。这样realloc就混杂了malloc和free的功能,极其容易出错。

  5.4 不要模棱两可,要明确地定义函数的参数

  像realloc那样灵活的参数不一定很好,要考虑程序员给出这样的输入参数可能是出于什么原因,如果没有充分理由,用断言来禁止太灵活的输入能减少错误。

  5.5 返回值与错误处理:编写函数使其在给定有效的输入情况下不会失败

  返回错误码不是唯一的处理错误的方式。Eg. Tolower函数在遇到输入是小写字母时,应该怎么办?

  如果返回-1,那么将遇到和getchar相同的问题:程序员要用int来存储tolower的返回值。此时,tolower返回原字符也许是一个更好的方式。

  5.6 使程序在调用点明了易懂:要避免布尔参数

  通过检查调用代码,检验界面设计的合理性。

  Eg. 以下两个函数声明会导致调用方式的不同:

  void UnsignedToStr(unsigned u, char *strResult, flag fDecimal);

  void UnsignedToStr(unsigned u, char* str, unsigned base);

  前者的调用方式是:

  UnsignedToStr(u, str, TRUE);

  UnsignedToStr(u, str, FALSE);

  这显然不好。而后者是UnsignedToStr(u, str, BASE10)则好的多。

  5.7 编写注解突出可能的异常情况

  用注释写出常见的错误用法和正确用法的例子。

  5.8 小结

  本章先给出一个界面不好的例子,再给出一般原则:要不遗余力的检查界面的合理性。然后讲功能要单一,输入要有限制,输出的正常返回值要与错误码分开,用调用方式检查界面,用注释来指出异常情况。

  6 风险事业

  6.1 使用有严格定义的数据类型

  可移植类型最值得注意之处是:它们只考虑了三种最通用的数制:壹的补码、贰的补码

  和有符号的数值。

  Char只有0~127吗是可移植的

  Unsigned char 是0~255,但signed char是-127~127 (没有-128吗)是可移植的

  6.2 经常反问:“这个变量表达式会上溢或下溢吗?”

  Eg. 以下代码会导致无穷循环,因为ch会上溢为0,导致不可能大于UCHAR_MAX。

  unsigned char ch;

  /* 首先将每个字符置为它自己 */

  for (ch=0; ch <= UCHAR_MAX;ch++)

  chToLower[ch] = ch;

  eg. 以下代码会下溢,导致无穷循环,因为size_t是无符号型,不可能小于0

  ize_t size = 100;

  while (--size >= 0)

  ULL;

  6.3 尽可能精确地实现设计,近似地实现设计就可能出错

  6.4 一个“任务”应只实现一次(Implement "the task" just once).

  一个原则:Strive to make everyfunction perform its task exactly

  one time

  tatic window * pwndRootChildren = NULL;

  void AddChild( window * pwndParent, window * pwndNewBorn )

  {

  /* 新窗口可能只有子窗口 ⋯ */

  ASSERT( pwndNewBorn->pwndSibling == NULL );

  if( pwndParent == NULL )

  {

  /* 将窗口加入到顶层根列表 */

  wndNewBorn->pwndSibling = pwndRootChildren;

  wndRootChildren = pwndNewBorn;

  }

  else

  {

  /* 如果是父母的第一个孩子,那么开始一个链,

  * 否则加到现存兄弟链的末尾处

  */

  if( pwndParent -> pwndChild == NULL )

  wndParent -> pwndChild = pwndNewBorn;

  else

  {

  window *pwnd = pwndParent -> pwndChild;

  while( pwnd -> pwndSibling != NULL)

  wnd = pwnd -> pwndSibling;

  wnd -> pwndSibling = pwndNewBorn;

  }

  }

  }

  .

  假如AddChild 是一个任务,要在现有窗口中增加子窗口,而上面的代码具有三个单独的插入过程。常识告诉我们如果有三段代码而不是一段代码来完成一个任务,则很可能有错。这往往意味着这个实现中有例外情况。

  其最终的改进见下一节。

  6.5 避免无关紧要地if 语句

  以指针为中心的树的构建,可以不必为特殊情况编写代码:

  void AddChild(window* pwndParent, window* pwndNewBorn )

  {

  window **ppwindNext;

  /* 新窗口可能没有兄弟窗口 ? */

  ASSERT( pwndNewBorn -> pwndSibling == NULL );

  /* 使用以指针为中心的算法

  * 设置ppwndNext 指向pwndParent -> pwndChild

  * 因为pwndParent -> pwndChild 是链中第一个“下一个兄弟指针”

  一个“任务”应只实现一次

  */

  wndNext = &pwndParent->pwndChild;

  while( *ppwndNext != NULL )

  wndNext = &( *ppwndNext )->pwndSibling;

  *ppwndNext = pwndNewBorn;

  }

  由于没有无关的if语句,使所有的程序都会经过同样的路径,因此这段代码就会被测试的很充分。

  6.6 避免使用嵌套的“?:“运算符

  重新整理思路,甚至用查表法,都能简化过程。

  6.7 每种特殊情况只能处理一次

  不要让处理同一个特殊情况的代码散布在多个地方

  6.8 避免使用有风险的语言惯用语

  这里举了好几个例子。

  Eg. pchEnd = pch + size;

  while( pch < pchEnd )

  ULL;

  如果pchEnd恰好查找到存储器的结尾处,那么所指的位置就不存在了

  Eg. 除以2和移位:移位有风险

  Eg. while (--size >= 0) 和while(size-- > 0),前者有风险,后者却没有。

  6.9 不能毫无必要地将不同类型地操作符混合使用,如果必须将不同类型地操作符混合使用,就用括号把它们隔离开来

  6.10 避免调用返回错误的函数(Avoid calling functions that return errors)

  这样,就不会错误地处理或漏掉由其它人设计的函数所返回的错误条件。

  如果自始至终程序反复处理同样的错误条件,就将错误处理部分独立出来。Eg. 单独的错误处理子程序。

  有时更好的方法是使错误根本不会发生。Eg. 窗口的rename函数可能要realloc,从而导致失败,但通过分配超额的内存空间(都取名字长度的最大值),则这个使错误不会出现,从而避免了错误处理的代码。

  7 编码中的假象

  7.1 只引用属于你自己的存储空间

  7.2 不能引用已释放的存储区

  7.3 只有系统才能拥有空闲的存储区,程序员不能拥有

  决不要使用free以后的内存

  7.4 不要把输出内存用作工作区缓存

  Don't use output memory as workspace buffers.

  7.5 不要利用静态(或全局)量存储区传递数据

  7.6 不要写寄生函数

  依赖于别的函数内部处理的函数叫寄生函数,被依赖的叫宿主函数。

  宿主函数的实现一旦改变,寄生函数就不能正常工作。

  Eg. ,FIG(FORTH Interest Group)公布的FORTH-77中有CMOVE, FILL等函数。如果用CMOVE实现FILL,则FILL就是寄生函数。如果CMOVE实现为一次拷贝4个字节,则FILL就失败。

  /* CMOVE ─── 用头到头的移动来转移存储 */

  void CMOVE (byte *pbFrom,byte *pbTo,size_t size)

  {

  while(size-- > 0 )

  *pbTo++ = *pbFrom++;

  }

  /* FILL 填充某一存储域 */

  void FILL (byte *pb,size_t size,byte b)

  {

  if(size>0)

  {

  *pb = b;

  CMOVE(pb,pb+1,size-1);

  }

  }

  7.7 不要滥用程序设计语言

  用一把螺丝刀来播开油漆罐的盖子,然后又用这把螺丝刀来搅拌油漆――这并不是正确的做法,之所以这样做是因为当时这样很方便,而且能够解决问题。

  程序设计语言也是如此。

  Eg. 不要将比较的结果作为计算表达式的一部分

  另外标准也会变。Eg. Forth-77和Forth-83中的布尔值定义

  7.8 紧凑的C 代码并不能保证得到高效的机器代码

  我的观点是:如果你总是使用稀奇古怪的表达式,以便把C 代码尽量写在源代码的一行上,从而达到最好的瑜伽状态的话,你很可能患有可怕的“一行清”(one-line-itis)疾病(也称为程序设计语言综合症)

  7.9 为一般水平的程序员编写代码

  8 剩下来的就是态度问题

  8.1 错误几乎不会“消失”

  错误消失有三个原因:一是错误报告不对;二是错误已被别的程序员改正了;三是这个错误依然存在但没有表现出来。

  8.2 马上修改错误,不要推迟到最后

  l 不要通过把改正错误移置产品开发周期的最后阶段来节省时间。修改一年前写的代

  码比修改几天前写的代码更难,实际上这是浪费时间。

  l “一次性”地修改错误会带来许多问题:早期发现的错误难以重现。

  l 错误是一种负反馈,程序开发倒是快了,却使程序员疏于检查。如果规定只有把错误全部改正之后才能增加新特征的话,那么在整个产品开发期间都可以避免程序员的疏漏,他们将忙于修改错误。反之,如果允许程序员略过错误,那就使管理失控。

  l 若把错误数保持在近乎于0 的数量上,就可以很容易地预言产品的完成时间。只需要估算一下完成 32 个特征所需的时间,而不需要估算完成32 个特征加上改正1742个错误所需的时间。更好的是,你总能处于可随时交出已开发特征的有利地位。

  8.3 修改错误要治本,不要治标

  8.4 除非关系产品的成败,否则不要整理代码

  整理代码的问题在于程序员总不把改进的代码作为新代码处理,导致测试不够

  8.5 不要实现没有战略意义的特征

  8.6 不设自由特征

  对于程序员来说,增加自由特征可能不费事,但是对于特征来讲,它不仅仅增多了代码,还必须有人为该特征写又档,还必须有人来测试它。不要忘记还必须有人来修改该特征可能出现的错误。

  8.7 不允许没有必要的灵活性

  Eg. realloc的参数

  8.8 在找到正确的解法之前,不要一味地“试”,要花时间寻求正确的解

  8.9 尽量编写和测试小块代码。即使测试代码会影响进度,也要坚持测试代码

  8.10 测试代码的责任不在测试员身上,而是程序员自己的责任

  开发人员和测试人员分别从内外开始测试,所以不是重复劳动。

  8.11 不要责怪测试员发现了你的错误

  8.12 建立自己优先级列表并坚持之

  约克的优先级列表

  吉尔的优先级列表

  正确性

  正确性

  全局效率

  可测试性

  大小

  全局效率

  局部效率

  可维护性/明晰性

  个人方便性

  一致性

  可维护性/明晰性

  大小

  个人表达方式

  局部效率

  可测试性

  个人表达方式

  一致性

  个人方便性

  8.13 你必须养成经常询问怎样编写代码的习惯。

  本书就是长期坚持询问一些简单问题所得的结果。

  l 我怎样才能自动检测出错误?

  l 我怎样才能防止错误?

  l 这种想法和习惯是帮助我编写无错代码呢还是妨碍了我编写无错代码?

  9 本书各章要点汇总

  书中每章结束时都小结了本章要点,这里汇总如下:

  9.1 假想的编译程序

  l 消除程序错误的最好方法是尽可能早、尽可能容易地发现错误,要寻求费力最小的自动查错方法。

  l 努力减少程序员查错所需的技巧。可以选择的编译程序或lint 警告设施并不要求程序员要有什么查错的技巧。在另一个极端,高级的编码方法虽然可以查出或减少错误,但它们也要求程序员要有较多的技巧,因为程序员必须学习这些高级的编码方法。

  9.2 自己设计并使用断言

  l 要同时维护交付和调试两个版本。封装交付的版本,应尽可能地使用调试版本进行自动查错。

  l 断言是进行调试检查的简单方法。要使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是在最终产品中必须处理的。

  l 使用断言对函数的参数进行确认,并且在程序员使用了无定义的特性时向程序员报警。函数定义得越严格,确认其参数就越容易。

  l 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了相应的假定,就要使用断言对所做的假定进行检验,或者重新编写代码去掉相应的假定。另外,还要问:“这个程序中最可能出错的是什么,怎样才能自动地查出相应的错误?”努力编写出能够尽早查出错误的测试程序。

  l 一般教科书都鼓励程序员进行防错性程序设计,但要记住这种编码风格会隐瞒错误。当进行防错性编码时如果“不可能发生”的情况确实发生了,要使用断言进行报警。

  9.3 为子系统设防

  l 考查所编写的子系统,问自己:“在什么样的情况下,程序员在使用这些子系统时会犯错误。”在子系统中加上相应的断言和确认检查代码,以捕捉难于发现的错误和常见的错误。

  l 如果不能使错误不断重现,就无法排除它们。找出程序中可能引起随机行为的因素,并将它们从程序的调试版本中清除。把目前尚“无定义”的内存单元置成了某个常量值,就可能产生这种错误。在这种情况下,如果程序在该单元被正确地定义为某个值之前引用了它的内容,那么每次执行这部分错误的代码,都会得到同样的错误结果。

  l 如果所编写的子系统释放内存(或者其它的资源),并因此产生了“ 无用信息”,那么要把它搅乱,使它真的像无用信息。否则,这些被释放了的数据就有可能仍被使用,而又不会被注意到。

  l 类似地,如果在所编写的子系统中某些事情可能发生,那么要为该子系统加上相应的调试代码,使这些事情一定发生。这样可以增大查出通常得不到执行的代码中的错误的可能性。

  l 尽力使所编写的测试代码甚至在程序员对其没有感觉的情况下亦能起作用。最好的测试代码是不用知道其存在也能起作用的测试代码。

  l 如果可能的话,把测试代码放到所编写的子系统中,而不要把它放到所编写子系统的外层。不要等到进行了系统编码时,才考虑其确认方法。在子系统设计的每一步,都要考虑“如何对这一实现进行详尽的确认”这一问题。如果发现这一设计难于测试或者不可能对其进行测试,那么要认真地考虑另一种不同的设计,即使这意味着用大小或速度作代价去换取该系统的测试能力也要这么做。

  l 在由于速度太慢或者占用的内存太多而抛弃一个确认测试程序之前,要三思而后行。切记,这些代码并不是存在于程序的交付版本中。如果发现自己正在想:“这个测试程序太慢、太大了”,那么要马上停下来问自己:“怎样才能保留这个测试程序,并使它既快又小?”

  9.4 对程序进行逐条跟踪

  l 代码中不会自己生出错误来,错误是程序员编写新代码或者修改现有代码的产物。如果你想发现代码中的错误,没有哪个办法比在对代码进行编译时对其进行逐条跟踪更好。

  l 虽然直观上你可能认为对代码进行走查会花费大量的时间,但这是不对的。刚开始进行代码的走查确实要多花一点时间,但当这一切习惯成自然之后并不会多花多少时间,你可以很快地走查一遍。

  l 一定要对每一条代码路径进行逐条的跟踪,至少要跟踪一遍,尤其是对代码中的错误处理部分。不要忘记 &&、|| 和?:这些运算符,它们每个都有两条代码路径需要进行测试。

  l 在某些情况下也许需要在汇编语言级对代码进行逐条的跟踪。尽管不必经常这样做,但在必要的时候不要回避这种做法。

  9.5 糖果机界面

  l 最容易使用和理解的函数界面,是其中每个输入和输出参数都只代表一种类型数据的界面。把错误值和其它的专用值混在函数的输入和输出参数中,只会搞乱函数的界面。

  l 设计函数的界面迫使程序员考虑所有重要细节(如错误情况的处理),不要使程序员能够很容易地忽视或者忘记有关的细节。

  l 老要想到程序员调用所编函数的方式,找出可能使程序员无意间引入错误的界面缺陷。尤其重要的是要争取编出永远成功的函数,使调用者不必进行相应的错误处理。

  l 为了增加程序的可理解性从而减少错误,要保证所编函数的调用能够被必须阅读这些调用的程序员所理解。莫明其妙的数字和布尔参数都与这一目标背道而驰,因此应该予以消除。

  l 分解多功能的函数。取更专门的函数名(如ShrinkMemory 而不是 realloc)不仅可以增进人们对程序的理解,而且使我们可以采用更加严格的断言自动地检查出调用错误。

  l 为了向程序员展示出所编函数的适当调用方法,要在函数的界面中通过注解的方式详细说明。要强调危险的方面。

  9.6 风险事业

  l 在选择数据类型的时候要谨慎。虽然ANSI 标准要求所有的执行程序都要支持char, int,long 等类型,但是它并没有具体定义这些类型。为了避免程序出错,应该只按照ANSI 的标准选择数据类型。

  l 由于代码可能会在不理想的硬件上运行,因此很可能算法是正确的而执行起来却有错。所以要经常详细检查计算结果和测试结果的数据类型范围是否上溢或下溢。

  l 在实现某个设计的时候,一定要严格按照设计去实现。如果在编写代码时只是近似地实现所提出的要求,那就很容易出错。

  l 每个函数应该只有一个严格定义的任务,不仅如此,完成每个任务也应只有一种途径。假如不管输入什么都能执行同样的代码,那就会大大降低那些不易被发现的错误所存在的概率。

  l if 语句是个警告信号,说明代码所做的工作可能比所需要的要多。努力消除代码中每一个不必要的if 语句,经常反问自己:“怎样改变设计从而删掉这个特殊情况?”有时可能要改变数据结构,有时又要改变一下考察问题的方式,就象透镜是凸的还是凹的问题一样。

  l 有时if 语句隐藏在while 和for 循环的控制表达式中。“?:”操作符是if 语句的另外一种形式。

  l 曾惕有风险的语言惯用语,注意那些相近但更安全的惯用语。特别要警惕那些看上去象是好编码的惯用语,因为这样的实现对总体效率很少有显著的影响,但却增加了额外的风险性。

  l 在写表达式时,尽量不要把不同类型的操作符混合起来,如果必须混合使用,用括号把它们分隔开来。

  l 特殊情况中的特殊情况是错误处理。如果有可能,应该尽量避免调用可能失败的函数,假如必须调用返回错误的函数,将错误处理局部化以便所有的错误都汇集到一点,这将增加在错误处理代码中发现错误的机会。

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

  9.7 编码中的假象

  l 如果你要用到的数据不是你自己所有的,那怕是临时的,也不要对其执行写操作。尽管你可能认为读数据总是安全的,但是要记住,从映射到I/O 的存储区读数据,可能会对硬件造成危害。

  l 每当释放了存储区人们还想引用它,但是要克制自己这么做。引用自由存储区极易引起错误。

  l 为了提高效率,向全局缓冲区或静态缓冲传递数据也是很吸引人的,但是这是一条充满风险的捷径。假若你写了一个函数,用来创建只给调用函数使用的数据,

  那么就将数据返回给调用函数,或保证不意外地更改这个数据。

  l 不要编写依赖支持函数的某个特殊实现的函数。我们已经看到,FILL 例程不该象给出的那样调用CMOVE,这种写法只能作为坏程序设计的例子。

  l 在进行程序设计的时候,要按照程序设计语言原来的本意清楚、准确地编写代码。避免使用有疑问的程序设计惯用语,即使语言标准恰好能保证它工作,也不要使用。请记住,标准也在改变。

  l 如果能用C 语言有效地表示某个概念,那么类似地,相应的机器代码也应该是有效的。逻辑上讲似乎应该是这样,可是事实上并非如此。因此在你将多行C代码压缩为一行代码之前,一定要弄清楚经过这样的更改以后,能否保证得到更好的机器代码。

  l 最后,不要象律师写合同那样来编写代码。如果一般水平的程序员不能阅读和理解你的代码,那就说明你的代码太复杂了,使用简单一点的语言。

  9.8 剩下来的就是态度问题

  l 错误既不会自己产生,也不会自己改正。如果你得到了一个错误报告,但这个错误不再出现了。不要假设测试员发生了幻觉,而要努力查找错误,甚至要恢复程序的老版本。

  l 不能“以后”再修改错误。这是许多产品被取消的共同教训。如果在你发现错误的时候就及时地更正了错误,那你的项目就不会遭受毁灭性的命运。当你的项目总是保持近似于0 个错误时,怎么可能会有一系列的错误呢?

  l 当你跟踪查到一个错误时,总要问一下自己,这个错误是否会是一个大错误的症状。当然,修改一个刚刚追踪到的症状很容易,但是要努力找到真正的起因。

  l 不要编写没有必要的代码。让你的竞争者去清理代码,去实现“冷门”但无价值的特征,去实现自由特征。让他们花大量的时间去修改由于这些无用代码所引起的所有没有必要的错误。

  l 记住灵活与容易使用并不是一回事。在你设计函数和特征时,重点是使之容易使用;如果它们仅仅是灵活的,象realloc 函数和Excel 中的彩色格式特征那样,那么就没法使得代码更加有用;相反地,使得发现错误变得更困难了。

  l 不要受“试一试”某个方案以达到预期结果的影响。相反,应把花在尝试方案上的时间用来寻找正确的解决方法。如果必要,与负责你操作系统的公司联系,这比提出一个在将来可能会出问题的古怪实现要好。

  l 代码写得尽量小以便于全面测试。在测试中不要马虎。记住,如果你不测试你的代码,就没有人会测试你的代码了。无论怎样,你也不要期望测试组为你测试代码。

  l 最后,确定你们小组的优先级顺序,并且遵循这个顺序。如果你是约克,而项目需要吉尔,那么至少在工作方面你必须改变习惯。

  10 本书附录A 编码检查表

  本附录给出的问题列表,总结了本书的所有观点。使用本表的最好办法是花两周时间评

  审一下你的设计和编码实现。先花几分钟时间看一看列表,一旦熟悉了这些问题,就可以灵

  活自如地按它写代码了。此时,就可以把表放在一边了。

  一般问题

  ── 你是否为程序建立了DEBUG 版本?

  ── 你是否将发现的错误及时改正了?

  ─一 你是否坚持彻底测试代码.即使耽误了进度也在所不惜?

  ── 你是否依靠测试组为你测试代码?

  ─一 你是否知道编码的优先顺序?

  ─一 你的编译程序是否有可选的各种警告?

  关于将更改归并到主程序

  ─一 你是否将编译程序的警告(包括可选的)都处理了?

  ── 你的代码是否未用Lint

  ─一 你的代码进行了单元测试吗?

  ─一 你是否逐步通过了每一条编码路径以观察数据流?

  ─一 你是否逐步通过了汇编语言层次上的所有关键代码?

  ── 是否清理过了任何代码?如果是,修改处经过彻底测试了吗?

  ─一 文档是否指出了使用你的代码有危险之处?

  ── 程序维护人员是否能够理解你的代码?

  每当实现了一个函数或子系统之时

  ─一 是否用断言证实了函数参数的有效性?

  ─一 代码中是否有未定义的或者无意义的代码?

  ─一 代码能否创建未定义的数据?

  ─一 有没有难以理解的断言?对它们作解释了没有?

  ─一 你在代码中是否作过任何假设?

  ─一 是否使用断言警告可能出现的非常情况?

  ─一 是否作过防御性程序设计?代码是否隐藏了错误?

  ─一 是否用第二个算法来验证第一个算法?

  ─一 是否有可用于确认代码或数据的启动(startup)检查?

  ─一 代码是否包含了随机行为?能消除这些行为吗?

  ── 你的代码若产生了无用信息,你是否在DEBUG 代码中也把它们置为无用信息?

  ── 代码中是否有稀奇古怪的行为?

  ── 若代码是子系统的一部分,那么你是否建立了一个子系统测试?

  ── 在你的设计和代码中是否有任意情况?

  ── 即使程序员不感到需要,你也作完整性检查吗?

  ── 你是否因为排错程序太大或太慢,而将有价值的DEBUG 测试抛置一边?

  ── 是否使用了不可移植的数据类型?

  ─一 代码中是否有变量或表达式产生上溢或下溢?

  ── 是否准确地实现了你的设计?还是非常近似地实现了你的设计?

  ── 代码是否不止一次地解同一个问题?

  ── 是否企图消除代码中的每一个if 语句?

  ── 是否用过嵌套?:运算符?

  ── 是否已将专用代码孤立出来?

  ── 是否用到了有风险的语言惯用语?

  ─一 是否不必要地将不同类型的运算符混用?

  ── 是否调用了返回错误的函数?你能消除这种调用吗?

  ─一 是否引用了尚未分配的存储空间?

  ─一 是否引用已经释放了的存储空间?

  ── 是否不必要地多用了输出缓冲存储?

  ── 是否向静态或全局缓冲区传送了数据?

  ── 你的函数是否依赖于另一个函数的内部细节?

  ── 是否使用了怪异的或有疑问的C 惯用语?

  ── 在代码中是否有挤在一行的毛病?

  ── 代码有不必要的灵活性吗?你能消除它们吗?

  ─一 你的代码是经过多次“试着”求解的结果吗?

  ─一 函数是否小并容易测试?

  每当设计了一个函数或子系统后

  ─一 此特征是否符合产品的市场策略?

  ─一 错误代码是否作为正常返回值的特殊情况而隐藏起来?

  ─一 是否评审了你的界面,它能保证难于出现误操作吗?

  ─一 是否具有多用途且面面俱到的函数?

  ─一 你是否有太灵活的(空空洞洞的)函数参数?

  ─一 当你的函数不再需要时,它是否返回一个错误条件?

  ─一 在调用点你的函数是出易读?

  ─一 你的函数是否有布尔量输入?

  修改错误之时

  ── 错误无法消失,是否能找到错误的根源?

  ─一 是修改了错误的真正根源,还是仅仅修改了错误的症状?

  转载自:http://blog.csdn.net/hopesophite/archive/2006/08/02/1010643.aspx

  《编程精粹》读后感(三):一本肝肠断

  一本谭浩强的《C语言程序设计》将一个少年引入了奇妙的C语言世界,从此为之疯狂。

  之后接触的《C和指针》、《C陷阱与缺陷》、《C程序设计语言》。。。无一不是C语言世界里的巨作。但是,这本《Write Solid Code》实在是令人震撼,与《程序设计实践》一样,作为一个程序员,您不得不看!

  这书并没有教你如何写代码,而是如何写正确的Bug-free的代码。本以为C语言标准库中的代码无懈可击,但是书中却对relloc函数大加批评,如此酣畅,如此淋漓。确实,这是一个很risky的函数。回头看以前自己写的代码,心中不免一阵厌恶,简直就是rubbish!。而更令自己恶心的是,对这些rubbish还觉得可以,无知真是相当的可怕。

  《Write Solid Code》,一本肝肠断呐。

  《编程精粹》读后感(四):用这些朴实的习惯写出易排错的代码。。。

  都是一些比较实用的习惯,能让程序的健壮性更强。

  1. 断言真的是很实用,能查出一些意外的bug。这点感触比较深,插入一些必要的断言,这样不至于在程序执行N久以后才恍然发现在前面N远处一个参数什么的传错了。,而且几乎不会影响性能。关于断言的使用也有一些注意事项(在《卓有成效的程序员》等书里都有提到),比如:assert(getchar() == 'c');这个断言就影响了程序本身。

  2. 对程序进行逐条跟踪。正如作者所说,有可能在逐条跟踪的时候觉得是浪费时间,但这能对这段代码的正确性有很好的把握(至少不会出现提交程序后别人再check 下来的居然编译都不过!),减少不少以后调bug的时间。

  3.要注意程序语言里未定义的行为,如对于c里的函数memcpy,当dest~dest+size和 src~ src+size 这两段区间如果有交集的话,结果是未知的。

  4. 边写代码边测试。(或者现在流行的测试先行?)

  5. 关于程序员和测试员。"程序员测试代码,是从里向外测试,而测试员则是从外向里测试。" 程序员对自己的代码更了解,所以自己通过多加断言等方式测试自己的代码是很有必要的。

  6. ......

  .......

  当然这很多的习惯都要在编码中才能有更好的体会。"临渊羡鱼,不如退而结网"。

  《编程精粹》读后感(五):【Markdown】笔记,大部分有价值的点

  避免低级错误

  ---------------

  1. `if(flight == 063)`。这里程序员的本意是对63 号航班进行测试,但因为前面多了一个0 使063 成了八进制数。结果变成对51 号航班进行测试。

  2. `If(pb != NULL & pb != 0xff)`。这里不小心把&&键入为&,结果即使pb 等于NULL 还会执行*pb != 0xff。

  3. `quot = numer/*pdenom`。这里无意间多了个*号结果使`/*`被解释为注释的开始。

  4. `word = bHigh<<8 + bLow`。由于出现了运算优先级错误,该语句被解释成了:`word = bHigh << (8+bLow)`

  5. C 的预处理程序也可能引起某些意想不到的结果。例如,宏UINT_MAX 定义在limit.h中,但假如在程序中忘了include 这个头文件,下面的伪指令就会无声无息地失败,因为预处理程序会把预定义的UINT_MAX 替换成0:

  ```

  ……

  #if UINT_MAX > 65535u

  ……

  #endif

  ```

  低级错误 详细例子

  ---------------

  1. 这样一个代码:`strCopy = memcpy(malloc(length), str, length);`

  该语句在多数情况下都会工作得很好,除非malloc 的调用产生失败。当malloc 失败时,

  就会给memcpy 返回一个NULL 指针。由于memcpy 处理不了NULL 指针,所以出现了错误。如

  果你很走运,在交付之前这个错误导致程序的瘫痪,从而暴露出来。但是如果你不走运,没

  有及时地发现这个错误,那某位顾客就一定会“走运”了。

  编译程序查不出这种或其他类似的错误。同样,编译程序也查不出算法的错误,无法验

  证程序员所作的假定。或者更一般地,编译程序也查不出所传递的参数是否有效。

  寻找这种错误非常艰苦,只有技术非常高的程序员或者测试者才能将它们根除并且不会

  引起其他的问题。

  然而假如你知道应该怎样去做的话,自动寻找这种错误就变得很容易了。

  让我们直接进入memcpy,看看怎样才能查出上面的错误。最初的解决办法是使memcpy

  对NULL 指针进行检查,如果指针为NULL,就给出一条错误信息,并中止memcpy 的执行。

  下面是这种解法对应的程序。

  ```

  /* memcpy ─── 拷贝不重叠的内存块 */

  void memcpy(void* pvTo, void* pvFrom, size_t size)

  {

  void* pbTo = (byte*)pvTo;

  void* pbFrom = (byte*)pvFrom;

  if(pvTo == NULL | | pvFrom == NULL)

  {

  fprintf(stderr, “Bad args in memcpyn”);

  abort();

  }

  while(size-->0)

  *pbTo++ == *pbFrom++;

  return(pvTo);

  }

  ```

  只要调用时错用了NULL 指针,这个函数就会查出来。所存在的唯一问题是其中的测试

  代码使整个函数的大小增加了一倍,并且降低了该函数的执行速度。如果说这是“越治病越

  糟”,确实有理,因为它一点不实用。要解决这个问题需要利用C 的预处理程序。

  如果保存两个版本怎么样?一个整洁快速用于程序的交付;另一个臃肿缓慢件(因为包

  括了额外的检查),用于调试。这样就得同时维护同一程序的两个版本,并利用C 的预处理

  程序有条件地包含或不包含相应的检查部分。

  ```

  #ifdef DEBUG

  void _Assert(char* , unsigned); /* 原型 */

  #define ASSERT(f)

  if(f)

  ULL;

  else

  _Assert(__FILE__ , __LINE__)

  #else

  #define ASSERT(f) NULL

  #endif

  ```

  2. 如果读者停下来读读 ANSI C 中memcpy 函数的定义,就会看到其最后一行说:“如果在

  存储空间相互重叠的对象之间进行了拷贝,其结果无定义

  `/* 内存块重叠吗?如果重叠,就使用memmove */`

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

  3. 消除所做的隐式假定

  最近,Microsoft 的一些小组渐渐发现他们不得不对其代码进行重新的考察和整理,因

  为相当多的代码中充满了“+2”而不是“+sizeof(int)”、与0xFFFF 而不是UINT_MAX 进行

  无符号数的比较、在数据结构中使用的是int 而不是真正想用的16 位数据类型这一类问题。

  你也许会认为这是因为这些程序员太懒散,但他们却不会同意这一看法。事实上,他们

  认为有很好的理由说明他们可以安全地使用“+2”这种形式,即相应的C 编译程序是由

  Microsoft 自己编写的。这一点给程序员造成了安全的假象,正如几年前一位程序员所说:

  “编译程序组从来没有做使我们所有程序垮掉的改变”。

  但这位程序员错了。

  为了在Intel 80386 和更新的处理器上生成更快更小的程序,编译程序组改变了int

  的大小(以及其他一些方面)。虽然编译程序组并不想使公司内部的代码垮掉,但是保持在

  市场上的竞争地位显然更重要。毕竟,这是那些自己做了错误假定的Microsoft 程序员的过

  错。

  4. 为某一数据结构分配内存时, 忘了初始化, 或当维护人员扩展该数据结构时, 忘了为新增的域编写相应的初始化代码.

  不管怎样,我们还是不希望所分配内存块的内容无定义,因为这样会使错误难以再现。

  那么如果只有当所分配内存块中的无用信息碰巧是某个特定值时才出错,会产生什么样的结

  果呢?这就会在大部分的时间内发现不了错误,而程序却会由于不明显的原因不断地失败、

  我们可以想象一下,如果每个错误都是在某个特定的时刻才发生,要排除程序中的所有错误

  会多难。要是这样,程序(和测试人员)非发疯不可。暴露错误的关键是消除错误发生的随

  机性。

  确实,如何做到这一点要取决于具体的子系统及其所涉及到的随机特性。但对于malloc

  来说,通过对其所分配的内存块进行填充,就可以消除其随机性。当然,这种填充只应该用

  在程序的调试版本中。这样既可以解决问题,又不影响程序的发行代码。然而必须记住,我

  们不希望隐瞒错误,所以用来填充内存块的值应该离奇得看起来象是无用的信息,但又应该

  能够使错误暴露。

  ```

  #define bGarbage 0xA3

  flag fNewMemory(void** ppv, size_t size)

  {

  yte** ppb = (byte**)ppv;

  ASSERT(ppv!=NULL && size!=0);

  *ppb = (byte*)malloc(size);

  #ifdef DEBUG

  {

  if( *ppb != NULL )

  memset(*ppb, bGarbage, size);

  }

  #endif

  return(*ppb != NULL);

  }

  ```

  5. 如果释放内存之后,内存指针未清除呢?

  ```

  void FreeMemory(void* pv)

  {

  ASSERT(pv != NULL);

  #ifdef DEBUG

  {

  memset(pv, bGarbage, sizeofBlock(pv) );

  }

  #endif

  free(pv);

  }

  ```

  6. 你可能有用过realloc?

  realloc 改变先前已分配的内存块的大小,该内存块的原有内容从该块的开始位置到新

  块和老块长度的最小长度之间得到保留。

  ?? 如果该内存块的新长度小于老长度,realloc 释放该块尾部不再想要的内存空间,

  返回的pv 不变。

  ?? 如果该内存块的新长度大于老长度,扩大后的内存块有可能被分配到新的地址处,

  该块的原有内容被拷贝到新的位置。返回的指针指向扩大后的内存块,并且该块扩

  大部分的内容未经初始化。

  ?? 如果满足不了扩大内存块的请求,realloc 返回NULL,当缩小内存块时,realloc

  总会成功。

  ?? 如果 pv 为NULL,那么realloc 的作用相当于调用malloc(size),并返回指向新分

  配内存块的指针,或者在该请求无法满足时返回NULL。

  ?? 如果 pv 不是NULL,但新的块长为零,那么realloc 的作用相当于调用free(pv)

  并且总是返回NULL。

  ?? 如果 pv 为NULL 且当前的内存块长为零,结果无定义

  7.dummy header 链表

  8. memchr是否存在问题

  ```

  void* memchr( void *pv, unsigned char ch, size_t size )

  {

  unsigned char *pch = ( unsigned char * )pv;

  unsigned char *pchEnd = pch + size;

  while( pch < pchEnd )

  {

  if( *pch == ch )

  return ( pch );

  ch ++ ;

  }

  return( NULL );

  }

  ```

  注意:

  ```

  chEnd = pch + size;

  while( pch < pchEnd )

  ```

  正确做法

  ```

  void *memchr( void *pv, unsigned char ch, size_t size )

  {

  unsigned char *pch = ( unsigned char * )pv;

  while( size -- > 0 )

  {

  if( *pch == ch )

  return( pch );

  ch ++;

  }

  return( NULL );

  }

  ```

  9. 它们有什么不同?

  char chGetNext(void)

  {

  int ch; /* ch“必须”是int类型 */

  ch = getchar();

  return(chRemapChar(ch));

  }

  char chGetNext(void)

  {

  return( chRemapChar(getchar()) );

  }

  《编程精粹》读后感(六):原书应该是1993年出版的。对这样一本书,应该既批判又继承的加以消化。

  写明是人民邮电出版社2009年出版的,但我怀疑原书应该是1993年的——差不多20多年前的一本书。我到www.amazon.com上搜索writing solid code,只找到一本,正是1993年的。

  那么问题就来了,今天读这本书,是不是要用一些批判的眼光:毕竟经过近20年的发展,编程的方方面面也发展了一些显著或微妙的变化。批判的同时,汲取其中对于编程一般规律的阐释。

  这就是我在开始阅读它之前的一些简单思考,也希望能提醒广大书友。因为读书本身并不是目的,吸收、借鉴书中有用的观点并拿来指导实践才是读书的初衷。

  《编程精粹》读后感(七):《Writing clean code》读书笔记

  写在前面的话:

  这两天看了《Writing Clean Code》,很受启发,感觉值得再读,于是整理了一点笔记,作为checklist,以备速查。

  原书共8章,每章都举一些例子,指出不足,再用通用的规则改写,每章结束时会总结一下要点,其中覆盖了比较重要的规则。附录A是作者整理的编码检查表。本笔记前8章和原书前8章对应,列出了所有的规则,对比较特别或者比较难理解的规则还附上了书中的例子,偶尔加一两句个人的想法。第9章是原书各章末尾要点的汇总。第10章是原书的编码检查表。

  本笔记只作为原书的一个速查手册,详细的内容请看原书。

  中译本:

  《编程精粹 ─── Microsoft 编写优质无错C 程序秘诀》Steve Maguire 著,姜静波 佟金荣 译,麦中凡 校,电子工业出版社

  英文版:

  《Writing Clean Code ── Microsoft Techniques for Developing Bug-free C Programs》Steve maguire, Microsoft Press

  英文版原名:

  《Writing Solid Code ── Microsoft Techniques for Developing Bug-free C Programs》Steve maguire, Microsoft Press

  1 假想的编译程序

  1.1 使用编译程序所有的可选警告设施

  1.2 使用lint 来查出编译程序漏掉的错误

  1.3 如果有单元测试,就进行单元测试

  1.4 Tips

  C 的预处理程序也可能引起某些意想不到的结果。例如,宏UINT_MAX 定义在limit.h

  中,但假如在程序中忘了include 这个头文件,下面的伪指令就会无声无息地失败,

  因为预处理程序会把预定义的UINT_MAX 替换成0:

  #if UINT_MAX > 65535u

  …

  #endif

  怎样使预处理程序报告出这一错误?

  2 构造自己的断言

  2.1 既要维护程序的交付版本,又要维护程序的调试版本

  少用预处理程序,那样会喧宾夺主,尝试用断言

  2.2 断言是进行调试检查的简单方法。要使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是在最终产品中必须处理的。

  这是断言和错误处理的区别

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

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

  这个对C/C++很适用

  2.5 不要浪费别人的时间 ─── 详细说明不清楚的断言

  森林中只标有“危险”,而没指出具体是什么危险的指示牌将会被忽略。

  2.6 断言不是用来检查错误的

  当程序员刚开始使用断言时,有时会错误地利用断言去检查真正地错误,而不去检查非

  法的况。看看在下面的函数strdup 中的两个断言:

  char* strdup(char* str)

  {

  char* strNew;

  ASSERT(str != NULL);

  trNew = (char*)malloc(strlen(str)+1);

  ASSERT(strNew != NULL);

  trcpy(strNew, str);

  return(strNew);

  }

  第一个断言的用法是正确的,因为它被用来检查在该程序正常工作时绝不应该发生的非

  法情况。第二个断言的用法相当不同,它所测试的是错误情况,是在其最终产品中肯定会出

  现并且必须对其进行处理的错误情况。

  2.7 用断言消除所做的隐式假定,或者利用断言检查其正确性

  Eg. 对于和机器相关的内存填充程序,不必也无法将其写成可移植的。可以用条件编译。但其中应该对某种机器的隐含假设做检查。

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

  压缩程序的例子:正常情况和特殊情况,重复次数>=4或者就等于1

  2.9 在进行防错性程序设计时,不要隐瞒错误

  2.10 要利用不同的算法对程序的结果进行确认

  2.11 不要等待错误发生,要使用初始检查程序

  2.12 Tips

  不要把真正需要执行的语句放在断言里

  3 为子系统设防

  3.1 要消除随机特性 ─── 使错误可再现

  3.2 冲掉无用的信息,以免被错误地使用

  分配内存时填充上非法值:eg. 68000 用0xA3,Intel X86系列用0xCC

  释放内存时立刻填上非法值

  引申:这个和《代码大全》中讲的进攻式编程观点类似

  3.3 如果某件事甚少发生的话,设法使其经常发生

  eg. 让realloc函数中移动内存块这种比较少发生的事情经常发生--自己包装一个relloc。

  3.4 保存调试信息到日志,以便进行更强的错误检查

  这里的日志信息相当于一个簿记功能的信息,写到内存链表中。

  168代码有错:

  if( pbiPrev == NULL )

  iHead = pbi->pbiHead;

  3.5 建立详尽的子系统检查并且经常地进行这些检查--调试检查

  eg。利用簿记和‘是否被引用’的标志,检查是否有内存泄漏和悬挂指针

  3.6 仔细设计程序的测试代码,任何选择都应该经过考虑

  eg. 先后顺序是有讲究的:先看500元的套装,再看80元的毛衣

  3.7 努力做到透明的一致性检查

  不要影响代码的使用者的使用方式

  3.8 不要把对交付版本的约束应用到相应的调试版本上

  要用大小和速度来换取错误检查能力

  3.9 每一个设计都要考虑如何确认正确性

  如果可能的话,把测试代码放到所编写的子系统中,而不要把它放到所编写子系统的外层。不要等到进行了系统编码时,才考虑其确认方法。在子系统设计的每一步,都要考虑“如何对这一实现进行详尽的确认”这一问题。

  引申:回忆高中时检查结果:如果是解方程,则代入数值验算就可;如果是计算题,换一个方法再算一遍。总之,要有方法确认其正确性。

  3.10 “调试代码时附加了额外信息的代码,而不是不同的代码”

  加调试代码时要保证产品代码一定也要运行,这样才能测试到真正的产品代码。

  3.11 在自己包装的内存函数中加上允许注入错误的机制。

  eg. 定义一个failure结构,在NewMemory中测试这个结构,如果为真,则返回false,表示内存分配失败。

  这样,开发者和测试者都能利用这个机制,人为的注入错误。

  4 对程序进行逐条跟踪

  4.1 代码中不会自己生出错误来,错误是程序员编写新代码或者修改现有代码的产物。如果你想发现代码中的错误,没有哪个办法比在对代码进行编译时对其进行逐条跟踪更好。

  这个如果用个“完美的”编译器就更好。

  4.2 不要等到出了错误再对程序进行逐条的跟踪

  而是把对程序逐条跟踪看成是一个必要的过程。这可以保证程序按你预想的方式工作。

  引申:可以和代码走查结合在一起。或者先进行代码走查,再逐条跟踪,共两遍检查代码。

  4.3 对每一条代码路径进行逐条的跟踪

  注意覆盖率问题:语句覆盖or分支覆盖

  4.4 当对代码进行逐条跟踪时,要密切注视数据流

  这样有助于发现以下错误:

  上溢和下溢错误;

  数据转换错误;

  差1 错误;

  ULL 指针错误;

  使用废料内存单元错误(0xA3 类错误);

  用 = 代替 == 的赋值错误;

  运算优先级错误;

  逻辑错误。

  4.5 源级调试程序可能会隐瞒执行的细节,对关键部分的代码要进行汇编指令级的逐条跟综

  对条件语句的各个子条件,不要一次越过,而要看每个子条件的值。

  5 糖果机界面

  作者以糖果机的糟糕的界面设计导致人犯错讲起,阐述界面设计应该指导程序员少犯错误。

  5.1 要使用户不容易忽视错误情况,不要在正常地返回值中隐藏错误代码

  作者以getchar函数为例:这个函数返回一个char或者是-1,由此要求使用getchar的程序员必须用int来接收getchar的返回值,但肯定会有很多程序员忘记这一点,由此可能会引发难以捕捉的错误。

  作者设计了另一个函数界面来处理这种情况:int fGetChar(char*),返回值存入char*所指位置,而int返回flag,为true表示正确。这样,由于划分了正常的返回值和错误代码,避免了getchar的返回值要用int接收的问题。

  5.2 要不遗余力地寻找并消除函数界面中的缺陷

  Eg. 下述代码隐含着一个错误

  uf = (byte*)realloc( pbBuf, sizeNew );

  if( pbBuf != NULL )

  使用初始化这个更大的缓冲区

  如果realloc分配内存时失败,返回NULL,则pbBuf为NULL,它原来指向的内存将会丢失。

  如果界面是flag fResizeMemory( void** ppv, size _t sizeNew )则好得多

  5.3 不要编写多种功能集于一身的函数,为了对参数进行更强的确认,要编写功能单一的函数

  以realloc为例,它接受的指针为NULL但size大于0时相当于malloc,指针不为NULL但size为0时相当于free。这样realloc就混杂了malloc和free的功能,极其容易出错。

  5.4 不要模棱两可,要明确地定义函数的参数

  像realloc那样灵活的参数不一定很好,要考虑程序员给出这样的输入参数可能是出于什么原因,如果没有充分的理由,用断言来禁止太灵活的输入能减少错误。

  5.5 返回值与错误处理:编写函数使其在给定有效的输入情况下不会失败

  返回错误码不是唯一的处理错误的方式。Eg. Tolower函数在遇到输入是小写字母时,应该怎么办?

  如果返回-1,那么将遇到和getchar相同的问题:程序员要用int来存储tolower的返回值。此时,tolower返回原字符也许是一个更好的方式。

  5.6 使程序在调用点明了易懂:要避免布尔参数

  通过检查调用代码,检验界面设计的合理性。

  Eg. 以下两个函数声明会导致调用方式的不同:

  void UnsignedToStr(unsigned u, char *strResult, flag fDecimal);

  void UnsignedToStr(unsigned u, char* str, unsigned base);

  前者的调用方式是:

  UnsignedToStr(u, str, TRUE);

  UnsignedToStr(u, str, FALSE);

  这显然不好。而后者是UnsignedToStr(u, str, BASE10)则好的多。

  5.7 编写注解突出可能的异常情况

  用注释写出常见的错误用法和正确用法的例子。

  5.8 小结

  本章先给出一个界面不好的例子,再给出一般原则:要不遗余力的检查界面的合理性。然后讲功能要单一,输入要有限制,输出的正常返回值要与错误码分开,用调用方式检查界面,用注释来指出异常情况。

  6 风险事业

  6.1 使用有严格定义的数据类型

  可移植类型最值得注意之处是:它们只考虑了三种最通用的数制:壹的补码、贰的补码

  和有符号的数值。

  Char只有0~127吗是可移植的

  Unsigned char 是0~255,但signed char是-127~127 (没有-128吗)是可移植的

  6.2 经常反问:“这个变量表达式会上溢或下溢吗?”

  Eg. 以下代码会导致无穷循环,因为ch会上溢为0,导致不可能大于UCHAR_MAX。

  unsigned char ch;

  /* 首先将每个字符置为它自己 */

  for (ch=0; ch <= UCHAR_MAX;ch++)

  chToLower[ch] = ch;

  eg. 以下代码会下溢,导致无穷循环,因为size_t是无符号型,不可能小于0

  ize_t size = 100;

  while (--size >= 0)

  ULL;

  6.3 尽可能精确地实现设计,近似地实现设计就可能出错

  6.4 一个“任务”应只实现一次(Implement "the task" just once).

  一个原则:Strive to make everyfunction perform its task exactly

  one time

  tatic window * pwndRootChildren = NULL;

  void AddChild( window * pwndParent, window * pwndNewBorn )

  {

  /* 新窗口可能只有子窗口 ⋯ */

  ASSERT( pwndNewBorn->pwndSibling == NULL );

  if( pwndParent == NULL )

  {

  /* 将窗口加入到顶层根列表 */

  wndNewBorn->pwndSibling = pwndRootChildren;

  wndRootChildren = pwndNewBorn;

  }

  else

  {

  /* 如果是父母的第一个孩子,那么开始一个链,

  * 否则加到现存兄弟链的末尾处

  */

  if( pwndParent -> pwndChild == NULL )

  wndParent -> pwndChild = pwndNewBorn;

  else

  {

  window *pwnd = pwndParent -> pwndChild;

  while( pwnd -> pwndSibling != NULL)

  wnd = pwnd -> pwndSibling;

  wnd -> pwndSibling = pwndNewBorn;

  }

  }

  }

  .

  假如AddChild 是一个任务,要在现有窗口中增加子窗口,而上面的代码具有三个单独的插入过程。常识告诉我们如果有三段代码而不是一段代码来完成一个任务,则很可能有错。这往往意味着这个实现中有例外情况。

  其最终的改进见下一节。

  6.5 避免无关紧要地if 语句

  以指针为中心的树的构建,可以不必为特殊情况编写代码:

  void AddChild(window* pwndParent, window* pwndNewBorn )

  {

  window **ppwindNext;

  /* 新窗口可能没有兄弟窗口 ? */

  ASSERT( pwndNewBorn -> pwndSibling == NULL );

  /* 使用以指针为中心的算法

  * 设置ppwndNext 指向pwndParent -> pwndChild

  * 因为pwndParent -> pwndChild 是链中第一个“下一个兄弟指针”

  一个“任务”应只实现一次

  */

  wndNext = &pwndParent->pwndChild;

  while( *ppwndNext != NULL )

  wndNext = &( *ppwndNext )->pwndSibling;

  *ppwndNext = pwndNewBorn;

  }

  由于没有无关的if语句,使所有的程序都会经过同样的路径,因此这段代码就会被测试的很充分。

  6.6 避免使用嵌套的“?:“运算符

  重新整理思路,甚至用查表法,都能简化过程。

  6.7 每种特殊情况只能处理一次

  不要让处理同一个特殊情况的代码散布在多个地方

  6.8 避免使用有风险的语言惯用语

  这里举了好几个例子。

  Eg. pchEnd = pch + size;

  while( pch < pchEnd )

  ULL;

  如果pchEnd恰好查找到存储器的结尾处,那么所指的位置就不存在了

  Eg. 除以2和移位:移位有风险

  Eg. while (--size >= 0) 和while(size-- > 0),前者有风险,后者却没有。

  6.9 不能毫无必要地将不同类型地操作符混合使用,如果必须将不同类型地操作符混合使用,就用括号把它们隔离开来

  6.10 避免调用返回错误的函数(Avoid calling functions that return errors)

  这样,就不会错误地处理或漏掉由其它人设计的函数所返回的错误条件。

  如果自始至终程序反复处理同样的错误条件,就将错误处理部分独立出来。Eg. 单独的错误处理子程序。

  有时更好的方法是使错误根本不会发生。Eg. 窗口的rename函数可能要realloc,从而导致失败,但通过分配超额的内存空间(都取名字长度的最大值),则这个使错误不会出现,从而避免了错误处理的代码。

  7 编码中的假象

  7.1 只引用属于你自己的存储空间

  7.2 不能引用已释放的存储区

  7.3 只有系统才能拥有空闲的存储区,程序员不能拥有

  决不要使用free以后的内存

  7.4 不要把输出内存用作工作区缓存

  Don't use output memory as workspace buffers.

  7.5 不要利用静态(或全局)量存储区传递数据

  7.6 不要写寄生函数

  依赖于别的函数内部处理的函数叫寄生函数,被依赖的叫宿主函数。

  宿主函数的实现一旦改变,寄生函数就不能正常工作。

  Eg. ,FIG(FORTH Interest Group)公布的FORTH-77中有CMOVE, FILL等函数。如果用CMOVE实现FILL,则FILL就是寄生函数。如果CMOVE实现为一次拷贝4个字节,则FILL就失败。

  /* CMOVE ─── 用头到头的移动来转移存储 */

  void CMOVE (byte *pbFrom,byte *pbTo,size_t size)

  {

  while(size-- > 0 )

  *pbTo++ = *pbFrom++;

  }

  /* FILL 填充某一存储域 */

  void FILL (byte *pb,size_t size,byte b)

  {

  if(size>0)

  {

  *pb = b;

  CMOVE(pb,pb+1,size-1);

  }

  }

  7.7 不要滥用程序设计语言

  用一把螺丝刀来播开油漆罐的盖子,然后又用这把螺丝刀来搅拌油漆――这并不是正确的做法,之所以这样做是因为当时这样很方便,而且能够解决问题。

  程序设计语言也是如此。

  Eg. 不要将比较的结果作为计算表达式的一部分

  另外标准也会变。Eg. Forth-77和Forth-83中的布尔值定义

  7.8 紧凑的C 代码并不能保证得到高效的机器代码

  我的观点是:如果你总是使用稀奇古怪的表达式,以便把C 代码尽量写在源代码的一行上,从而达到最好的瑜伽状态的话,你很可能患有可怕的“一行清”(one-line-itis)疾病(也称为程序设计语言综合症)

  7.9 为一般水平的程序员编写代码

  8 剩下来的就是态度问题

  8.1 错误几乎不会“消失”

  错误消失有三个原因:一是错误报告不对;二是错误已被别的程序员改正了;三是这个错误依然存在但没有表现出来。

  8.2 马上修改错误,不要推迟到最后

  l 不要通过把改正错误移置产品开发周期的最后阶段来节省时间。修改一年前写的代

  码比修改几天前写的代码更难,实际上这是浪费时间。

  l “一次性”地修改错误会带来许多问题:早期发现的错误难以重现。

  l 错误是一种负反馈,程序开发倒是快了,却使程序员疏于检查。如果规定只有把错误全部改正之后才能增加新特征的话,那么在整个产品开发期间都可以避免程序员的疏漏,他们将忙于修改错误。反之,如果允许程序员略过错误,那就使管理失控。

  l 若把错误数保持在近乎于0 的数量上,就可以很容易地预言产品的完成时间。只需要估算一下完成 32 个特征所需的时间,而不需要估算完成32 个特征加上改正1742个错误所需的时间。更好的是,你总能处于可随时交出已开发特征的有利地位。

  8.3 修改错误要治本,不要治标

  8.4 除非关系产品的成败,否则不要整理代码

  整理代码的问题在于程序员总不把改进的代码作为新代码处理,导致测试不够

  8.5 不要实现没有战略意义的特征

  8.6 不设自由特征

  对于程序员来说,增加自由特征可能不费事,但是对于特征来讲,它不仅仅增多了代码,还必须有人为该特征写又档,还必须有人来测试它。不要忘记还必须有人来修改该特征可能出现的错误。

  8.7 不允许没有必要的灵活性

  Eg. realloc的参数

  8.8 在找到正确的解法之前,不要一味地“试”,要花时间寻求正确的解

  8.9 尽量编写和测试小块代码。即使测试代码会影响进度,也要坚持测试代码

  8.10 测试代码的责任不在测试员身上,而是程序员自己的责任

  开发人员和测试人员分别从内外开始测试,所以不是重复劳动。

  8.11 不要责怪测试员发现了你的错误

  8.12 建立自己优先级列表并坚持之

  约克的优先级列表

  吉尔的优先级列表

  正确性

  正确性

  全局效率

  可测试性

  大小

  全局效率

  局部效率

  可维护性/明晰性

  个人方便性

  一致性

  可维护性/明晰性

  大小

  个人表达方式

  局部效率

  可测试性

  个人表达方式

  一致性

  个人方便性

  8.13 你必须养成经常询问怎样编写代码的习惯。

  本书就是长期坚持询问一些简单问题所得的结果。

  l 我怎样才能自动检测出错误?

  l 我怎样才能防止错误?

  l 这种想法和习惯是帮助我编写无错代码呢还是妨碍了我编写无错代码?

  9 本书各章要点汇总

  书中每章结束时都小结了本章要点,这里汇总如下:

  9.1 假想的编译程序

  l 消除程序错误的最好方法是尽可能早、尽可能容易地发现错误,要寻求费力最小的自动查错方法。

  l 努力减少程序员查错所需的技巧。可以选择的编译程序或lint 警告设施并不要求程序员要有什么查错的技巧。在另一个极端,高级的编码方法虽然可以查出或减少错误,但它们也要求程序员要有较多的技巧,因为程序员必须学习这些高级的编码方法。

  9.2 自己设计并使用断言

  l 要同时维护交付和调试两个版本。封装交付的版本,应尽可能地使用调试版本进行自动查错。

  l 断言是进行调试检查的简单方法。要使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是在最终产品中必须处理的。

  l 使用断言对函数的参数进行确认,并且在程序员使用了无定义的特性时向程序员报警。函数定义得越严格,确认其参数就越容易。

  l 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了相应的假定,就要使用断言对所做的假定进行检验,或者重新编写代码去掉相应的假定。另外,还要问:“这个程序中最可能出错的是什么,怎样才能自动地查出相应的错误?”努力编写出能够尽早查出错误的测试程序。

  l 一般教科书都鼓励程序员进行防错性程序设计,但要记住这种编码风格会隐瞒错误。当进行防错性编码时如果“不可能发生”的情况确实发生了,要使用断言进行报警。

  9.3 为子系统设防

  l 考查所编写的子系统,问自己:“在什么样的情况下,程序员在使用这些子系统时会犯错误。”在子系统中加上相应的断言和确认检查代码,以捕捉难于发现的错误和常见的错误。

  l 如果不能使错误不断重现,就无法排除它们。找出程序中可能引起随机行为的因素,并将它们从程序的调试版本中清除。把目前尚“无定义”的内存单元置成了某个常量值,就可能产生这种错误。在这种情况下,如果程序在该单元被正确地定义为某个值之前引用了它的内容,那么每次执行这部分错误的代码,都会得到同样的错误结果。

  l 如果所编写的子系统释放内存(或者其它的资源),并因此产生了“ 无用信息”,那么要把它搅乱,使它真的像无用信息。否则,这些被释放了的数据就有可能仍被使用,而又不会被注意到。

  l 类似地,如果在所编写的子系统中某些事情可能发生,那么要为该子系统加上相应的调试代码,使这些事情一定发生。这样可以增大查出通常得不到执行的代码中的错误的可能性。

  l 尽力使所编写的测试代码甚至在程序员对其没有感觉的情况下亦能起作用。最好的测试代码是不用知道其存在也能起作用的测试代码。

  l 如果可能的话,把测试代码放到所编写的子系统中,而不要把它放到所编写子系统的外层。不要等到进行了系统编码时,才考虑其确认方法。在子系统设计的每一步,都要考虑“如何对这一实现进行详尽的确认”这一问题。如果发现这一设计难于测试或者不可能对其进行测试,那么要认真地考虑另一种不同的设计,即使这意味着用大小或速度作代价去换取该系统的测试能力也要这么做。

  l 在由于速度太慢或者占用的内存太多而抛弃一个确认测试程序之前,要三思而后行。切记,这些代码并不是存在于程序的交付版本中。如果发现自己正在想:“这个测试程序太慢、太大了”,那么要马上停下来问自己:“怎样才能保留这个测试程序,并使它既快又小?”

  9.4 对程序进行逐条跟踪

  l 代码中不会自己生出错误来,错误是程序员编写新代码或者修改现有代码的产物。如果你想发现代码中的错误,没有哪个办法比在对代码进行编译时对其进行逐条跟踪更好。

  l 虽然直观上你可能认为对代码进行走查会花费大量的时间,但这是不对的。刚开始进行代码的走查确实要多花一点时间,但当这一切习惯成自然之后并不会多花多少时间,你可以很快地走查一遍。

  l 一定要对每一条代码路径进行逐条的跟踪,至少要跟踪一遍,尤其是对代码中的错误处理部分。不要忘记 &&、|| 和?:这些运算符,它们每个都有两条代码路径需要进行测试。

  l 在某些情况下也许需要在汇编语言级对代码进行逐条的跟踪。尽管不必经常这样做,但在必要的时候不要回避这种做法。

  9.5 糖果机界面

  l 最容易使用和理解的函数界面,是其中每个输入和输出参数都只代表一种类型数据的界面。把错误值和其它的专用值混在函数的输入和输出参数中,只会搞乱函数的界面。

  l 设计函数的界面迫使程序员考虑所有重要细节(如错误情况的处理),不要使程序员能够很容易地忽视或者忘记有关的细节。

  l 老要想到程序员调用所编函数的方式,找出可能使程序员无意间引入错误的界面缺陷。尤其重要的是要争取编出永远成功的函数,使调用者不必进行相应的错误处理。

  l 为了增加程序的可理解性从而减少错误,要保证所编函数的调用能够被必须阅读这些调用的程序员所理解。莫明其妙的数字和布尔参数都与这一目标背道而驰,因此应该予以消除。

  l 分解多功能的函数。取更专门的函数名(如ShrinkMemory 而不是 realloc)不仅可以增进人们对程序的理解,而且使我们可以采用更加严格的断言自动地检查出调用错误。

  l 为了向程序员展示出所编函数的适当调用方法,要在函数的界面中通过注解的方式详细说明。要强调危险的方面。

  9.6 风险事业

  l 在选择数据类型的时候要谨慎。虽然ANSI 标准要求所有的执行程序都要支持char, int,long 等类型,但是它并没有具体定义这些类型。为了避免程序出错,应该只按照ANSI 的标准选择数据类型。

  l 由于代码可能会在不理想的硬件上运行,因此很可能算法是正确的而执行起来却有错。所以要经常详细检查计算结果和测试结果的数据类型范围是否上溢或下溢。

  l 在实现某个设计的时候,一定要严格按照设计去实现。如果在编写代码时只是近似地实现所提出的要求,那就很容易出错。

  l 每个函数应该只有一个严格定义的任务,不仅如此,完成每个任务也应只有一种途径。假如不管输入什么都能执行同样的代码,那就会大大降低那些不易被发现的错误所存在的概率。

  l if 语句是个警告信号,说明代码所做的工作可能比所需要的要多。努力消除代码中每一个不必要的if 语句,经常反问自己:“怎样改变设计从而删掉这个特殊情况?”有时可能要改变数据结构,有时又要改变一下考察问题的方式,就象透镜是凸的还是凹的问题一样。

  l 有时if 语句隐藏在while 和for 循环的控制表达式中。“?:”操作符是if 语句的另外一种形式。

  l 曾惕有风险的语言惯用语,注意那些相近但更安全的惯用语。特别要警惕那些看上去象是好编码的惯用语,因为这样的实现对总体效率很少有显著的影响,但却增加了额外的风险性。

  l 在写表达式时,尽量不要把不同类型的操作符混合起来,如果必须混合使用,用括号把它们分隔开来。

  l 特殊情况中的特殊情况是错误处理。如果有可能,应该尽量避免调用可能失败的函数,假如必须调用返回错误的函数,将错误处理局部化以便所有的错误都汇集到一点,这将增加在错误处理代码中发现错误的机会。

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

  9.7 编码中的假象

  l 如果你要用到的数据不是你自己所有的,那怕是临时的,也不要对其执行写操作。尽管你可能认为读数据总是安全的,但是要记住,从映射到I/O 的存储区读数据,可能会对硬件造成危害。

  l 每当释放了存储区人们还想引用它,但是要克制自己这么做。引用自由存储区极易引起错误。

  l 为了提高效率,向全局缓冲区或静态缓冲传递数据也是很吸引人的,但是这是一条充满风险的捷径。假若你写了一个函数,用来创建只给调用函数使用的数据,

  那么就将数据返回给调用函数,或保证不意外地更改这个数据。

  l 不要编写依赖支持函数的某个特殊实现的函数。我们已经看到,FILL 例程不该象给出的那样调用CMOVE,这种写法只能作为坏程序设计的例子。

  l 在进行程序设计的时候,要按照程序设计语言原来的本意清楚、准确地编写代码。避免使用有疑问的程序设计惯用语,即使语言标准恰好能保证它工作,也不要使用。请记住,标准也在改变。

  l 如果能用C 语言有效地表示某个概念,那么类似地,相应的机器代码也应该是有效的。逻辑上讲似乎应该是这样,可是事实上并非如此。因此在你将多行C代码压缩为一行代码之前,一定要弄清楚经过这样的更改以后,能否保证得到更好的机器代码。

  l 最后,不要象律师写合同那样来编写代码。如果一般水平的程序员不能阅读和理解你的代码,那就说明你的代码太复杂了,使用简单一点的语言。

  9.8 剩下来的就是态度问题

  l 错误既不会自己产生,也不会自己改正。如果你得到了一个错误报告,但这个错误不再出现了。不要假设测试员发生了幻觉,而要努力查找错误,甚至要恢复程序的老版本。

  l 不能“以后”再修改错误。这是许多产品被取消的共同教训。如果在你发现错误的时候就及时地更正了错误,那你的项目就不会遭受毁灭性的命运。当你的项目总是保持近似于0 个错误时,怎么可能会有一系列的错误呢?

  l 当你跟踪查到一个错误时,总要问一下自己,这个错误是否会是一个大错误的症状。当然,修改一个刚刚追踪到的症状很容易,但是要努力找到真正的起因。

  l 不要编写没有必要的代码。让你的竞争者去清理代码,去实现“冷门”但无价值的特征,去实现自由特征。让他们花大量的时间去修改由于这些无用代码所引起的所有没有必要的错误。

  l 记住灵活与容易使用并不是一回事。在你设计函数和特征时,重点是使之容易使用;如果它们仅仅是灵活的,象realloc 函数和Excel 中的彩色格式特征那样,那么就没法使得代码更加有用;相反地,使得发现错误变得更困难了。

  l 不要受“试一试”某个方案以达到预期结果的影响。相反,应把花在尝试方案上的时间用来寻找正确的解决方法。如果必要,与负责你操作系统的公司联系,这比提出一个在将来可能会出问题的古怪实现要好。

  l 代码写得尽量小以便于全面测试。在测试中不要马虎。记住,如果你不测试你的代码,就没有人会测试你的代码了。无论怎样,你也不要期望测试组为你测试代码。

  l 最后,确定你们小组的优先级顺序,并且遵循这个顺序。如果你是约克,而项目需要吉尔,那么至少在工作方面你必须改变习惯。

  10 本书附录A 编码检查表

  本附录给出的问题列表,总结了本书的所有观点。使用本表的最好办法是花两周时间评

  审一下你的设计和编码实现。先花几分钟时间看一看列表,一旦熟悉了这些问题,就可以灵

  活自如地按它写代码了。此时,就可以把表放在一边了。

  一般问题

  ── 你是否为程序建立了DEBUG 版本?

  ── 你是否将发现的错误及时改正了?

  ─一 你是否坚持彻底测试代码.即使耽误了进度也在所不惜?

  ── 你是否依靠测试组为你测试代码?

  ─一 你是否知道编码的优先顺序?

  ─一 你的编译程序是否有可选的各种警告?

  关于将更改归并到主程序

  ─一 你是否将编译程序的警告(包括可选的)都处理了?

  ── 你的代码是否未用Lint

  ─一 你的代码进行了单元测试吗?

  ─一 你是否逐步通过了每一条编码路径以观察数据流?

  ─一 你是否逐步通过了汇编语言层次上的所有关键代码?

  ── 是否清理过了任何代码?如果是,修改处经过彻底测试了吗?

  ─一 文档是否指出了使用你的代码有危险之处?

  ── 程序维护人员是否能够理解你的代码?

  每当实现了一个函数或子系统之时

  ─一 是否用断言证实了函数参数的有效性?

  ─一 代码中是否有未定义的或者无意义的代码?

  ─一 代码能否创建未定义的数据?

  ─一 有没有难以理解的断言?对它们作解释了没有?

  ─一 你在代码中是否作过任何假设?

  ─一 是否使用断言警告可能出现的非常情况?

  ─一 是否作过防御性程序设计?代码是否隐藏了错误?

  ─一 是否用第二个算法来验证第一个算法?

  ─一 是否有可用于确认代码或数据的启动(startup)检查?

  ─一 代码是否包含了随机行为?能消除这些行为吗?

  ── 你的代码若产生了无用信息,你是否在DEBUG 代码中也把它们置为无用信息?

  ── 代码中是否有稀奇古怪的行为?

  ── 若代码是子系统的一部分,那么你是否建立了一个子系统测试?

  ── 在你的设计和代码中是否有任意情况?

  ── 即使程序员不感到需要,你也作完整性检查吗?

  ── 你是否因为排错程序太大或太慢,而将有价值的DEBUG 测试抛置一边?

  ── 是否使用了不可移植的数据类型?

  ─一 代码中是否有变量或表达式产生上溢或下溢?

  ── 是否准确地实现了你的设计?还是非常近似地实现了你的设计?

  ── 代码是否不止一次地解同一个问题?

  ── 是否企图消除代码中的每一个if 语句?

  ── 是否用过嵌套?:运算符?

  ── 是否已将专用代码孤立出来?

  ── 是否用到了有风险的语言惯用语?

  ─一 是否不必要地将不同类型的运算符混用?

  ── 是否调用了返回错误的函数?你能消除这种调用吗?

  ─一 是否引用了尚未分配的存储空间?

  ─一 是否引用已经释放了的存储空间?

  ── 是否不必要地多用了输出缓冲存储?

  ── 是否向静态或全局缓冲区传送了数据?

  ── 你的函数是否依赖于另一个函数的内部细节?

  ── 是否使用了怪异的或有疑问的C 惯用语?

  ── 在代码中是否有挤在一行的毛病?

  ── 代码有不必要的灵活性吗?你能消除它们吗?

  ─一 你的代码是经过多次“试着”求解的结果吗?

  ─一 函数是否小并容易测试?

  每当设计了一个函数或子系统后

  ─一 此特征是否符合产品的市场策略?

  ─一 错误代码是否作为正常返回值的特殊情况而隐藏起来?

  ─一 是否评审了你的界面,它能保证难于出现误操作吗?

  ─一 是否具有多用途且面面俱到的函数?

  ─一 你是否有太灵活的(空空洞洞的)函数参数?

  ─一 当你的函数不再需要时,它是否返回一个错误条件?

  ─一 在调用点你的函数是出易读?

  ─一 你的函数是否有布尔量输入?

  修改错误之时

  ── 错误无法消失,是否能找到错误的根源?

  ─一 是修改了错误的真正根源,还是仅仅修改了错误的症状?

  转载自:http://blog.csdn.net/hopesophite/archive/2006/08/02/1010643.aspx

  《编程精粹》读后感(八):只是小部分程序设计tricks

  刚买这本书的时候对他的期望还是很高的,看完之后,有了些失望。讲思想感觉这本书不如程序员修炼之道,讲具体的编码细节不如代码大全。豆瓣评分居然有9分,感觉还是有些过的,与前面两本书的层次还是有些差距的。

  这本书中最独特的视角可能就是作者的那些错误代码的分析了,不过其中的例子也不是很多。第一章是用编译器预警、第二章是断言、第三章是subsystems、第四章是step through,后面的几章关注的都是一些极其特殊的情况(over flow、portable等问题)可能做产品需要,但是平常的编程数据处理基本用不着。

  现在感觉代码大全可能真的是经的起检验的书了

评价:

[匿名评论]登录注册

评论加载中……