本文是笔者在阅读周立功的《软件单元测试入门与实践》一书时的内容摘抄,我一直相信,写代码这件事,要想把它做好,光掌握语言本身是不够的,应该从工程的角度去看待写代码这件事,除了实现代码功能,还要注重代码的健壮性、可读性、可扩展性、可维护性。除了自己阅读各种规范,保持编写代码时的良好习惯外,借助外在辅助工具可以帮助我们更好地写出高质量的代码,这也是我阅读这本书的初衷。以下是对于书中内容的摘抄。
规则1:始终使用大括号
该条规则要求在 if、else、for、while 这四个关键字后面的语句块即使只有一条语句或者没有语句的情况下也必须使用大括号括起来。
不使用大括号会带来以下几个问题:
- 当需要在语句块中增加一条语句时,有可能会忘记增加括号而引入Bug;
- 如果开发者在调试代码时注释掉语句块中的语句,接下来一条语句执行会发生错误;
- 当语句块中的单条语句是宏调用时,而宏定义里面又包含多条语句时,除第一条语句之外的其他语句在执行时会发生错误;
- 当嵌套层次比较多的时候代码会变得不容易理解,增加维护难度。
下方代码块列出了不合适的代码编写情况以及建议的修改方法。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/* 不建议的书写方法 */
if
(...
)
单条语句 else 单条语句 /* 建议修改如下 */ if (... ) { 单条语句 } else { 单条语句 } /* 不建议的书写方法 */ for (... ) 单条语句 /* 建议修改如下 */ for (... ) { 单条语句 } /* 不建议的书写方法 */ while (... ) ; /* 建议修改如下 */ while (... ) { } /* 不建议的书写方法 */ while (... ) 单条语句 /* 建议修改如下 */ while (... ) { 单条语句 } |
规则2:尽可能使用 const 关键字
如果确定一个变量不会被改变,那么就使用const关键字进行修饰。
使用const关键字有以下两个好处:
- 使用 const 修饰变量有一个好处就是在修改代码时不小心对对应变量进行了误修改,那么编译器会将其当成一个编译错误,这样就能在编译阶段就发现这个错误;
- 某些嵌入式编程工具会将const修饰的变量存放在ROM中,这对于RAM空间有限的嵌入式系统俩说是非常有用的。
如下代码块中,上面的代码应该修改为下面的代码。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* 未使用const修饰 */
bool_t IsLeapYear ( int32_t year ) { bool_t flag = FALSE ; if ( ( 0 == year % 400 ) || ( 0 == year % 100 ) || ( 0 == year % 4 ) ) { flag = TRUE ; } return flag } /* 使用const修饰year防止其被函数误修改 */ bool_t IsLeapYear ( const int32_t year ) { bool_t flag = FALSE ; if ( ( 0 == year % 400 ) || ( 0 == year % 100 ) || ( 0 == year % 4 ) ) { flag = TRUE ; } return flag } |
规则3:尽可能使用 static 关键字
在定义函数或者全局变量时,如果确定函数或者变量只在当前源文件中使用,那么就使用 static 关键字修饰。如果不使用 static 修饰的话,有可能其他文件中会使用这些变量或函数,很显然这是我们不希望看到的。(笔者注:个人认为应该尽量避免全局变量的跨文件直接调用,如果需要在一个源文件中修改另一个源文件中的全局变量,那么另一个源文件对应的头文件应该提供修改这个变量的函数接口,而不是直接使用 extern 关键字对全局变量进行跨文件调用。同时使用函数接口的方式修改其他文件的变量,也可以方便在修改时进行赋值前的检查,防止不合法的变量赋值。)
规则4:尽可能使用 volatile 关键字
volatile 在多线程编程中用于修饰会被多个线程使用的变量。如果一个变量可能会被多个线程使用,那么就要使用 volatile 修饰,以防止编译器优化引入Bug。
1
2 3 4 5 6 7 8 9 10 11 |
/* 线程1 */
flag = true ; while (flag ) { } 后续处理 /* 线程2 */ 前置处理 flag = false; 后续处理 |
在如上代码块所示的情况中,线程1需要等待线程2将 flag 的值设置为 false 后才能进行下一步处理。在编译器开启优化的情况下,编译器发现在 while 语句块中 flag 的值并没有发生变化,所以编译器在第一次从内存中取出 flag 后,后续并不会每次判断前都从内存中取值以进行比较,而是每次都使用第一次取出的值进行比较。在这种情况下,即使线程2改变了 flag 的值,线程1也还会处于等待状态。
使用 volatile 关键字就是为了告诉编译器,这个值随时有可能变化,让编译器每次都从内存中取值。在如上代码块中,如果在定义 flag 变量时使用了 volatile 关键字进行修饰,就不会出现线程1一直等待的问题。
规则5:不要注释掉代码
通常情况下,在以下两种情况下开发者会注释代码:
- 调试代码时先注释部分代码,调试完毕后再恢复;
- 修改代码时害怕修改后的代码没以前的好,先注释以前的代码,如果以后觉得以前的代码好,很容易进行恢复。
注释代码会给维护人员带来困惑:不知道被注释的代码是忘记恢复的代码还是不用的老代码。
如果害怕修改后的代码没以前的代码好,可以再修改前将代码提交到版本库中,利用版本管理器管理代码的历史版本。
规则6:使用固定宽度的类型
像 short、int、long 这些类型在不同的平台可能长度不一致,那么当代码中有使用这些变量时、有时候就会在移植代码时带来麻烦。开发者在编写代码的过程中应该使用固定宽度的数据类型(int8_t,int16_t,int32_t,int64_t),以方便代码的移植。(笔者注:在C99标准引进的stdint.h头文件中,定义以上了固定宽度的数据类型,同样的还有:uint8_t,uint16_t,uint32_t,uint64_t。 )
规则7:不要使用移位运算符操作有符号数
由于有符号位的存在,使用移位运算符操作有符号数时,并不是所有编译器都能够正确处理符号位,从而带来一些问题。
规则8:有符号和无符号类型不要混用
由于有符号数和无符号数的范围不同,可能会造成有数据丢失的风险。
规则9:尽量不要使用函数功能的宏
函数功能的宏看起来像一个函数,但实际上与函数又有些不同。函数功能的宏在使用过程中可能结果会和开发者预想的不一样。如果需要定义函数功能的宏时,尽量以内联函数代替。
例如如下代码是计算两个数的最大值的宏。
1
|
#define max(x, y) ((x)>(y)?(x):(y))
|
调用的代码如下所示:
1
|
n
=
(max
(
++i
, j
)
)
;
|
宏展开的结果如下所示:
1
|
n
=
(
(
(
++i
)
>
;
(j
)
?
(
++i
)
:
(j
)
)
)
;
|
可以看待,宏展开后,++i被执行了两次,而实际上只需要执行一次,那么得到的 n 的值也将是错误的。
规则10:每行只定义一个变量
在一行代码中定义多个变量会带来一些理解上的困难,甚至会造成一些Bug。例如在如下的代码中原本是要定义两个指针 p1 和 p2 ,而实际上 p2 并不是一个指针。
1
|
int
* p1
, p2
;
|