[C] Indian Hill C Style(印第安山风格指南)

时间:2023-02-01 00:07:08

C语言的印第安山风格及编码标准

贝尔实验室

Henry Spencer

[说明]:英语四级略过的水平的翻译,希望在令人捉急的英语阅读水平上进步。在此路懒跑的同时也会不禁地惭愧着。《Indian Hill C Style(印第安山风格指南)》英文版


摘要

此文档是跟原稿同名的含注释的版本。此文档推荐了一套UNIX官方支持的编码标准。此文档只涉及编码风格,不含具体的功能实现、组织。

1990年4月21日


1. 简介

此文档诞生于在印第安山召开的委员会,此次委员会的目标是为印第安山团队制定一套通用编码风格。编码风格中只讲解了编码风格,并未对程序功能实现做介绍。此文档中的标准不是只适用于ESS的程序。在此标准中,我们还整合之前在C风格之上做的工作,这套标准适合于任何使用C编程的工程。


2. 文件组织

组成文件的各模块需要用空白行隔开。虽然对源文件的最大长度无规定,但遇到一个超过1500行的文件时可能会感觉难处理。除此之外,编辑器也可能没有足够的临时空间来编辑文件,编辑速度就会变慢。因为我们大多数都使用300波特的终端,不建议整行整行地充满着星号。如果每行超过80列也不会被终端处理得很好,应尽可能的避免这样的情况。


建议源文件中各模块的顺序如下:

1.      任何头文件的包含应该放在文件的最前面。

2.      在包含头文件模块之后紧接着应该是叙述本文件的内容。简介一下本文件一些对象的作用(如函数,外部数据变量或定义,或其它),这样的简介比对象名称更有用。

3.      类型定义(typedef)和宏定义作为一个整体的模块作为文件第三个模块。

4.      接下来的模块是全局(或外部external)变量的声明。如果有特殊的宏定义用作全局变量(如标志变量),则应该将这些宏紧跟在变量声明之后定义。

5.      函数作为文件的最后一个模块。[ 函数间最好按照某种意义的顺序进行定义,一般来说,“自上而下”要比“自下而上”的设计方法好,“宽度优先”(被调用层次相近的函数排在一块)比“深度优先”(尽可能将被调用的函数写在前面)的方法更令人喜欢。值得考虑的是,如果要定义大量的、重要的工具函数,用字符顺序排列这些函数][2014.6.20]


2.1 文件名约定

UNIX要求文件名要有确定的后缀,这样才能被cc命令处理。需要的后缀列举如下:

  • C源文件名必须以“.c”结束。
  • 汇编源文件名必须以“.s”结束。

此外,还通常遵循以下的约定:

  • 目标文件名以“.o”结束。
  • 头文件名以“.h”或“.d”结束。
  •  Ldp 特定文件名以“.b”结束。
  •  Yacc源文件名以“.y”结束。
  • Lex源文件名以“.l”结束。

3 头文件

头文件被包含在其它文件前面,这些包含头文件的语句被C预处理编辑(处理)。有些头文件的定义处于系统级别(库头文件)。如stdio.h文件,如果程序中用到了标准的I/O库函数,那在源程序中一定要包含stdio.h。如果有的数据不止被一个文件中的程序用到,头文件也被用来定义和声明这些数据。需要合理的组织头文件,例如,针对单个的子系统的声明要分开定义在不同的头文件中。还要值得注意的是,当代码从一个平台移植到另一个平台上时,那些需要修改的声明要单独写在头文件中。


头文件不应该被嵌套包含。在同一个文件中,对编译器来说,像类型定义(typedef)和初始化只能出现一次。在除UNIX系统之上,对于那些未初始化的没有extern修饰符的声明也只能出现一次。在这种情况下,如果头文件被嵌套包含,编译就会失败。


4 外部声明

外部声明的语句需要从第一列开始。每个声明需要独占一行。除了具有能够明显表征其作用命名的常量不需要注释外,其它的声明需要一个简单的注释来说明其作用。注释应该用标签让注释彼此像队列一样有序。用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 */
};

5 注释

