C语言的印第安山风格及编码标准
贝尔实验室
Henry Spencer
[说明]:英语四级略过的水平的翻译,希望在令人捉急的英语阅读水平上进步。在此路懒跑的同时也会不禁地惭愧着。《Indian Hill C Style(印第安山风格指南)》英文版。
摘要
此文档是跟原稿同名的含注释的版本。此文档推荐了一套UNIX官方支持的编码标准。此文档只涉及编码风格,不含具体的功能实现、组织。
1990年4月21日
此文档诞生于在印第安山召开的委员会,此次委员会的目标是为印第安山团队制定一套通用编码风格。编码风格中只讲解了编码风格,并未对程序功能实现做介绍。此文档中的标准不是只适用于ESS的程序。在此标准中,我们还整合之前在C风格之上做的工作,这套标准适合于任何使用C编程的工程。
组成文件的各模块需要用空白行隔开。虽然对源文件的最大长度无规定,但遇到一个超过1500行的文件时可能会感觉难处理。除此之外,编辑器也可能没有足够的临时空间来编辑文件,编辑速度就会变慢。因为我们大多数都使用300波特的终端,不建议整行整行地充满着星号。如果每行超过80列也不会被终端处理得很好,应尽可能的避免这样的情况。
建议源文件中各模块的顺序如下:
1. 任何头文件的包含应该放在文件的最前面。
2. 在包含头文件模块之后紧接着应该是叙述本文件的内容。简介一下本文件一些对象的作用(如函数,外部数据变量或定义,或其它),这样的简介比对象名称更有用。
3. 类型定义(typedef)和宏定义作为一个整体的模块作为文件第三个模块。
4. 接下来的模块是全局(或外部external)变量的声明。如果有特殊的宏定义用作全局变量(如标志变量),则应该将这些宏紧跟在变量声明之后定义。
5. 函数作为文件的最后一个模块。[ 函数间最好按照某种意义的顺序进行定义,一般来说,“自上而下”要比“自下而上”的设计方法好,“宽度优先”(被调用层次相近的函数排在一块)比“深度优先”(尽可能将被调用的函数写在前面)的方法更令人喜欢。值得考虑的是,如果要定义大量的、重要的工具函数,用字符顺序排列这些函数][2014.6.20]
UNIX要求文件名要有确定的后缀,这样才能被cc命令处理。需要的后缀列举如下:
此外,还通常遵循以下的约定:
头文件被包含在其它文件前面,这些包含头文件的语句被C预处理编辑(处理)。有些头文件的定义处于系统级别(库头文件)。如stdio.h文件,如果程序中用到了标准的I/O库函数,那在源程序中一定要包含stdio.h。如果有的数据不止被一个文件中的程序用到,头文件也被用来定义和声明这些数据。需要合理的组织头文件,例如,针对单个的子系统的声明要分开定义在不同的头文件中。还要值得注意的是,当代码从一个平台移植到另一个平台上时,那些需要修改的声明要单独写在头文件中。
头文件不应该被嵌套包含。在同一个文件中,对编译器来说,像类型定义(typedef)和初始化只能出现一次。在除UNIX系统之上,对于那些未初始化的没有extern修饰符的声明也只能出现一次。在这种情况下,如果头文件被嵌套包含,编译就会失败。
外部声明的语句需要从第一列开始。每个声明需要独占一行。除了具有能够明显表征其作用命名的常量不需要注释外,其它的声明需要一个简单的注释来说明其作用。注释应该用标签让注释彼此像队列一样有序。用tab来代替空格。对于结构体和联合的声明,其内的每个元素应该独占一行,且应该伴随着一个注释来描述这个变量的作用。左大括号({)应该跟结构体标记名同行,右大括号(})应该独占一行且在第一列。举个例子如下:
struct boat { int wllength; /* water line length in feet */ int type; /* see below */ long sarea; /* sail area in square feet */ };
/* * defines for boat.type */ #define KETCH 1 #define YAWL 2 #define SLOOP 3 #define SQRIG 4 #define MOTOR 5
如果一个外部变量已经被初始化(对于全局变量来说,对未初始化的变量给予0作为初始值),那么等号不可被省略。
int x = 1; char *msg = "message"; struct boat winner = { 40, /* water line length */ YAWL, 600 /* sail area */ };
注释用来解释数据结构、算法等。注释内容在/*和*/间。注释从第一列以/*开始,以后的每一行的注释内容在*之后,*从第二列开始。注释以*/结束,分别被列在2-3列。
例:
/* * Here is a block comment. * The comment text should be tabbed over(*后可用空格不用tab) * and the opening /* and closing star-slash * should be alone on a line. */
注意命令 grep ^.\*可以捕捉到文件中的所有注释快。在某些情形下,块注释可以适当的用在函数内部,注释需要跟所注释的代码保持相同的缩进。短的注释需要单独成行,被注释的代码紧跟注释。
例:
if (argc > 1) { /* Get input file from command line. */ if (freopen(argv[1], "r", stdin) == NULL) error("can’t open %s\n", argv[1]); }
很短的注释可以紧跟在代码后与代码处于同一行,但是需要用缩进来将它与代码隔开。如果这样的注释不止一个,那么它们之间应该要有相同的缩进设置。
例:
if (a == 2) return(TRUE); /* special case */ else return(isprime(a)); /* works only for odd a */
[2014.6.23]
在每个函数前都应该给出一段注释来给出函数名并简要描述此函数的功能。如果函数有返回值,函数的返回类型应当从第一列开始独占一行(不要省略其返回值类型,让系统默认函数的返回值为int)。如果函数没有返回值,那么函数前就不应有返回类型。如果函数返回值为一long类型,那么在注释中应该说明这一点;或者将long作为返回类型。函数名和参数应从第一列起独占一行。每个参数需要被准确的声明(不要将其默认为int类型),并且用一行来注释它。函数体的大括号({)也需要从第一列起独占一行。函数名、参数列表和大括号之间要隔一空行[这个没怎么被采纳]。局部变量和函数内的代码至少需要一个tab缩进。
如果函数用了任何的外部变量,它们应该在函数内用extern关键字来被声明。如果外部变量是一个数组,那么数组的维数一定要包含在声明内。在函数内内调用外部函数也要用extern关键字来声明,这对调用别人写的函数非常有好处。如果一个函数的返回值非整型(int),那么编译器就会要求在调用这些函数之前对这些函数进行声明。用extern对被调用函数进行声明就可以避免这个问题。
一般说来,声明在函数内的每个变量需要独占一行并用注释来描述此变量的作用。如果这些变量是外部变量或者是要经本函数改变值的指针参数,需要在注释里面特别说明这一点。对参数和局部变量的注释需要缩进对其,让它们看起来很整齐。所有的声明和函数内的语句用一空行隔开。
局部变量不可在局部区域内嵌套声明,虽然C语言支持这个特性。潜在的困惑是当给lint-h参数时它会抱怨。
/* * skyblue() * * Determine if the sky is blue. */ int /* TRUE or FALSE */ skyblue() { extern int hour; if (hour < MORNING ???hour > EVENING) return(FALSE); /* black */ else return(TRUE); /* blue */ } /* * tail(nodep) * * Find the last element in the linked list * pointed to by nodep and return a pointer to it. */ NODE * /* pointer to tail of list */ tail(nodep) NODE *nodep; /* pointer to head of list */ { register NODE *np; /* current pointer advances to NULL */ register NODE *lp; /* last pointer follows np */ np = lp = nodep; while ((np = np->next) != NULL) lp = np; return(lp); }
符合语句是在括号({})内的语句组成的。符合语句需要用一个或者更多的tab来进行缩进。左括号({)需要在符合语句末尾,右括号(})需要独占一行。在括号内的符合语句需要缩进。注意,函数的左括号是独占一行的。
if (expr) { statement; statement; } if (expr) { statement; statement; } else { statement; statement; }
注意,在else和do-while中while语句(如下例)前是右括号不独占一行的唯一位置。
for (i = 0; i < MAX; i++) { statement; statement; } while (expr) { statement; statement; } do { statement; statement; } while (expr); switch (expr) { case ABC: case DEF: statement; break; case XYZ: statement; break; default: statement; break; //严格来说,这个break完全没有必要。但它又是必要的,为了避免default后被加了case语句。 }
注意当多重case被使用时,每个case都需要独占一行。C中switch中会执行所有case中包含的情况(case后无break)很少,如果这样用了switch一定要注释清楚供远维护。
if (strcmp(reply, "yes") == EQUAL) { statements for yes ... } else if (strcmp(reply, "no") == EQUAL) { statements for no ... } else if (strcmp(reply, "maybe") == EQUAL) { statements for maybe ... } else { statements for none of the above ... }
最后这个例子就是广义下的switch语句,从缩进可以看出switch一个或者多个情况是可以选择的而不是只有使用嵌套的选择。[2014.6.24]
老版本的复合等于运算符=+、+-、+*等已经不能用,与之代替的是+=、-=、*=等符号。 除了‘.’和‘->’外,所有的二元运算符都需要用空格将其和操作数隔开。另外,如果关键字后面紧跟带括号的表达式,则关键字后面需要有一个空格。参数列表中的逗号后面也需要用空格以形象的区分开各参数。另一方面,带参数的宏名和函数名与括号之间不能有空格。特别的,C预处理要求左括号紧跟宏名之后,否则参数将不会被正确识别。一元运算符不能跟它们的操作数分开。由于C运算符有比较复杂的优先级,所以在混用各运算符时最好用括号来表示优先级。
例子:
a += c + d; a = (a + b) / (c * d); strp->field = str.fl - ((x & MASK) >> DISP); while (*d++ = *s++) ; /* EMPTY BODY */
毫无疑问,个人的工程有个人的命名约定。但这里还是有一些普遍的规则:
不可将数值常量直接写在程序中(起码也要注释一下此数值的来源)。C语言预处理define所定义的标识符需要有一个明确意义的命名。常量的修改可以通过统一修改宏值方式,这样对于较大的程序来说更方便维护。由于lint可以实现类型检查,当程序中需要离散常量的时候可以优先的选择枚举类型。
0和1两个常量需要直接在程序中出现,而非用宏的方式代替。举个例子,如果对于for循环的初始值
for (i = 0; i < ARYBOUND; i++)
这样用0值是合理的。但
fptr = fopen(filename, "r"); if (fptr == 0) error("can’t open %s\n", filename);这个例子中的0就使用的不合理。NULL是被定义在标准I/O库stdio.h头文件中的,在这里NULL需要代替掉0。
Lint是C程序的一个检查助手,它能够检查并报出C源文件中存在的类型不兼容、函数和调用之间的不一致性及程序中一些潜在的BUGS等。工程中的程序使用lint作为官方性的一个过程很被期待。另外,在5521部门修改lint的工作一直在进行,这样它就能够持续的为想要复合C标准的C语言程序服务。
Lint能够检查出目前给的所有标准还言之尚早。在一些情况里比如注释是否令人误解或者不正确还需要人为操作与判断。像另外的一些情况像检查函数后的大括号是否从第一列起独占一行,这项测试已经被添进去了。当遇到新的问题时还会往lint中添加新的功能并公告。
注意在程序中使用lint的最好方式不是为了让程序让官方接受而将其当成一个屏障来克服,而是当代码有较大变动时将其当成一个工具来使用。Lint能够在问题发生前找到模糊的BUGS并且能够确认程序的可行性。
这个模块包含一些比较混杂的关于做和不做的事项。
while ((c = getchar()) != EOF) { process the character }
采用嵌入式的语句来提高运行时性能也是很有可能的。当人为的使用嵌入语句时但需要权衡程序运行速度的增加与程序可维护性的减少的结果,例如以下的代码:
a = b + c; d = a + r;
不能替换为d = (a = b + c)+ r;
尽管后面的代码可能能够节约一个周期的运行时间。从长远来看,与两者可维护差异相比,两者之间的时差将减少收益度。
for (...) for (...) { ... if (disaster) goto error; } ... error: clean up the mess
当goto语句必要时,与其伴随的标签要单独占一行并且需要缩进一个tab位置来搭配紧跟的代码。
个人的工程也许希望能够成立除这里提到的额外的标准。以下提到的就是应被每个工程管理组组重视的问题。[2014.6.27]
一系列与C相关的标准已经以上文字呈现。最重要的一点事能够合理的使用空格及注释来使源文件代码具有清晰的布局。需要记住的另一点是,当写程序时一定要想到代码能方便别人的维护和修改,以及它会运行到不同的平台之上。
作为任何的标准来说,如果它有用就可以使用它。印第安山的lint将强制性的要求这些标准义务的进行自动检查。当使用这些标准遇到问题时请不要直接忽略它。在印第安山的程序员将他们的问题带给软件开发集团5522部门。在印度安山区域外的程序员可以联系处理器应用组5512部门。
[1] B.A. Tague, "C Language Portability", Sept 22, 1977. This document issued by department 8234 contains three memos by R.C.Haight, A.L. Glasser, and T.L. Lyon dealing with style and portability.
[2] S.C. Johnson, "Lint, aC Program Checker", Technical Memorandum, 77-1273-14, September 16,1977.
[3] R.W. Mitze, "The3B/PDP-11 Swabbing Problem", Memorandum for File, 1273-770907.01MF,September14, 1977.
[4] R.A. Elliott and D.C.Pfeffer, "3B Processor Common Diagnostic Standards- Version 1",Memorandum for File, 5514-780330.01MF,March 30, 1978.
[5] R.W. Mitze, "AnOverview of C Compilation of UNIX User Processes on the 3B", Memorandumfor File, 5521-780329.02MF, March29, 1978.
[6] B.W. Kernighan and D.M. Ritchie, The C Programming Language, Prentice-Hall 1978.
翻译水平差。有的C术语还不清楚。还急躁。还只翻译一遍。[2014.6.28-14:44]
T Note Over.