时间:2021-10-13 09:10:25 | 栏目:C代码 | 点击:次
0. 摘要
本文翻译自《Recommended C Style and Coding Standards》。
作者信息:
L.W. Cannon (Bell Labs)
R.A. Elliott (Bell Labs)
L.W. Kirchhoff (Bell Labs)
J.H. Miller (Bell Labs)
J.M. Milner (Bell Labs)
R.W. Mitze (Bell Labs)
E.P. Schan (Bell Labs)
N.O. Whittington (Bell Labs)
Henry Spencer (Zoology Computer Systems, University of Toronto)
David Keppel (EECS, UC Berkeley, CS&E, University of Washington)
Mark Brader (SoftQuad? Incorporated, Toronto)
本文是《Indian Hill C Style and Coding Standards》的更新版本,上面提到的最后三位作者对其进行了修改。本文主要介绍了一种C程序的推荐编码标准,内容着重于讲述编码风格,而不是功能 组织(Functional Organization)。
1. 简介
本文档修改于AT&T Indian Hill实验室内部成立的一个委员会的一份文档,旨在于建立一套通用的编码标准并推荐给Indian Hill社区。
本文主要讲述编码风格。良好的风格能够鼓励大家形成一致的代码布局,提高代码可移植性并且减少错误数量。
本文不关注功能组织,或是一些诸如如何使用goto的一般话题。我们尝试将之前的有关C代码风格的文档整合到一套统一的标准中,这套标准将适合于 任何使用C语言的工程,当然还是会有部分内容是针对一些特定系统的。另外不可避免地是这些标准仍然无法覆盖到所有情况。经验以及广泛的评价十分重 要,遇到特殊情况时,大家应该咨询有经验的C程序员,或者查看那些经验丰富的C程序员们的代码(最好遵循这些规则)。
本文中的标准本身并不是必需的,但个别机构或团体可能部分或全部采用该标准作为程序验收的一部分。因此,在你的机构中其他人很可能以一种相似的风 格编码。最终,这些标准的目的是提高可移植性,减少维护工作,尤其是提高代码的清晰度。
这里很多风格的选择都有些许武断。混合的编码风格比糟糕的编码风格更难于维护,所以当变更现有代码时,最好是保持与现有代码风格一致,而不是盲目 地遵循本文档中的规则。
"清晰的是专业的;不清晰的则是外行的" ― Sir Ernest Gowers
2. 文件组织
一个文件包含的各个部分应该用若干个空行分隔。虽然对源文件没有最大长度限制,但超过1000行的文件处理起来非常不方便。编辑器很可能没有足够 的临时空间来编辑这个文件,编译过程也会因此变得十分缓慢。与回滚到前面所花费的时间相比,那些仅仅呈现了极少量信息的多行星号是不值得的,我们 不鼓励使用。超过79列的行无法被所有的终端都很好地处理,应该尽可能的避免使用。过长的行会导致过深的缩进,这常常是一种代码组织不善的症状。
2.1 文件命名惯例
文件名由一个基础名、一个可选的句号以及后缀组成。名字的第一个字符应该是一个字母,并且所有字符(除了句号)都应该是小写的字母和数字。基础名 应该由八个或更少的字符组成,后缀应该由三个或更少的字符组成(四个,如果你包含句号的话)。这些规则对程序文件以及程序使用和产生的默认文件都 适用(例如,"rogue.sav")。
一些编译器和工具要求文件名符合特定的后缀命名约定。下面是后缀命名要求:
C源文件的名字必须以.c结尾
汇编源文件的名字必须以.s结尾
我们普遍遵循以下命名约定:
可重定位目标文件名以.o结尾
头文件名以.h结尾
在多语言环境中一个可供选择的更好的约定是用语言类型和.h共同作为后缀(例如,"foo.c.h" 或 "foo.ch")。
Yacc源文件名以.y结尾
Lex源文件名以.l结尾
C++使用编译器相关的后缀约定,包括.c,..c,.cc,.c.c以及.C。由于大多C代码也是C++代码,因此这里并没有一个明确的方案。
此外,我们一般约定使用"Makefile"(而不是"makefile")作为make(对于那些支持make的系统)工具的控制文件,并且使 用"README"作为简要描述目录内容或目录树的文件。
2.2 程序文件
下面是一个程序文件各个组成部分的推荐排列顺序:
文件的第一部分是一个序,用于说明该文件中的内容是什么。对文件中的对象(无论它们是函数,外部数据声明或定义,或是其他一些东西)用途的描述比 一个对象名字列表更加有用。这个序可选择地包含作者信息、修订控制信息以及参考资料等。
接下来是所有被包含的头文件。如果某个头文件被包含的理由不是那么显而易见,我们需要通过增加注释说明原因。大多数情况下,类似stdio.h这 样的系统头文件应该被放在用户自定义头文件的前面。
接下来是那些用于该文件的defines和typedefs。一个常规的顺序是先写常量宏、再写函数宏,最后是typedefs和枚举 (enums)定义。
接下来是全局(外部)数据声明,通常的顺序如下:外部变量,非静态(non-static)全局变量,静态全局变量。如果一组定义被用于部分特定 全局数据(如一个标志字),那么这些定义应该被放在对应数据声明后或嵌入到结构体声明中,并将这些定义缩进到其应用的声明的第一个关键字的下一个 层次(译注:实在没有搞懂后面这句的含义)。
最后是函数,函数应该以一种有意义的顺序排列。相似的函数应该放在一起。与深度优先(函数定义尽可能在他们的调用者前后)相比,我们应该首选广度 优先方法(抽象层次相似的函数放在一起)。这里需要相当多的判断。如果定义大量本质上无关的工具函数,可考虑按字母表顺序排列。
2.3 头文件
头文件是那些在编译之前由C预处理器包含在其他文件中的文件。诸如stdio.h的一些头文件被定义在系统级别,所有使用标准I/O库的程序必须 包含它们。头文件还用来包含数据声明和定义,这些数据不止一个程序需要。头文件应该按照功能组织,例如,独立子系统的声明应该放到独立的头文件 中。如果一组声明在代码从一种机器移植到另外一种机器时变动的可能性很大,那么这些声明也应该被放在独立的头文件中。
避免私有头文件的名字与标准库头文件的名字一样。下面语句:
当预期的头文件在当前目录下没有找到时,它将会包含标准库中的math头文件。如果这的确是你所期望发生的,那么请加上注释。包含头文件时不要使 用绝对路径。当从标准位置获取头文件时,请使用<name>包含头文件;或相对于当前路径定义它们。C编译器的"include- path"选项(在许多系统中为-l)是处理扩展私有库头文件的最好方法,它允许在不改变源码文件的情况下重新组织目录结构。
声明了函数或外部变量的头文件应该被那些定义了这些函数和变量的文件所包含。这样一来,编译器就可以做类型检查了,并且外部声明将总是与定义保持 一致。
在头文件中定义变量往往是个糟糕的想法,它经常是一个在文件间对代码进行低劣划分的症状。此外,在一次编译中,像typedef和经过初始化的数 据定义无法被编译器看到两次。在一些系统中,重复的没有使用extern关键字修饰的未初始化定义也会导致问题。当头文件嵌套时,会出现重复的声 明,这将导致编译失败。
头文件不应该嵌套。一个头文件的序应该描述其使用的其他被包含的头文件的实用特性。在极特殊情况下,当大量头文件需要被包含在多个不同的源文件中 时,可以被接受的做法是将公共的头文件包含在一个单独的头文件中。
一个通用的做法是将下面这段代码加入到每个头文件中以防止头文件被意外多次包含。
我们不应该对这种避免多次包含的机制产生依赖,特别是不应该因此而嵌套包含头文件。
2.4 其他文件
还有一个惯例就是编写一个名为"README"的文件,用于描述程序的整体情况以及问题。例如,我们经常在README包含程序所使用的条件编译 选项列表以及相关说明,还可以包含机器无关的文件列表等。
3. 注释
"当代码与注释不一致时,两者很可能都是错的" -- Norm Schryer
注释应该描述发生了什么,如何做的,参数的含义,使用和修改了哪些全局变量以及约束或Bugs。避免给那些本身很清晰的代码加注释,因为这些注释信息将很快的过时。注释与代码不一致将会带来负面影响。短小的注释应该是关于做什么的,比如"计算有意义的值",而不是关于"怎么做"的,例如"值的总和除以n"。C不是汇编;在头3-10行的区域添加注释,说明代码总体是做什么的,经常要比为每行添加注释说明微逻辑更加有用。
注释应该为那些令人不悦的代码作出"辩护"。辩护应该是这样的:如果使用正常的代码,一些糟糕的事情将会发生。但仅仅让代码运行的更快还不足以让这些hack代码显得合理化;而是应该将那些在不使用hack代码时令人无法接受的性能数据展示出来。注释应该对着写不可接受的行为作出解释,并告诉大家为什么使用Hack代码可以很好的解决这个问题。
那些用于描述数据结构,算法等的注释应该以块注释的形式存在。块注释起始以/*占据1-2列,*放在每行注释前面的第二列,块注释最后以占据2-3列的*/结尾。另外一个候选方案是每行注释文字前面用*占据1-2列,块注释以占据1-2列的*/收尾。
/*
** Alternate format for block comments
*/
将匹配文件中所有的注释。特别长的块注释,诸如持久讨论或版权声明,经常以占据1-2列的/*开始,每行注释文字前没有*,并最终以占据1-2列的*/结束。函数内部很适合使用块注释,块注释应该与其描述的代码拥有相同的缩进。独立的单行注释也应该与其说明的代码缩进一致。
4. 声明
全局声明应该从第一列开始。在所有外部数据声明的前面都应该放置extern关键字。如果一个外部变量是一个在定义时大小确定的数组,那么这个数 组界限必须在extern声明时显示指出,除非数组的大小与数组本身编码在一起了(例如,一个总是以0结尾的只读字符数组)。重复声明数组大小对 于一些使用他人编写的代码的人特别有益。
指针修饰符*应该与变量名在一起,而不是与类型在一起。
替换
后者是错误的,因为实际上t和u并未如预期那样被声明为指针。
不相关的声明,即使是相同类型的,也应该独立占据一行。我们应该对声明对象的角色进行注释,不过当常量名本身足以说明角色时,使用#define 定义的常量列表则不需要注释。通常多行变量名、值与注释使用相同缩进,使得他们在一列直线上。尽量使用Tab字符而不是空格。结构体和联合体的声 明时,每个元素应该单独占据一行,并附带一条注释。{应该与结构体的tag名放在同一行,}应该放在声明结尾的第一列。
/* defines for boat.type */
#define KETCH (1)
#define YAWL (2)
#define SLOOP (3)
#define SQRIG (4)
#define MOTOR (5)
最重要的类型应该被typedef,即使他们只是整型,因为独立的名字使得程序更加易读(如果只有很少的几个integer的typedef)。 结构体在声明时应该被typedef。保持结构体标志的名字与typedef后的名字相同。
"C语言的观点之一是程序员永远是对的" ― Michael DeCorte
5. 函数声明
每个函数前面应该放置一段块注释,概要描述该函数做什么以及(如果不是很清晰)如何使用该函数。重要的设计决策讨论以及副作用说明也适合放在注释 中。避免提供那些代码本身可以清晰提供的信息。
函数的返回类型应该单独占据一行,(可选的)缩进一个级别。不用使用默认返回类型int;如果函数没有返回值,那么将返回类型声明为void。如 果返回值需要大段详细的说明,可以在函数之前的注释中描述;否则可以在同一行中对返回类型进行注释。函数名(以及形式参数列表)应该被单独放在一 行,从第一列开始。目的(返回值)参数一般放在第一个参数位置(从左面开始)。所有形式参数声明、局部声明以及函数体中的代码都应该缩进一级。函 数体的开始括号应该单独一行,放在开始处的第一列。
每个参数都应该被声明(不要使用默认类型int)。通常函数中每个变量的角色都应该被描述清楚,我们可以在函数注释中描述,或如果每个声明单独一 行,我们可以将注释放在同一行上。像循环计数器"i",字符串指针"s"以及用于标识字符的整数类型"c"这些简单变量都无需注释。如果一组函数 都拥有一个相似的参数或局部变量,那么在所有函数中使用同一个名字来标识这个变量是很有益处的(相反,避免在相关函数中使用一个名字标识用途不同 的变量)。不同函数中的相似参数还应该放在各个参数列表中的相同位置。
参数和局部变量的注释应该统一缩进以排成一列。局部变量声明应用一个空行与函数语句分隔开来。
当你使用或声明变长参数的函数时要小心。目前在C中尚没有真正可移植的方式处理变长参数。最好设计一个使用固定个数参数的接口。如果一定要使用变 长参数,请使用标准库中的宏来声明具有变长参数的函数。
如果函数使用了在文件中没有进行全局声明的外部变量(或函数),我们应该在函数体内部使用extern关键字单独对这些变量进行声明。
避免局部声明覆盖高级别的声明。尤其是,局部变量不应该在嵌套代码块中被重声明。虽然这在C中是合法的,但是当使用-h选项时,潜在的冲突可能性 足以让lint工具发出抱怨之声。