Wednesday, January 20, 2016

Writing Solid Code - Book Notes



http://www.cnblogs.com/soroman/archive/2007/12/archive/2007/08/06/845465.html
【注】虽然书中很多例子是基于C语言的,但是编程的思想是相通的,而且书中提出的解决问题的方法及原理是语言无关的。这本书是由多条指导方针组成的,正如作者所说,这些指导方针是多年长期思考及实践的结果,指导方针看似简单,却是经验的总结。除次之外,书中对于解决问题的思考过程显得尤为珍贵。

以下摘取每章的概述以及每条指导方针,附翻译及简单注释。
------------
Introduction------------

Summary:
With the growing complexity of software and the associated climb in bug rates, it's becoming increasingly necessary for programmers to produce
bug-free code much earlier in the development cycle, before the code is first sent to Testing. The key to writing bug-free code is to become more
aware of how bugs come about. Programmers can cultivate this awareness by asking themselves two simple questions about every bug they encounter:
"How could I have prevented this bug?" and "How could I have automatically detected this bug?" The guidelines in this book are the results of regularly asking these two questions over a number of years.

概述:
随着软件复杂性的增长以及相应的bug发生率的爬升,对于程序员来说,在开发周期中代码拿去测试之前,更早地编写bug-free代码变得更加必要。编写bug-free代码的关键是意识到bugs是怎样出现的。程序员能够通过问他们自己关于遇到的每个bug这样两个问题来培养这个技能。

1.我怎样才能阻止这个bug发生?
2.我怎样才能自动察觉到这个bug?

这本书中的指导方针就是多年规律性地问这两个问题的结果。

--------------------
Chapter 1 A Hypothetical Compiler 
一个假定的编译器--------------------
Summary:
If your compiler could detect every bug in your program-no matter the type-and issue an error message, ridding your code of bugs would be
simple. Such omniscient compilers don't exist, but by enabling optional compiler warnings, using syntax and portability checkers, and using automated 
unit tests, you can increase the number of bugs that are detected for you automatically.

概述:
如果你的编译器能够察觉到程序中每个bug ,无论是什么类型,并且还发出一个错误消息,那么让你的代码规避bugs就是简单的事情。这样无所不知的编译器是不存在的,但是通过开启可选的编译器警告选项,使用语法以及可移植性检查器,以及使用单元测试,被你自动察觉到的bugs数目就会增加。

Guidelines:
1.1.Enable all optional compiler warnings.
开启所有可选的编译器警告选项。

1.2.Use lint to catch bugs that your compiler may miss.
使用lint来抓住那些编译器可能遗漏的bugs。

【注】lint是一种代码质量保证工具,能够进行更强的编译检查。如:pc-lint。在有些公司,在check in代码到main branch之前,一般需要通过lint检查,当然最好也开启lint的所有警告开关,久而久之,不需要工具辅助,也能写出健壮的代码。

1.3.If you have unit tests, use them.
如果有单元测试,使用他们。

【注】程序员所做的单元测试侧重于验证代码内部结构和处理过程,大部分利用白盒测试手段,比如设计一些用例测试其内部的逻辑,象控制点(如:条件判断点,循环点,选择分支点等),比如检查内部数据的有效性,甚至检查代码的冗余等。仅仅依赖于黑盒测试是不够的。

--------------------------
Chapter  2 Assert Yourself
断言你自己
---------------------------
Summary:
A good development strategy is to maintain two versions of your program: one that you ship and one that you use to debug the code. By using debugging
assertion statements, you can detect bugs caused by bad function arguments, accidental use of undefined behavior, mistaken assumptions made by other programmers, and impossible conditions that nevertheless somehow show up. Debug-only backup algorithms help verify function results and the algorithms used in functions.

概述:
一个好的开发策略是维护两套版本的程序,一个用来发布一个用来调试。通过使用调试断言语句,你可以察觉到由非法的函数参数,未定义行为的意外使用,其他程序员的错误的假设以及不可能发生的情况仍然不知何故出现了所造成的bugs。只用于调试的备份算法能帮助验证函数的结果以及函数中使用的算法。

Guidelines:
2.1.Maintain both ship and debug versions of your program.
维护发布和调试两套版本的程序

2.2.Use assertions to validate function arguments
使用断言来验证函数参数

【注】常见的使用Assert的一个地方。在入口处检查参数,及早发现问题,使用Assert的好处是在调试版本下能立刻定位到上下文。

2.3.Strip undefined behavior from your code, or use assertions to catch illegal uses of undefined behavior.
去除代码中的未定义的行为,或者使用断言来抓住未定义行为的非法使用。

【注】使用Assert的又一个地方。作者举了个c实现内存拷贝的例子,当在两块重叠的内存之间进行拷贝的时候的行为是未定义的。所以加上个Assert来断言传入的两个指针所指内存之间足够大。

2.4.Don't waste people's time. Document unclear assertions.
不要浪费人们的时间。为不清楚的断言加上说明

2.5.Either remove implicit assumptions, or assert that they are valid.
要么消除隐式的假设,要么断言它们是合法的。

【注】使用Assert的又一个地方。作者举了个c实现内存设值的例子,有时为了加快memset,会将byte*指针转换成long*的。但是byte*的指针可能是个奇地址,如果转换成long*的,在有些系统上会出问题,因为long*的指针在这些系统上不能以奇地址开始。

2.6.Use assertions to detect impossible conditions.
使用断言来察觉不可能的情况。