注释用来解释数据结构、算法等。注释内容在/*和*/间。注释从第一列以/*开始,以后的每一行的注释内容在*之后,*从第二列开始。注释以*/结束,分别被列在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]

6 函数声明

在每个函数前都应该给出一段注释来给出函数名并简要描述此函数的功能。如果函数有返回值,函数的返回类型应当从第一列开始独占一行(不要省略其返回值类型,让系统默认函数的返回值为int)。如果函数没有返回值,那么函数前就不应有返回类型。如果函数返回值为一long类型,那么在注释中应该说明这一点;或者将long作为返回类型。函数名和参数应从第一列起独占一行。每个参数需要被准确的声明(不要将其默认为int类型),并且用一行来注释它。函数体的大括号({)也需要从第一列起独占一行。函数名、参数列表和大括号之间要隔一空行[这个没怎么被采纳]。局部变量和函数内的代码至少需要一个tab缩进。

如果函数用了任何的外部变量,它们应该在函数内用extern关键字来被声明。如果外部变量是一个数组,那么数组的维数一定要包含在声明内。在函数内内调用外部函数也要用extern关键字来声明,这对调用别人写的函数非常有好处。如果一个函数的返回值非整型(int),那么编译器就会要求在调用这些函数之前对这些函数进行声明。用extern对被调用函数进行声明就可以避免这个问题。

一般说来,声明在函数内的每个变量需要独占一行并用注释来描述此变量的作用。如果这些变量是外部变量或者是要经本函数改变值的指针参数,需要在注释里面特别说明这一点。对参数和局部变量的注释需要缩进对其,让它们看起来很整齐。所有的声明和函数内的语句用一空行隔开。

局部变量不可在局部区域内嵌套声明,虽然C语言支持这个特性。潜在的困惑是当给lint-h参数时它会抱怨。

6.1 例子

/*
* 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);
}


7 复合语句

符合语句是在括号({})内的语句组成的。符合语句需要用一个或者更多的tab来进行缩进。左括号({)需要在符合语句末尾,右括号(})需要独占一行。在括号内的符合语句需要缩进。注意,函数的左括号是独占一行的。


7.1例子

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]


8 表达式

8.1 运算符

老版本的复合等于运算符=+、+-、+*等已经不能用,与之代替的是+=、-=、*=等符号。  除了‘.’和‘->’外,所有的二元运算符都需要用空格将其和操作数隔开。另外,如果关键字后面紧跟带括号的表达式,则关键字后面需要有一个空格。参数列表中的逗号后面也需要用空格以形象的区分开各参数。另一方面,带参数的宏名和函数名与括号之间不能有空格。特别的,C预处理要求左括号紧跟宏名之后,否则参数将不会被正确识别。一元运算符不能跟它们的操作数分开。由于C运算符有比较复杂的优先级,所以在混用各运算符时最好用括号来表示优先级。


例子:

a += c + d;
a = (a + b) / (c * d);
strp->field = str.fl - ((x & MASK) >> DISP);
while (*d++ = *s++)
; /* EMPTY BODY */


8.2 命名约定

毫无疑问,个人的工程有个人的命名约定。但这里还是有一些普遍的规则:

  • 以下划线开始和结束形式不宜作为用户的命名方式。UNIX用这种方式来为变量命名(如像I/O标准库,用户不需要知道)。
  • 宏名,类型定义名(typedef)及用define定义的标识符全都应该大写。
  •  变量名,结构体标识名以及函数名都应该小写。有的宏(像getchar和putchar)用小写是因为它们会作为函数的形式存在。要注意的是,函数传参方式是传值,宏传参是宏名替换。

8.3 常量

不可将数值常量直接写在程序中(起码也要注释一下此数值的来源)。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。

9 可移植性

都知道程序的可移植性的好处。这个版块就对写可执行代码给出一些指导,可移植性就是指源文件内包含可移植的源代码,它在不同的机器之上可能包含不一样的头文件但都能够编译通过并能正确执行。头文件内包含适合不同机器的宏定义及类型定义(typedef)。参考[1]包含了对风格和可移植的有用信息。本文档中的许多推荐都源自[1]。以下列举平时应避免的陷阱,并推荐一些在设计可移植代码时值得考虑的建议:

  • 首先要意识到程序有的地方是不具有可移植性的。像程序状态字及处理硬件特殊部分(如汇编器或I/O驱动)这些会涉及到特殊寄存器的程序。即使在这些情况之下,仍能够组织一些代码和数据来使不同的机器独立。建议将不可移植的代码和具移植性的代码写在不同的文件里。如果此时这些程序被移植到一个新的机子之上,就会更加容易的知道文件的哪一部分需要被修改。在不具移植性的代码文件里也有可能对本程序有用。
  • 注意字长问题。以下列举的在不同平台下的C的基本数据类型的长度(3B是贝尔实验室的机器。表中没有列举VAX,它跟3B机器相差不大。6800类似于pdpll或者3B,得看具体的编译器):
[C] Indian Hill C Style(印第安山风格指南)

一般来说,如果字长很重要,在以上列举的任何平台之上short和long应该会分别得到16和32位的长度。如果在循环中用计数器,那就用int,它能在当前的机器之上获得很好的效率。

  • 字长也会影响移位和与运算。如果x是PDP11之上的一个整型,代码x       &= 0177770将会清除x最右边的三位为0。在3B的平台之上,此代码将会清除x的最右3位及最高15位0。用x &=~07代替以上代码就能够正确的工作于所有的平台之上。
  • 在多数平台上,程序中都不宜用二进制补码表示数字。用等效的移位运算符的代码优化方式来代替算法上的改进是特别不推荐的。当移植这些代码到不同平台上时,应该权衡程序的效率与潜在的模糊性和bugs。比如将代码从3B平台移植到1A之上。
  • 注意有符号字符类型。在PDP-11平台之上,在表达式中的字符被扩展为有符号类型,但不是在每个平台上都是如此扩展。特别要注意的是,getchar这个函数(宏)的返回值是一个整型, 3B或者IBM机器之上,它不能将它的返回值给字符型的变量。
  • 对于一个字来说,PDP-11是唯一一个按照字节为单位从右往左数的处理器。其它的机器(3B,IBM,Interdata 8/32, Honeywell)字节都是从左往右数。因此,任何有关于以左或右字节起源的来形成一个字的代码都会涉及特殊的安全问题。结构体的位段成员只有在彼此既无交集又不会形成一个单元的情况下才是可移植的。
  • 不要有布尔值是否为非0的操作。if(f() !=FAIL)比if(f())要好。即使FAIL可能的拥有C用来表示假意义的值0(一个特别声名狼藉的案例是用strcmp来测试字符串的相等,它的返回值是变化的,可以定义宏STREQ来成为一个更好的方式:#define STREQ(a, b) (strcmp( (a), (b) ) == 0 ))。当有人描述一个失败操作的返回值是1而不是0时将会帮助你更快的找到错误。
  • 谨慎代码中出现的数值。即使是像0或1这样简单的数值也应该使用像FALSE和TRUE这样的宏来代替。其它出现的常量也应该表达为一个宏。这样方便程序中常量值得更改,也增进程序的可读性。
  • 熟悉库内的函数及宏定义。你不必重写一个字符串的比较函数,或者自定义库中已经存在的结构。这样不仅浪费你的时间,还会限制你用提高系统程序性能的微码或者其它手段。
  • 用lint。这是一个有价值的工具,它能够找到系统依赖的构造、程序中不一致的地方,还可以通过编译器来找到程序的bugs。

10 Lint

Lint是C程序的一个检查助手,它能够检查并报出C源文件中存在的类型不兼容、函数和调用之间的不一致性及程序中一些潜在的BUGS等。工程中的程序使用lint作为官方性的一个过程很被期待。另外,在5521部门修改lint的工作一直在进行,这样它就能够持续的为想要复合C标准的C语言程序服务。


Lint能够检查出目前给的所有标准还言之尚早。在一些情况里比如注释是否令人误解或者不正确还需要人为操作与判断。像另外的一些情况像检查函数后的大括号是否从第一列起独占一行,这项测试已经被添进去了。当遇到新的问题时还会往lint中添加新的功能并公告。


注意在程序中使用lint的最好方式不是为了让程序让官方接受而将其当成一个屏障来克服,而是当代码有较大变动时将其当成一个工具来使用。Lint能够在问题发生前找到模糊的BUGS并且能够确认程序的可行性。


11 注意事项

这个模块包含一些比较混杂的关于做和不做的事项。

  • 不要通过宏代替来改变语法。它会使程序变得不可理解。
  • 嵌入式的语句有时间和空间上的权衡问题。但对于某些程序构造来说,除了用让代码体积更大或者降低其可读性之外没有更好的办法。8.1中的while循环就是一个空间合适的例子。另外就是像这样的代码段:
while ((c = getchar()) != EOF) {
process the character
}

采用嵌入式的语句来提高运行时性能也是很有可能的。当人为的使用嵌入语句时但需要权衡程序运行速度的增加与程序可维护性的减少的结果,例如以下的代码:

a	= b + c;
d = a + r;

不能替换为d = (a = b + c)+ r;

尽管后面的代码可能能够节约一个周期的运行时间。从长远来看,与两者可维护差异相比,两者之间的时差将减少收益度。

  • 三元运算符?:和逗号运算符也有在时间和空间上的讨论。在条件运算符?:前的表达式应该被括起来:(x >= 0)?x:-x。嵌套使用条件运算符将是令人困惑的,应该尽可能的避免。逗号运算符在for语句中的多个初始化和变量增加部分显得很有用。
  • 要十分保守的使用Goto语句(其它的结构化语句如continue也要少用,break还是没那么麻烦)。它们主要的有用的使用地方是跳出某层的switch,for以及while嵌套。

for (...)
for (...) {
...
if (disaster)
goto error;
}
...
error:
clean up the mess

当goto语句必要时,与其伴随的标签要单独占一行并且需要缩进一个tab位置来搭配紧跟的代码。

  • 委员会不推荐程序员不要依赖于自动美化工具,有以下几个理由。1,从良好程序风格中受益的是程序员本身。对于前期设计手写算法程序或者伪代码来说更是如此。自动美化工具只能用于完整的、语法正确的程序,因此它不适用与需要在空格与缩进之间的问题。2,程序员只要关注一般的细节就能够排版出一个使源文件中的函数布局等更好的程序。草率的程序员应该向细心的程序员学习而不是依赖自动美化工具来提高他们的程序的可读性。3,由于自动美化程序是重要的程序必须要解析源码,维护它们的重任面临着C的可持续性,还不如亲自美化一个程序的收获大。


12 项目相关的标准

个人的工程也许希望能够成立除这里提到的额外的标准。以下提到的就是应被每个工程管理组组重视的问题。[2014.6.27]

  • 还有什么其它的命名约定可以遵循?特别的,系统用的结构体、联合体等对全局变量用前缀的约定可能也比较使用。
  • 什么样的包含文件组织适合项目的特定数据层次结构?
  • 应该成立什么样的程序来回复lint的抱怨?需要建立一个比较宽容的水平来隐藏无关紧要的或不一致投诉。
  • 如果一个工程建立了它自己的库,它应该为系统管理者为此库开发一个lint程序。这样就能够让lint检查此库函数的兼容性。

13 总结

一系列与C相关的标准已经以上文字呈现。最重要的一点事能够合理的使用空格及注释来使源文件代码具有清晰的布局。需要记住的另一点是,当写程序时一定要想到代码能方便别人的维护和修改,以及它会运行到不同的平台之上。


作为任何的标准来说,如果它有用就可以使用它。印第安山的lint将强制性的要求这些标准义务的进行自动检查。当使用这些标准遇到问题时请不要直接忽略它。在印第安山的程序员将他们的问题带给软件开发集团5522部门。在印度安山区域外的程序员可以联系处理器应用组5512部门。


14 参考

[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.


15 我的总结

翻译水平差。有的C术语还不清楚。还急躁。还只翻译一遍。[2014.6.28-14:44]


T Note Over.