Recommended C Style and Coding Standards中文翻译版第2/3页
6. 空格
o, world!\n",'/'/'/'));}read(j,i,p){write(j/p+p,i---j,i/i);}
- 不光彩的事情,模糊C代码大赛,1984年。作者要求匿名。
通常情况下,请使用纵向和横向的空白。缩进和空格应该反映代码的块结构。例如,在一个函数定义与下一个函数的注释之间,至少应该有两行空白。
如果一个条件分支语句过长,那就应该将它拆分成若干单独的行。
if (foo->next==NULL && totalcount<needed && needed<=MAX_ALLOT
&& server_active(current_input)) { ...
也许下面这样更好
if (foo->next == NULL
&& totalcount < needed && needed <= MAX_ALLOT
&& server_active(current_input))
{
...
类似地,复杂的循环条件也应该被拆分为不同行。
for (curr = *listp, trail = listp;
curr != NULL;
trail = &(curr->next), curr = curr->next )
{
...
其他复杂的表达式,尤其是那些使用了?:操作符的表达式,最好也能拆分成多行。
c = (a == b)
? d + f(a)
: f(b) - d;
当关键字后面有放在括号内的表达式时,应该使用空格将关键字与左括号分隔(sizeof操作符是个例外)。在参数列表中,我们也应该使用空格显式 的将各个参数隔开。然而,带有参数的宏定义一定不能在名字与左括号间插入空格,否则C预编译器将无法识别后面的参数列表。
7. 例子
* Determine if the sky is blue by checking that it isn't night.
* CAVEAT: Only sometimes right. May return TRUE when the answer
* is FALSE. Consider clouds, eclipses, short days.
* NOTE: Uses 'hour' from 'hightime.c'. Returns 'int' for
* compatibility with the old version.
*/
int /* true or false */
skyblue()
{
extern int hour; /* current hour of the day */
return (hour >= MORNING && hour <= EVENING);
}
/*
* Find the last element in the linked list
* pointed to by nodep and return a pointer to it.
* Return NULL if there is no last element.
*/
node_t *
tail(nodep)
node_t *nodep; /* pointer to head of list */
{
register node_t *np; /* advances to NULL */
register node_t *lp; /* follows one behind np */
if (nodep == NULL)
return (NULL);
for (np = lp = nodep; np != NULL; lp = np, np = np->next)
; /* VOID */
return (lp);
}
8. 简单语句
每行只应该有一条语句,除非多条语句关联特别紧密。
case FOO: oogle (zork); boogle (zork); break;
case BAR: oogle (bork); boogle (zork); break;
case BAZ: oogle (gork); boogle (bork); break;
for或while循环语句的空体应该单独放在一行并加上注释,这样可以清晰的看出空体是有意而为,并非遗漏代码。
while (*dest++ = *src++)
; /* VOID */
不要对非零表达式进行默认测试,例如:
if (f() != FAIL)
比下面的代码更好
if (f())
即使FAIL的值可能为0(在C中0被认为是假)。当后续有人决定使用-1替代0作为失败返回值时,一个显式的测试将解决你的问题。即使比较的值永远不会改变,我们也应该使用显式的比较;例如
if (!(bufsize % sizeof(int)))
应该被写成
if ((bufsize % sizeof(int)) == 0)
这样可以反映这个测试的数值(非布尔)本质。一个常见的错误点是使用strcmp测试字符串是否相同,这个测试的结果永远不应该被放弃。比较好的方法是定义一个宏STREQ。
#define STREQ(a, b) (strcmp((a), (b)) == 0)
对谓词或满足下面约束的表达式,非零测试经常被放弃:
0表示假,其他都为真。
通过其命名可以看出返回真是显而易见的。
用isvalid或valid称呼一个谓词,不要用checkvalid。
一个非常常见的实践就是在一个全局头文件中声明一个布尔类型"bool"。这个特殊的名字可以极大地提高代码可读性。
typedef int bool;
#define FALSE 0
#define TRUE 1
或
typedef enum { NO=0, YES } bool;
即便有了这些声明,也不要检查一个布尔值与1(TRUE,YES等)的相当性;可用测试与0(FALSE,NO等)的不等性替代。绝大多数函数都可以保证为假的时候返回0,但为真的时候只返回非零。
if (func() == TRUE) { ...
必须被写成
if (func() != FALSE) { ...
如果可能的话,最好为函数/变量重命名或者重写这个表达式,这样就可以显而易见的知道其含义,而无需再与true或false比较了(例如,重命名为isvalid())。
嵌入赋值语句也有用武之地。在一些结构中,在没有降低代码可读性的前提下,没有比这更好的方式来实现这个结果了。
while ((c = getchar()) != EOF) {
process the character
}
++和--操作符可算作是赋值语句。这样,为了某些意图,实现带有副作用的功能。使用嵌入赋值语句也可能提高运行时的性能。不过,大家应该在提高的性能与下降的可维护性之间做好权衡。当在一些人为的地方使用嵌入赋值语句时,这种情况会发生,例如:
a = b + c;
d = a + r;
不应该被下面代码替代:
d = (a = b + c) + r;
即使后者可能节省一个计算周期。在长期运行时,由于优化器渐获成熟,两者的运行时间差距将下降,而两者在维护性方面的差异将提高,因为人类的记忆会随着时间的流逝而衰退。
在任何结构良好的代码中,goto语句都应该保守地使用。使用goto带来好处最大的地方是从switch、for和while多层嵌套中跳出,但这样做的需求也暗示了代码的内层结构应该被抽取出来放到一个单独的返回值为成功或失败的函数中。
for (...) {
while (...) {
...
if (disaster)
goto error;
}
}
...
error:
clean up the mess
当需要goto时候,其对应的标签应该被放在单独一行,并且后续的代码缩进一级。使用goto语句时应该增加注释(可能放在代码块的头)以说明它的功用和目的。continue应该保守地使用,并且尽可能靠近循环的顶部。Break的麻烦比较少。
非原型函数的参数有时需要被显式做类型提升。例如,如果函数期望一个32bit的长整型,但却被传入一个16bit的整型数,可能会导致函数栈不对齐。指针,整型和浮点值都会发生此问题。
9. 复合语句
复合语句是一个由括号括起来的语句列表。有许多种常见的括号格式化方式。如果你有一个本地标准,那请你与本地标准保持一致,或选择一个标准,并持续地使用它。在编辑别人的代码时,始终使用那些代码中使用的样式。
control {
statement;
statement;
}
上面的风格被称为"K&R风格",如果你还没有找到一个自己喜欢的风格,那么可以优先考虑这个风格。在K&R风格中,if-else语句中的else部分以及do-while语句中的while部分应该与结尾大括号在同一行中。而其他大部分风格中,大括号都是单独占据一行的。
当一个代码块拥有多个标签时,每个标签应该单独放在一行上。必须为C语言的switch语句的fall-through特性(即在代码段与下一个case语句之前间没有break)增加注释以利于后期更好的维护。最好是lint风格的注释/指示。
switch (expr) {
case ABC:
case DEF:
statement;
break;
case UVW:
statement;
/*FALLTHROUGH*/
case XYZ:
statement;
break;
}
这里,最后那个break是不必要的,但却是必须的,因为如果后续另外一个case添加到最后一个case的后面时,它将阻止fall-through错误的发生。如果使用default case,那么应该该default case放在最后,且不需要break,如果它是最后一个case。
一旦一个if-else语句在if或else段中包含一个复合语句,if和else两个段都应该用括号括上(称为全括号(fully bracketed)语法)。
if (expr) {
statement;
} else {
statement;
statement;
}
在如下面那样的没有第二个else的if-if-else语句序列里,括号也是不必可少的。如果ex1后面的括号被省略,编译器解析将出错:
if (ex1) {
if (ex2) {
funca();
}
} else {
funcb();
}
一个带else if的if-else语句在书写上应该让else条件左对齐。
if (STREQ (reply, "yes")) {
statements for yes
...
} else if (STREQ (reply, "no")) {
...
} else if (STREQ (reply, "maybe")) {
...
} else {
statements for default
...
}
这种格式看起来像一个通用的switch语句,并且缩进反映了在这些候选语句间的精确切换,而不是嵌套的语句。
Do-while循环总是使用括号将循环体括上。
下面的代码非常危险:
#ifdef CIRCUIT
# define CLOSE_CIRCUIT(circno) { close_circ(circno); }
#else
# define CLOSE_CIRCUIT(circno)
#endif
...
if (expr)
statement;
else
CLOSE_CIRCUIT(x)
++i;
注意,在CIRCUIT没有定义的系统上,语句++i仅仅在expr是假的时候获得执行。这个例子指出宏用大写命名的价值,以及让代码完全括号化的价值。
有些时候,通过break,continue,goto或return,if可以无条件地进行控制转移。else应该是隐式的,并且代码不应该缩进。
if (level > limit)
return (OVERFLOW)
normal();
return (level);
平坦的缩进告诉读者布尔测试在密封块的其他部分是保持不变的。
10. 操作符
一元操作符不应该与其唯一的操作数分开。通常,所有其他二元操作符都应该使用空白与其操作树分隔开,但'.'和'->'例外。当遇到复杂表达式的时候我们需要做出一些判断。如果内层操作符没有使用空白分隔而外层使用了,那么表达式也许会更清晰些。
如果你认为一个表达式很难于阅读,可以考虑将这个表达式拆分为多行。在接近中断点的最低优先级操作符处拆分是最好的选择。由于C具有一些想不到的优先级规则,混合使用操作符的表达式应该使用括号括上。但是过多的括号也会使得代码可读性变差,因为人类不擅长做括号匹配。
二元逗号操作符也会被使用到,但通常我们应该避免使用它。逗号操作符的最大用途是提供多元初始化或操作,比如在for循环语句中。复杂表达式,例如那些使用了嵌套三元?:操作符的表达式,可能引起困惑,并且应该尽可能的避免使用。三元操作符和逗号操作符在一些使用宏的地方很有用,诸如getchar。在三元操作符?:前的逻辑表达式的操作数应该被括起来,并且两个子表达式的返回值应该是相同类型。
11. 命名约定
毫无疑问,每个独立的工程都有一套自己的命名约定,不过仍然有一些通用的规则值得参考。
1).为系统用途保留以下划线开头或下划线结尾的名字,并且这些名字不应该被用在任何用户自定义的名字中。大多数系统使用这些名字用于用户不应 该也不需知道的名字中。如果你一定要使用你自己私有的标识符,可以用标识它们归属的包的字母作为开头。
2).#define定义的常量名字应该全部大写。
3).Enum常量应该大写或全部大写。
4).函数名、typedef名,变量名以及结构体、联合体与枚举标志的名字应该用小写字母。
5).很多"宏函数"都是全部大写的。一些宏(诸如getchar和putchar)使用小写字母命名,这事因为他们可能被当成函数使用。只有在宏的行为类似一 个函数调用时才允许小写命名的宏,也就是说它们只对其参数进行一次求值,并且不会给具名形式参数赋值。有些时候我们无法编写出一个具有函数行为的 宏,即使其参数也只是求值一次。
6).避免在同一情形下使用不同命名方式,比如foo和Foo。同样避免foobar和foo_bar这种方式。需要考虑这样所带来的困惑。
7).同样,避免使用看起来相似的名字。在很多终端以及打印设备上,'I'、'1'和'l'非常相似。给变量命名为l特别糟糕,因为它看起来十分像常量'1'。
通常,全局名字(包括enum)应该具有一个统一的前缀,通过该前缀名我们可以识别出这个名字归属于哪个模块。全局变量可以选择汇集在一个全局结 构中。typedef的名字通常在结尾加一个't'。
避免名字与各种标准库中的名字冲突。一些系统可能包含一些你所不需要的库。另外你的程序将来某天很可能也要扩展。
12. 常量
数值型常量不应该被硬编码到源文件中。应该使用C预处理器的#define特性为常量赋予一个有意义的名字。符号化的常量可以让代码具有更好的可读性。在一处地方统一定义这些值也便于进行大型程序的管理,这样常量值可以在一个地方进行统一修改,只需修改define的值即可。枚举数据类型更适合声明一组具有离散值的变量,并且编译器还可以对其进行额外的类型检查。至少,任何硬编码的值常量必须具有一段注释,以说明该值的来历。
常量的定义应该与其使用是一致的;例如使用540.0作为一个浮点数,而不是使用540外加一个隐式的float类型转换。有些时候常量0和1被直接使用而没有用define进行定义。例如,一个for循环语句中用于标识数组下标的常量,
for (i = 0; i < ARYBOUND; i++)
上面代码是合理的,但下面代码
door_t *front_door = opens(door[i], 7);
if (front_door == 0)
error("can't open %s\\\\n", door[i]);
是不合理的。在最后的那个例子中,front_door是一个指针。当一个值是指针的时候,它应该与NULL比较而不是与0比较。NULL被定义在标准I/O库头文件stdio.h中,在一些新系统中它在stdlib.h中定义。即使像1或0这样的简单值,我们最好也用define定义成TRUE和FALSE定义后再使用(有些时候,使用YES和NO可读性更好)。
简单字符常量应该被定义成字面值,不应该使用数字。不鼓励使用非可见文本字符,因为它们是不可移植的。如果非可见文本字符十分必要,尤其是当它们在字符串中使用时,它们应该定义成三个八进制数字的转义字符(例如: '\007‘)而非一个字符。即使这样,这种用法也应该考虑其机器相关性,并按这里的方法处理。
13. 宏
复杂表达式可能会被用作宏参数,这可能会因操作符优先级顺序而引发问题,除非宏定义中所有参数出现的位置都用括号括上了。对这种因参数内副作用而引发的问题,我们似乎也无能为例,除了在编写表达式时杜绝副作用(无论如何,这都是一个很好的主意)。如果可能的话,尽量在宏定义中对宏参数只进行一次求值。有很多时候我们无法写出一个可像函数一样使用的宏。
一些宏也当成函数使用(例如,getc和fgetc)。这些宏会被用于实现其他函数,这样一旦宏自身发生变化,使用该宏的函数也会受到影响。在交换宏和函数时务必要小心,因为函数参数是按值传递的,而宏参数则是通过名称替换。只有在宏定义时特别谨慎小心,才有可能减少使用宏时的担心。
宏定义中应该避免使用全局变量,因为全局变量的名字很可能被局部声明遮盖。对于那些对具名参数进行修改(不是这些参数所指向的存储区域)或被用作赋值语句左值的宏,我们应该添加相应的注释以给予提醒。那些不带参数但引用变量,或过长或作为函数别名的宏应该使用空参数列表,例如:
#define OFF_A() (a_global+OFFSET)
#define BORK() (zork())
#define SP3() if (b) { int x; av = f (&x); bv += x; }
宏节省了函数调用和返回的额外开销,但当一个宏过长时,函数调用和返回的额外开销就变得微不足道了,这种情况下我们应该使用函数。
在一些情况下,让编译器确保宏在使用时应该以分号结尾是很有必要的。
if (x==3)
SP3();
else
BORK();
如果省略SP3调用后面的分号,后面的else将会匹配到SP3宏中的那个if。有了分号,else分支就不会与任何if匹配。SP3宏可以这样安全地实现:
#define SP3() \\\\
do { if (b) { int x; av = f (&x); bv += x; }} while (0)
手工给宏定以加上do-while包围看起来很别扭,而且很多编译器和工具会抱怨在while条件是一个常量值。一个用来声明语句的宏可以使得编码更加容易:
#ifdef lint
static int ZERO;
#else
# define ZERO 0
#endif
#define STMT( stuff ) do { stuff } while (ZERO)
我们可以用下面代码来声明SP3宏:
#define SP3() \\\\
STMT( if (b) { int x; av = f (&x); bv += x; } )
使用STMT宏可以有效阻止一些可以潜在改变程序行为的打印排版错误。
除了类型转换、sizeof以及上面那些技巧和手法,只有当整个宏用括号括上时才应该包含关键字。
14. 条件编译
条件编译在处理机器依赖、调试以及编译阶段设定特定选项时十分有用。不过要小心条件编译。各种控制很容易以一种无法预料的方式结合在一起。如果使用#ifdef判断机器依赖,请确保当没有机器类型适配时,返回一个错误,而不是使用默认机器类型(使用#error并缩进一级,这样它可以一些老旧的编译器下工作)。如果你#ifdef优化选项,默认情况下应该是一个未经优化的代码,而不是一个不兼容的程序。确保测试的是未经优化的代码。
注意在#ifdef区域内的文本可能会被编译器扫描(处理),即使#ifdef求值的结果为假。但即使文件的#ifdef部分永远不能被编译到(例如,#ifdef COMMENT),这部分也不该随意的放置文本。
尽可能地将#ifdefs放在头文件中,而不是源文件中。使用#ifdef定义可以在源码中统一使用的宏。例如,一个用于检查内存分配的头文件可能这样实现:(省略了REALLOC和FREE):
#ifdef DEBUG
extern void *mm_malloc();
# define MALLOC(size) (mm_malloc(size))
#else
extern void *malloc();
# define MALLOC(size) (malloc(size))
#endif
条件编译通常应该基于一个接一个的特性的。多数情况下,都应该避免使用机器或操作系统依赖。
#ifdef BSD4
long t = time ((long *)NULL);
#endif
上面代码之所以糟糕有两个原因:很可能在某个4BSD系统上有更好的选择,并且也可能存在在某个非4BSD系统中上述代码是最佳代码。我们可以通过定义诸如TIME_LONG和TIME_STRUCTD等宏作为替代,并且在诸如config.h的配置文件中定义一个合适的宏。
15. 调试
"C代码。C代码运行。运行,代码,运行... 请运行!!!" -- Barbara Tongue
如果你使用枚举,第一个枚举常量应该是一个非零值,或者第一个常量应该指示一个错误。
enum { STATE_ERR, STATE_START, STATE_NORMAL, STATE_END } state_t;
enum { VAL_NEW=1, VAL_NORMAL, VAL_DYING, VAL_DEAD } value_t;
未初始化的值后续将会自己获取。
检查所有错误返回值,即使是那些"不能"失败的函数的返回值。考虑即使之前所有的文件操作都已经成功了,close()和fclose也可能失败。编写你自己的函数,使得它们以一种明确的方式测试错误、返回错误码或从程序中退出。包含大量调试和错误检查代码,并把其中大多数留在最终的产品中。甚至检查那些"不可能"的错误。
使用assert机制保证传给每个函数的值都是定义明确的,并且中间结果是形式良好的。
尽可能少的在调试代码中使用#ifdef。例如,如果mm_malloc是一个调试用的内存分配器,那么MALLOC将挑选合适的分配器,避免使用#ifdef在代码中堆砌垃圾,并且使得分配之间的差异变得清晰,只是在调试期会分配些额外内存。
#ifdef DEBUG
# define MALLOC(size) (mm_malloc(size))
#else
# define MALLOC(size) (malloc(size))
#endif
对那些"不可能"溢出的对象做边界校验。一个向变长存储区写入的函数应该接受一个参数maxsize,该参数即目标内存区域的大小。如果有时候目标内存区域大小未知,一些maxsize的"魔数"值应该意味着"没有边界检查"。当边界检查失败,请确保这个函数做一些有用的事情,诸如退出程序或返回一个错误状态。
/*
* INPUT: A null-terminated source string `src' to copy from and
* a `dest' string to copy to. `maxsize' is the size of `dest'
* or UINT_MAX if the size is not known. `src' and `dest' must
* both be shorter than UINT_MAX, and `src' must be no longer than
* `dest'.
* OUTPUT: The address of `dest' or NULL if the copy fails.
* `dest' is modified even when the copy fails.
*/
char *
copy (dest, maxsize, src)
char *dest, *src;
unsigned maxsize;
{
char *dp = dest;
while (maxsize\-\- > 0)
if ((*dp++ = *src++) == '\\\\0')
return (dest);
return (NULL);
}
总之,记住一个程序产生错误答案的速度快两倍(译注:是否有南辕北辙的意味),实则是变得无限缓慢,这个道理对那些偶尔崩溃或打击有效数据的程序同样成立。
最新评论