2.7.Don't hide bugs when you program defensively.
在进行防御式编程时,不要隐藏bugs。
【注】防御式编程是一种编程风格,主要目的是保护系统不受“非法”输入的破坏,但这样很容易掩盖bugs。

2.8.Use a second algorithm to validate your results.
使用第二套算法来验证你的结果。

2.9.Don't wait for bugs to happen; use startup checks.
不要等着bugs发生,使用启动检查。
http://www.cnblogs.com/soroman/archive/2007/12/22/1010870.html

http://www.cnblogs.com/soroman/archive/2007/12/25/1014142.html
Chapter 4 Step through your code
检查你的代码
-----------------------------------


Summary:
The best way to find bugs is to step through all new code in a debugger. By stepping through each instruction with your focus on the data flow, you can quickly detect problems in your expressions and algorithms. Keeping the focus on the data, not the instructions, gives you a second, very different, view of the code. Stepping through code takes time, but not nearly as much as most programmers would expect it to.

概述:
发现bugs最好的方法是在调试器里逐行检查所有新的代码。通过检查每个指令,集中精力注意数据流,你可以很快侦测到语句和算法中的问题。
把注意力放在数据上,而非指令上,能给你另外一种对代码的了解。逐行检查代码是花费时间,但没有绝大部分的程序员认为的需要花费那么多的时间。

4.1.Don't wait until you have a bug to step through your code.
不要等到bug出现时才去检查你的代码。

【注】编写没有bug的代码的最好的方式是什么?就是主动地检查新增加的或者修改后的代码,观察它们的执行情况,验证每个指令是不是按照你的意图做了该做的事情。
黑盒测试的问题是你不知道到盒子里到底发生了什么。检查代码可以说是一种白盒测试,这是程序员自己的责任。下些断点,观察下数据,你就会发现bugs了。

4.2.Step through every code path.
检查每条代码路径。

【注】除了if-else, switch path外,&&, ||, ?: 也包含多个path。还有通常error handling分支的bug很难被发现,因为进入该分支的机会少,需要注意。

4.3.As you step through code, focus on dataflow.
检查代码的时候,注意数据流。

【注】检查代码的真正威力在于跟踪数据流向,由此,能发现很多bugs:
Overflow and underflow bugs
Data conversion bugs
Off-by-one bugs
NULL pointer bugs
Bugs using garbage memory (OxA3 bugs)
Assignment bugs in which you've used =instead of ==
Precedence bugs
Logic bugs

4.4.Source level debuggers can hide execution details. Step through critical code at the instruction level.
源代码级的调试器可能隐藏运行细节。在指令层级上检查重要的代码。

【注】比如你在walk through复杂条件语句(如包含&& or ||),你可以在指令级(或汇编级,汇编语句与机器指令是相互对应的关系)
观察以得出所有分支部分的结果,而这些你在源代码级可能不方便得到。

-----------------------------------
Chapter 5 Candy Machine Interfaces
糖果机接口
-----------------------------------


Summary:
It's not enough that your functions be bug-free; functions must be easy to use without introducing unexpected bugs. If bug rates are to be reduced,
each function needs to have one well-defined purpose, to have explicit single-purpose inputs and outputs, to be readable at the point where it is called, and ideally to never return an error condition. Functions with these attributes are easy to validate using assertions and debug code, and they minimize the amount of error handling code that must be written.

概述:
只做到你的函数本身没有bug是不够的,函数必须是容易使用,同时不会引入意外的bugs(你自己没有bug还不够,还需要别人使用你也不会出现bug)。如果要减少bug出现率,那么每个函数需要一个定义

好的目的,需要显式的用于单个用途的输入和输出,需要在它被调用的地方有好的可读性,以及理想情况下不会返回出错情况。拥有这些属性的函数是很容易通过Assertions和调试代码验证的,这种函数减少了那些必须的error handling的代码。

Guidelines:

5.1.Make it hard to ignore error conditions.Don 't bury error codes in return values.
不要轻易忽略出错情形。不要在返回值中携带出错代码。

【注】象getchar, malloc这种函数接口就不好:
getchar返回整型,如果出错的话返回-1(EOF),象这种将所有信息放在返回值的接口是不好的。如果一个系统char型的都是无符号数,即不会返回-1,那么getchar失败时也无法知道。
malloc返回指针,如果失败返回0。
改进getchar接口:
如果成功读取一个新的char,则返回TRUE,否则False,char可以从参数返回。

5.2.Always look for, and eliminate flaws in your intefaces.
从你的接口中不停地寻找并减少瑕疵。

【注】下面代码的隐患在于如果realloc失败,那么pbBuf=NULL,pbBuf所指向的原先的内存块就会丢失掉。
1pbBuf = (byte * ) realloc(pbBuf , sizeNew);2if (pbBuf != NULL)3 /*use/initialize the larger buffer*/

改进接口:
1flag fResizeMemory(void **ppv, size_t sizeNew)2{3    byte **ppb = (byte **)ppv;4    byte *pbNew;5    pbNew = (byte *) realloc (*ppb , sizeNew);6    i f (pbNew != NULL)7        *ppb = pbNew;8    return (pbNew != NULL);9}

这样即使失败,原先的指针不会被破坏。

5.3.Don't write multipurpose functions. Write separate functions to allow stronger argument validation.
不要写含多个目的的函数。分开(目的)写函数以允许更强的参数检查。

5.4.Don't be wishy-washy. Define explicit function arguments.
不要不正式。定义显式的函数参数。

【注】参考下面的函数:
1char *CopySubStr(char *strTo, char *strFrom, size_t size)2{3    char* str Start = strTo:4    while (size-- > 0)5     *strTo++ = *strFrom++;   6    *strTo = '\0' ;7    return (strstart) ;8}

函数中没有验证参数size的大小,对于size=0,函数可以跳出,但是对于size大于strFrom指向的长度的时候,bug就发生了,而且调用者需要花费力气找出bug。
有时候允许一个无意义的参数,比如说size=0,是值得的,因为可以减少调用者在外部的测试。但是,如果调用者传入0 size的机会是很少的或者从不,那么就不要handle 0 size,因为这可能隐藏bug,所以使用assert来保证size != 0从而发现bug。这还是防御式编程与隐藏bug的矛盾。参考在fFreeMemory中对NULL指针的处理(Assert)。

5.5.Write functions that, given valid inputs, cannot fail.
编写只要传入合法的输入,不会失败的函数。

【注】象tolower函数,很多人喜欢将它设计成
1char to1ower(char ch)2{3    if(ch >= 'A' && ch <= 'Z' )4        return (ch + 'a' - 'A' ) ;5    else
6        return (-1);7}

这同样是一个返回值包含过多信息的例子。其弊病参见前面所述。你当然可以象前面那样改进。但是,用户始终需要作runtime check。如果这样改进:
1char tolower(char ch)2{3    ASSERT(ch >= 'A' && ch <= 'Z' ) ;4    return (ch + 'a' - 'A' ) ;5}

这样,用户永远不需要作runtime error check,减少了调用者的代码量以及出错的可能(在debug版已经被assert了),这其实就是debug版的作用。

5.6.Make the code intelligible at the point of call. Avoid boolean arguments.
使得(调用函数的)代码是容易理解的。避免布尔参数。

【注】布尔参数不容易扩展。

5.7.Write comments that emphasize potential hazards.
写些注释强调潜在的风险。

http://www.cnblogs.com/soroman/archive/2008/01/19/1045432.html
Chapter 6 Risky Business
危险的事情
-----------------------------------


Summary:
Given the numerous implementation possibilities for a given function, it should come as no surprise that some implementations will be more errorprone than others. 
The key to writing robust functions is to exchange risky algorithms and language idioms for alternatives that have proven to be comparably efficient yet much safer. 

At one extreme this can mean using unambiguous data types; at the other it can mean tossing out an entire design
simply because it would be difficult, or impossible, to test.

概述:
如果一个函数有很多实现方案的可能,那么一定是其中一些实现比另一些出错的可能性更大。实现稳定的函数的关键是不用带风险的算法和语言习惯,而是去用那些同样高效但是很安全的方案。在一个极端下,这意味着使用无歧义的数据类型,在另一个极端下,这意味着必须扔掉全部的设计,仅仅是因为它是很难或者不可能被测试的。

Guidlines:
6.1.Use well-defined data types.
使用定义明确的数据类型。

【注】当ANSI委员会看到C运行在各种平台上,它们看到C不是一个象用户认为的那样的一个portable的语言。并仅仅是因为C标准库在不同的系
统上不一样,还因为预处理器和语言本身。ANSI标准委员会对大部分方面作了标准化,但是却忽略了基本数据类型。ANSI没有具体的定义int,char,long,而是留下具体实现给编译器厂商。结果,有些ANSI标准的编译器支持32位整型和带符号的字符型,另外一些编译器可能支持16位整型和无符号char型。
那么看看下面的代码:
1char ch;2ch = 0xFF;3if (ch == 0xFF)4{5 6}

上面的判断到底是true or false,你永远也不知道,因为这依赖于编译器的实现。如果是无符号char型,那么结果是true,如果是带符号char型,那么结果就是false。还有象类似:int reg = 3;虽然reg被定义成int,reg可以是signed或者unsigned,还是取决于你的编译器。你必须使用signed int或者unsigned int。
short是多大?int呢?long呢?ANSI标准并没有说,而是留给编译器的作者去决定。标准没有规定的原因是考虑到兼容以前的代码。

6.2.Always ask, "Can this variable or expression over- or underflow?"
时刻问自己:”这个变量或者表达式溢出了吗?”

【注】看看下面的代码:
 1# include <limits.h> /*Pull in UCHARMAX. */
 2char chToLower[UCHARMAX+1]; 3void Bui1dToLowerTable(void/* ASCII version */
 4{ 5 unsigned char ch; 6 /* First set every character to itself. */
 7 for(ch = 0; ch <= UCHARMAX; ch++) 8  chToLower[ch] = ch: 9 /* Now poke lowercase letters into the uppercase slots . */
10 for (ch = 'A' ; ch <= 'Z'; ch++ )11  chToLower[ch] = ch + 'a' - 'A' :12}

这段代码的问题在于ch=UCHARMAX的时候,再加1,会导致等于0,所以造成死循环。上面是Overflow的问题,同样存在Underflow的问题:
 1void *memchr(void *pv, unsigned char ch, size_t size) 2{ 3 unsigned char *pch = (unsigned char *)pv; 4 while (--size >= 0) 5 { 6  if(*pch == ch) 7   return (pch); 8  pch++; 9 }
10 return(NULL);11}

如果size是unsigned的话,当size=0后再进行递减操作的话,那么就会翻转成unsigned型的size_t的最大值。好消息是如果你按照我在第四章所建议的那样去检查的话,你就可以发现这些溢出bugs。

6.3.Implement "the task" just once.
只实现“任务”一次。

【注】设想为了表示一个文档处理程序中的层次状的Window结构,设计如下结构体:
1typedef struct WINDOW2{3 struct WINDOW *pwndChild; /* NULL if no children */
4 struct WINDOW *pwndSibling; /* NULL if no brothers or sisters */
5 char *strWndTitle;6}
 window; /* Naming: wnd, *pwnd */

现在表示所有的Window可以由该结构体节点构成的二叉树来表示,注意其中的Root节点实际上只有成员pwndChild是有意义的--指向所有top-level的windows, 因为没有兄弟姐妹和Title,而且Root是不能move,hide,or delete的,所以有人将Root节点简化成其中一个成员,即用指针pwndRootChildren来表示,这样至少可以减少空间。于是AddChild(添加一个子window到已存在的window节点下)的实现如下:
 1/* pwndRootChildren is the pointer to the list of top-level 2* windows,such as the menu bar and the main document windows. 3*/
 4static window *pwndRootChildren = NULL; 5 6void AddChild(window *pwndParent, window *pwndNewBorn) 7{ 8 /* New windows may have c h i l d r e n but not s i b l i n g s  */
 9 ASSERT(pwndNewB0rn->pwndSibling == NULL);10 if(pwndparent == NULL)11 {12  /* Add window t o the t o p - l e v e l root l i s t . */
13  pwndNewBorn->pwndSibling = pwndRootChildren;14  pwndRootChildren = pwndNewBorn;15 }

16 else
17 {18  /* If Parent's first child. start a new sibling chain;19   * otherwise, add child to the end of the existing20   * sibling chain.21   */
22  if (pwndparent->pwndChild == NULL)23   pwndparent->pwndChild = pwndNewBorn;24  else
25  {26   window *pwnd = pwndparent->pwndChild;27   while (pwnd->pwndSibling != NULL)28   pwnd = pwnd->pwndSibling;29   pwnd->pwndSibling = pwndNewBorn;30  }
31 }

32}

上述代码至少违背了编写bug-free代码的三个原则:
1.Don't accept special purpose arguments such as the NULL pointer.
2.Implement your design, not something that approximates it.
3.The third principle is new: Strive to make every function perform its task exactly one time

前俩个原则前面已经说过,第三个是新的。上面的代码含有三个不同的插入路径,直觉告诉我们越多的路径越可能导致bugs。这相当于一个任务需要三次“实现”才能完成。尽量用一次“实现”搞定一个任务。上面的代码的改进可以是将Root当作普通的节点,这样逻辑就比较简单了,不用处理Root节点这一特殊情况。代码如下:
 1/* pwndDisplay points to the root-level window. which is 2* a1located during program initialization .*/
 3window *pwndDisplay = NULL; 4 5void AddChild(window *pwndParent, window *pwndNewBorn) 6{ 7/* New windows may have children but not siblings  */
 8ASSERT(pwndNewBorn->pwndSibling == NULL); 9/* If Parent's first child, start a new sibling chain;10* otherwise, add child to the end of the existing sibling chain.*/
1112if(pwndparent->pwndChild == NULL)13  pwndparent->pwndChild = pwndNewBorn;14else15{16 window *pwnd = pwndparent->pwndChild;17 while (pwnd->pwndSibiing != NULL)18  pwnd = pwnd->pwndSibling;19  pwnd->pwndSibling = pwndNewBorn;20 }
21}

6.4.Get rid of extraneous if statements.
处理无关的if语句。

【注】上面改进的AddChild虽然比最开始的要好,但是它还是要做“两次”,还是包含两个路径。实际上,上面的算法都是以Window为中心的,如果换成以Pointer为中心,那么就有:
 1void AddChild(window *pwndParent, window *pwndNewBorn) 2{ 3 window **ppwndNext; 4 /* New windows may have c h i l d r e n but not s i b l i n g s  */
 5 ASSERT(pwndNewBorn->pwndSibling == NULL): 6 /* Traverse the sibling chain using a pointer - centric 7 * algorithm . We set ppwndNext to point at 8 * pwndparent->pwndChild since the latter pointer 9 * is the first "next sibling pointer" of the list .*/
10 ppwndNext = &pwndParent->pwndChiId;11 while(*ppwndNext != NULL)12  ppwndNext = &(*ppwndNext)->pwndSibling;1314 *ppwndNext = pwndNewBorn;15}

这样,你就无需处理特殊路径了,减少了出错的可能。

6.5.Avoid using nested ?: operators.
避免使用嵌套的?:操作符。

【注】看下下面这段代码:
 1/* uCycleCheckBox -- return the next state for a checkbox. 2* 3* Given the current setting .uCur, return what the next 4* checkbox state should be. This function handles both 5* two-state checkboxes that toggle between 0 and 1, and 6* three-state checkboxes that cycle through 2, 3, 4, 2,  7*/
 8unsigned uCycleCheckBox(unsigned uCur) 9{10 return ((uCur<=l) ? (uCur?O:l) : (uCur==4)?2:(uCur+l)):11}

其中用到了嵌入的?:操作符。相当于以下用if语句的代码:
 1unsigned uCycleCheckBox(unsigned uCur) 2{ 3 unsigned uRet ; 4 if(uCur <= 1) 5 { 6  if (uCur != 0/* Handle the 0, 1, 0,. . . cycle. */
 7   uRet = 0; 8  else
 9   uRet = 1;10 }

11 else
12 {13  if (uCur == 4 ) /* Handle the 2, 3, 4. 2.  cycle. */
14   uRet = 2;15  else
16   uRet = uCur+l;17 }

18 return (uRet);19}

如果你的编译器优化的话,可能变成以下:
 1unsigned uCycleCheckBox(unsigned uCur) 2{ 3 unsigned uRet ; 4 if(uCur <= 1) 5 { 6  uRet = 0/* Handle the 0, 1, 0.  cycle. */
 7  if (uCur == 0) 8   uRet = 1; 9 }

10 else
11 {12  uRet = 2/* Handle the 2 , 3, 4. 2.  cycle. */
13  if (uCur != 4)14   uRet = uCur+l;15 }

16 return(uRet);17}

以上单个版本的代码不好理解,路径多。直接的实现如下:

1unsigned uCycleCheckBox(unsigned uCur)2{3 ASSERT(uCur >= 0 && uCur <= 4);4 if(uCur == 1/* Time t o r e s t a r t the f i r s t cycle? */
5  return ( 0 ) ;6 if(uCur == 4/* What about the second one? */
7  return ( 2 ) ;8 return (uCur+l); /* Nope, nothing special t h i s time. */
9}

或者使用表来解决:
1unsigned uCycleCheckBox(unsigned uCur)2{3 static const unsigned uNextState[] = 10342 };4 ASSERT(uCur >= 0 && uCur <= 4);5 return (uNextState[uCur]);6}


6.6.Handle your special cases just once.
只处理你的特殊情形一次。

【注】看看下面的代码:
1void *memchr(void *pv, unsigned char ch, size_t s i z e )2{3 unsigned char *pch = (unsigned char *)pv;4 unsigned char *pchEnd = pch + size;5 while (pch < pchEnd && *pch != ch)6  pch++;7 return ((pch < pchEnd) ? pch : NULL);8}

其中对特殊情况有两处处理代码,这不利于维护。可以将同样特殊处理的代码合并在一处:

 1void *memchr(void *pv, unsigned char ch, size_t size ) 2{ 3 unsigned char *pch = (unsigned char *)pv; 4 unsigned char *pchEnd = pch + size; 5 while(pch < pchEnd) 6 { 7 if (*pch == ch) 8 return (pch); 9 pch++;10 }
11 return(NULL);12}

6.7.Avoid risky language idioms.
避免带风险的语言惯用法。

【注】再看看上面memchr的代码,它们都存在一个难以发现bug。这个地方:

1pchEnd = pch + size;2while (pch < pchEnd)3

如果要搜寻的地址范围正好是可寻址内存的最后一段,比如,pv指向可寻址内存范围的最后72 bytes,size=72,那么会发生什么?死循环。为了避免pchEnd指向一不存在的内存,一个可能的改进如下:

1pchEnd = pch + size - 1;2while (pch <= pchEnd)3

这样保证了pchEnd指向最后一个char的位置,这个位置肯定是存在的。
但是,这里又有一个新问题,前面也出过的overflow问题。(UCHAR-MAX overflow bug we saw earlier in BuildToLowerTable)当有指针和计数器时,安全的覆盖一个范围的方法是使用计数器来控制逻辑:

 1void *memchr(void *pv, unsigned char ch, size_t size ) 2{ 3 unsigned char *pch = (unsigned char *)pv; 4 while (size-- > 0 ) 5 { 6  if (*pch == ch) 7  return (pch) ; 8  pch ++; 9 }
10 return (NULL);11}

有人可能推荐你说使用 --size >= 0 代替 size-- > 0
理由是可以产生更高效的代码。问题是这样做是有可能会产生bug的,比如当size是unsigned值的时候,这个循环会永远执行下去。即使不是unsigned值,当size的初始值是INT_MIN的时候,--size会发生underflow。

另一个有风险的惯用法属于“未有效利用的提高效率方法”是使用位操作进行乘法除法以及2的次方数的mod操作。比如说在第二章中提到的快速版的memset函数中的代码:
1pb = (byte *)longfill((long *)pb, 1 , size / 4);2size = size % 4;

有人可能会说怎么不优化成如下这样?
1pb = (byte *)longfill((long *)pb, 1 , size >> 2);2size = size & 3;

问题是>>2等价于/4只在目标操作数是unsigned的value才成立。
C中还有很多带风险的习惯用法,最好的方式是你从你的bug中学习,然后并牢记那些带风险的习惯用法,从而去避免它们。

6.8.Don't needlessly mix operator types. If you must mix operators, use parentheses to
isolate the operations.
在不必要的时候不要混合使用操作符。如果必须这么做的话,使用括号来分隔这些符号。

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

8       剩下来的就是态度问题

8.1    错误几乎不会“消失”

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

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

l         不要通过把改正错误移置产品开发周期的最后阶段来节省时间。修改一年前写的代
码比修改几天前写的代码更难,实际上这是浪费时间。
l         “一次性”地修改错误会带来许多问题:早期发现的错误难以重现。
l         错误是一种负反馈,程序开发倒是快了,却使程序员疏于检查。如果规定只有把错误全部改正之后才能增加新特征的话,那么在整个产品开发期间都可以避免程序员的疏漏,他们将忙于修改错误。反之,如果允许程序员略过错误,那就使管理失控。
l         若把错误数保持在近乎于的数量上,就可以很容易地预言产品的完成时间。只需要估算一下完成 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       你必须养成经常询问怎样编写代码的习惯。

本书就是长期坚持询问一些简单问题所得的结果。
我怎样才能自动检测出错误?
我怎样才能防止错误?
这种想法和习惯是帮助我编写无错代码呢还是妨碍了我编写无错代码?

9.1    假想的编译程序

l         消除程序错误的最好方法是尽可能早、尽可能容易地发现错误,要寻求费力最小的自动查错方法。
努力减少程序员查错所需的技巧。可以选择的编译程序或lint 警告设施并不要求程序员要有什么查错的技巧。在另一个极端,高级的编码方法虽然可以查出或减少错误,但它们也要求程序员要有较多的技巧,因为程序员必须学习这些高级的编码方法。

9.2    自己设计并使用断言

l         要同时维护交付和调试两个版本。封装交付的版本,应尽可能地使用调试版本进行自动查错。
断言是进行调试检查的简单方法。要使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是在最终产品中必须处理的。
使用断言对函数的参数进行确认,并且在程序员使用了无定义的特性时向程序员报警。函数定义得越严格,确认其参数就越容易。
在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了相应的假定,就要使用断言对所做的假定进行检验,或者重新编写代码去掉相应的假定。另外,还要问:“这个程序中最可能出错的是什么,怎样才能自动地查出相应的错误?”努力编写出能够尽早查出错误的测试程序。
一般教科书都鼓励程序员进行防错性程序设计,但要记住这种编码风格会隐瞒错误。当进行防错性编码时如果“不可能发生”的情况确实发生了,要使用断言进行报警。

9.3    为子系统设防

l         考查所编写的子系统,问自己:“在什么样的情况下,程序员在使用这些子系统时会犯错误。”在子系统中加上相应的断言和确认检查代码,以捕捉难于发现的错误和常见的错误。
如果不能使错误不断重现,就无法排除它们。找出程序中可能引起随机行为的因素,并将它们从程序的调试版本中清除。把目前尚“无定义”的内存单元置成了某个常量值,就可能产生这种错误。在这种情况下,如果程序在该单元被正确地定义为某个值之前引用了它的内容,那么每次执行这部分错误的代码,都会得到同样的错误结果。
如果所编写的子系统释放内存(或者其它的资源),并因此产生了“ 无用信息”,那么要把它搅乱,使它真的像无用信息。否则,这些被释放了的数据就有可能仍被使用,而又不会被注意到。
类似地,如果在所编写的子系统中某些事情可能发生,那么要为该子系统加上相应的调试代码,使这些事情一定发生。这样可以增大查出通常得不到执行的代码中的错误的可能性。
尽力使所编写的测试代码甚至在程序员对其没有感觉的情况下亦能起作用。最好的测试代码是不用知道其存在也能起作用的测试代码。
如果可能的话,把测试代码放到所编写的子系统中,而不要把它放到所编写子系统的外层。不要等到进行了系统编码时,才考虑其确认方法。在子系统设计的每一步,都要考虑“如何对这一实现进行详尽的确认”这一问题。如果发现这一设计难于测试或者不可能对其进行测试,那么要认真地考虑另一种不同的设计,即使这意味着用大小或速度作代价去换取该系统的测试能力也要这么做。
在由于速度太慢或者占用的内存太多而抛弃一个确认测试程序之前,要三思而后行。切记,这些代码并不是存在于程序的交付版本中。如果发现自己正在想:“这个测试程序太慢、太大了”,那么要马上停下来问自己:“怎样才能保留这个测试程序,并使它既快又小?”

9.4    对程序进行逐条跟踪

代码中不会自己生出错误来,错误是程序员编写新代码或者修改现有代码的产物。如果你想发现代码中的错误,没有哪个办法比在对代码进行编译时对其进行逐条跟踪更好。
虽然直观上你可能认为对代码进行走查会花费大量的时间,但这是不对的。刚开始进行代码的走查确实要多花一点时间,但当这一切习惯成自然之后并不会多花多少时间,你可以很快地走查一遍。
一定要对每一条代码路径进行逐条的跟踪,至少要跟踪一遍,尤其是对代码中的错误处理部分。不要忘记 &&、|| 和?:这些运算符,它们每个都有两条代码路径需要进行测试。
在某些情况下也许需要在汇编语言级对代码进行逐条的跟踪。尽管不必经常这样做,但在必要的时候不要回避这种做法。

9.5    糖果机界面

最容易使用和理解的函数界面,是其中每个输入和输出参数都只代表一种类型数据的界面。把错误值和其它的专用值混在函数的输入和输出参数中,只会搞乱函数的界面。
设计函数的界面迫使程序员考虑所有重要细节(如错误情况的处理),不要使程序员能够很容易地忽视或者忘记有关的细节。
老要想到程序员调用所编函数的方式,找出可能使程序员无意间引入错误的界面缺陷。尤其重要的是要争取编出永远成功的函数,使调用者不必进行相应的错误处理。
为了增加程序的可理解性从而减少错误,要保证所编函数的调用能够被必须阅读这些调用的程序员所理解。莫明其妙的数字和布尔参数都与这一目标背道而驰,因此应该予以消除。
分解多功能的函数。取更专门的函数名(如ShrinkMemory 而不是 realloc)不仅可以增进人们对程序的理解,而且使我们可以采用更加严格的断言自动地检查出调用错误。
为了向程序员展示出所编函数的适当调用方法,要在函数的界面中通过注解的方式详细说明。要强调危险的方面。

9.6       风险事业

在选择数据类型的时候要谨慎。虽然ANSI 标准要求所有的执行程序都要支持char, int,long 等类型,但是它并没有具体定义这些类型。为了避免程序出错,应该只按照ANSI 的标准选择数据类型。
由于代码可能会在不理想的硬件上运行,因此很可能算法是正确的而执行起来却有错。所以要经常详细检查计算结果和测试结果的数据类型范围是否上溢或下溢。
在实现某个设计的时候,一定要严格按照设计去实现。如果在编写代码时只是近似地实现所提出的要求,那就很容易出错。
每个函数应该只有一个严格定义的任务,不仅如此,完成每个任务也应只有一种途径。假如不管输入什么都能执行同样的代码,那就会大大降低那些不易被发现的错误所存在的概率。
if 语句是个警告信号,说明代码所做的工作可能比所需要的要多。努力消除代码中每一个不必要的if 语句,经常反问自己:“怎样改变设计从而删掉这个特殊情况?”有时可能要改变数据结构,有时又要改变一下考察问题的方式,就象透镜是凸的还是凹的问题一样。
有时if 语句隐藏在while 和for 循环的控制表达式中。“?:”操作符是if 语句的另外一种形式。
曾惕有风险的语言惯用语,注意那些相近但更安全的惯用语。特别要警惕那些看上去象是好编码的惯用语,因为这样的实现对总体效率很少有显著的影响,但却增加了额外的风险性。
在写表达式时,尽量不要把不同类型的操作符混合起来,如果必须混合使用,用括号把它们分隔开来。
特殊情况中的特殊情况是错误处理。如果有可能,应该尽量避免调用可能失败的函数,假如必须调用返回错误的函数,将错误处理局部化以便所有的错误都汇集到一点,这将增加在错误处理代码中发现错误的机会。
在某些情况下,取消一般的错误处理代码是有可能的,但要保证所做的事情不会失败。这就意味着在初始化时要对错误进行一次性处理或是从根本上改变设计。

9.7    编码中的假象

如果你要用到的数据不是你自己所有的,那怕是临时的,也不要对其执行写操作。尽管你可能认为读数据总是安全的,但是要记住,从映射到I/O 的存储区读数据,可能会对硬件造成危害。
每当释放了存储区人们还想引用它,但是要克制自己这么做。引用自由存储区极易引起错误。
为了提高效率,向全局缓冲区或静态缓冲传递数据也是很吸引人的,但是这是一条充满风险的捷径。假若你写了一个函数,用来创建只给调用函数使用的数据,
那么就将数据返回给调用函数,或保证不意外地更改这个数据。
不要编写依赖支持函数的某个特殊实现的函数。我们已经看到,FILL 例程不该象给出的那样调用CMOVE,这种写法只能作为坏程序设计的例子。
在进行程序设计的时候,要按照程序设计语言原来的本意清楚、准确地编写代码。避免使用有疑问的程序设计惯用语,即使语言标准恰好能保证它工作,也不要使用。请记住,标准也在改变。
如果能用C 语言有效地表示某个概念,那么类似地,相应的机器代码也应该是有效的。逻辑上讲似乎应该是这样,可是事实上并非如此。因此在你将多行C代码压缩为一行代码之前,一定要弄清楚经过这样的更改以后,能否保证得到更好的机器代码。
最后,不要象律师写合同那样来编写代码。如果一般水平的程序员不能阅读和理解你的代码,那就说明你的代码太复杂了,使用简单一点的语言。

9.8    剩下来的就是态度问题

错误既不会自己产生,也不会自己改正。如果你得到了一个错误报告,但这个错误不再出现了。不要假设测试员发生了幻觉,而要努力查找错误,甚至要恢复程序的老版本。
不能“以后”再修改错误。这是许多产品被取消的共同教训。如果在你发现错误的时候就及时地更正了错误,那你的项目就不会遭受毁灭性的命运。当你的项目总是保持近似于0 个错误时,怎么可能会有一系列的错误呢?
当你跟踪查到一个错误时,总要问一下自己,这个错误是否会是一个大错误的症状。当然,修改一个刚刚追踪到的症状很容易,但是要努力找到真正的起因。
不要编写没有必要的代码。让你的竞争者去清理代码,去实现“冷门”但无价值的特征,去实现自由特征。让他们花大量的时间去修改由于这些无用代码所引起的所有没有必要的错误。
记住灵活与容易使用并不是一回事。在你设计函数和特征时,重点是使之容易使用;如果它们仅仅是灵活的,象realloc 函数和Excel 中的彩色格式特征那样,那么就没法使得代码更加有用;相反地,使得发现错误变得更困难了。
不要受“试一试”某个方案以达到预期结果的影响。相反,应把花在尝试方案上的时间用来寻找正确的解决方法。如果必要,与负责你操作系统的公司联系,这比提出一个在将来可能会出问题的古怪实现要好。
代码写得尽量小以便于全面测试。在测试中不要马虎。记住,如果你不测试你的代码,就没有人会测试你的代码了。无论怎样,你也不要期望测试组为你测试代码。
最后,确定你们小组的优先级顺序,并且遵循这个顺序。如果你是约克,而项目需要吉尔,那么至少在工作方面你必须改变习惯。

一般问题
── 你是否为程序建立了DEBUG 版本?
── 你是否将发现的错误及时改正了?
─一 你是否坚持彻底测试代码.即使耽误了进度也在所不惜?
── 你是否依靠测试组为你测试代码?
─一 你是否知道编码的优先顺序?
─一 你的编译程序是否有可选的各种警告?
关于将更改归并到主程序
─一 你是否将编译程序的警告(包括可选的)都处理了?
── 你的代码是否未用Lint
─一 你的代码进行了单元测试吗?
─一 你是否逐步通过了每一条编码路径以观察数据流?
─一 你是否逐步通过了汇编语言层次上的所有关键代码?
── 是否清理过了任何代码?如果是,修改处经过彻底测试了吗?
─一 文档是否指出了使用你的代码有危险之处?
── 程序维护人员是否能够理解你的代码?
每当实现了一个函数或子系统之时
─一 是否用断言证实了函数参数的有效性?
─一 代码中是否有未定义的或者无意义的代码?
─一 代码能否创建未定义的数据?
─一 有没有难以理解的断言?对它们作解释了没有?
─一 你在代码中是否作过任何假设?
─一 是否使用断言警告可能出现的非常情况?
─一 是否作过防御性程序设计?代码是否隐藏了错误?
─一 是否用第二个算法来验证第一个算法?
─一 是否有可用于确认代码或数据的启动(startup)检查?
─一 代码是否包含了随机行为?能消除这些行为吗?
── 你的代码若产生了无用信息,你是否在DEBUG 代码中也把它们置为无用信息?
── 代码中是否有稀奇古怪的行为?
── 若代码是子系统的一部分,那么你是否建立了一个子系统测试?
── 在你的设计和代码中是否有任意情况?
── 即使程序员不感到需要,你也作完整性检查吗?
── 你是否因为排错程序太大或太慢,而将有价值的DEBUG 测试抛置一边?
── 是否使用了不可移植的数据类型?
─一 代码中是否有变量或表达式产生上溢或下溢?
── 是否准确地实现了你的设计?还是非常近似地实现了你的设计?
── 代码是否不止一次地解同一个问题?
── 是否企图消除代码中的每一个if 语句?
── 是否用过嵌套?:运算符?
── 是否已将专用代码孤立出来?
── 是否用到了有风险的语言惯用语?
─一 是否不必要地将不同类型的运算符混用?
── 是否调用了返回错误的函数?你能消除这种调用吗?
─一 是否引用了尚未分配的存储空间?
─一 是否引用已经释放了的存储空间?
── 是否不必要地多用了输出缓冲存储?
── 是否向静态或全局缓冲区传送了数据?
── 你的函数是否依赖于另一个函数的内部细节?
── 是否使用了怪异的或有疑问的C 惯用语?
── 在代码中是否有挤在一行的毛病?
── 代码有不必要的灵活性吗?你能消除它们吗?
─一 你的代码是经过多次“试着”求解的结果吗?
─一 函数是否小并容易测试?
每当设计了一个函数或子系统后
─一 此特征是否符合产品的市场策略?
─一 错误代码是否作为正常返回值的特殊情况而隐藏起来?
─一 是否评审了你的界面,它能保证难于出现误操作吗?
─一 是否具有多用途且面面俱到的函数?
─一 你是否有太灵活的(空空洞洞的)函数参数?
─一 当你的函数不再需要时,它是否返回一个错误条件?
─一 在调用点你的函数是出易读?
─一 你的函数是否有布尔量输入?
修改错误之时
── 错误无法消失,是否能找到错误的根源?
─一 是修改了错误的真正根源,还是仅仅修改了错误的症状?


Labels

Review (572) System Design (334) System Design - Review (198) Java (189) Coding (75) Interview-System Design (65) Interview (63) Book Notes (59) Coding - Review (59) to-do (45) Linux (43) Knowledge (39) Interview-Java (35) Knowledge - Review (32) Database (31) Design Patterns (31) Big Data (29) Product Architecture (28) MultiThread (27) Soft Skills (27) Concurrency (26) Cracking Code Interview (26) Miscs (25) Distributed (24) OOD Design (24) Google (23) Career (22) Interview - Review (21) Java - Code (21) Operating System (21) Interview Q&A (20) System Design - Practice (20) Tips (19) Algorithm (17) Company - Facebook (17) Security (17) How to Ace Interview (16) Brain Teaser (14) Linux - Shell (14) Redis (14) Testing (14) Tools (14) Code Quality (13) Search (13) Spark (13) Spring (13) Company - LinkedIn (12) How to (12) Interview-Database (12) Interview-Operating System (12) Solr (12) Architecture Principles (11) Resource (10) Amazon (9) Cache (9) Git (9) Interview - MultiThread (9) Scalability (9) Trouble Shooting (9) Web Dev (9) Architecture Model (8) Better Programmer (8) Cassandra (8) Company - Uber (8) Java67 (8) Math (8) OO Design principles (8) SOLID (8) Design (7) Interview Corner (7) JVM (7) Java Basics (7) Kafka (7) Mac (7) Machine Learning (7) NoSQL (7) C++ (6) Chrome (6) File System (6) Highscalability (6) How to Better (6) Network (6) Restful (6) CareerCup (5) Code Review (5) Hash (5) How to Interview (5) JDK Source Code (5) JavaScript (5) Leetcode (5) Must Known (5) Python (5)

Popular Posts