使用C以外的语言
使用C以外的语言就好像使用非标准特征:它将为用户带来麻烦。即使GCC能够支持其它
语言,用户也可能因为不得不安装其它语言的编译器以创建你的程序而感到不便。所以
请使用C语言。
这条规则有三个例外:
如果有些程序包括了特殊语言的解释器,那么就可以使用这种语言。
因此,GNU Emacs包含用Emacs Lisp写的代码就没有问题,因为GNU Emacs包含了Lisp解
释器。
如果一个工具就是为了某种语言而编写的,那么就可以使用那种语言。
这是因为那些需要创建这个工具的人必然是那些已经安装了其它语言的人。
如果一个应用程序没有被极端广泛地关注,那么应用程序的安装不太方面就不是特别重
要。
格式化你的源代码
把作为C函数的开头的左花括号放到第零列是十分重要的,并且避免把任何其它的左花括
号、左括号或者左方括号放到第零列。有些工具通过寻找在第零列的左花括号来寻找C函
数的起点。这些工具将不能处理那些不按照这种方式排版的代码。
对于函数定义来说,把函数名的起始字符放到第零列也同样重要。这帮助任何寻找函数
定义,并且可能有助于帮助某些工具识别它们。因此,正确的格式应该是:
static char *
concat (s1, s2) /* Name starts in column zero here */
char *s1, *s2;
{ /* Open brace in column zero here */
...
}
或者,如果你希望使用标准C,定义的格式是:
static char *
concat (char *s1, char *s2)
{
...
}
在标准C中,如果参数不能够被美观地放在一行中,按照下面的方式把它们分开:
int
lots_of_args (int an_integer, long a_long, short a_short,
double a_double, float a_float)
...
对于函数体,我们希望它按照如下方式排版:
if (x < foo (y, z))
haha = bar[4] + 5;
else
{
while (z)
{
haha += foo (z, z);
z--;
}
return ++x + bar ();
}
我们发现如果在左括号之前以及逗号之后添加空格将使程序更加容易阅读。尤其是在逗
号之后添加空格。
当我们把一个表达式分成多行的时候,在操作符之前而不是之后分割。下面是正确的方
式:
if (foo_this_is_long && bar > win (x, y, z)
&& remaining_condition)
尽力避免让两个不同优先级的操作符出现在相同的对齐方式中。例如,不要象下面那样
写:
mode = (inmode[j] == VOIDmode
|| GET_MODE_SIZE (outmode[j]) > GET_MODE_SIZE (inmode[j])
? outmode[j] : inmode[j]);
应该附加额外的括号以使得文本缩进可以表示出这种嵌套:
mode = ((inmode[j] == VOIDmode
|| (GET_MODE_SIZE (outmode[j]) > GET_MODE_SIZE (inmode[j])))
? outmode[j] : inmode[j]);
插入额外的括号以使得Emacs可以正确地对齐它们。例如,如果你手工完成缩进工作,那
么它们看起来不错,但Emacs将把它们混在一起:
v = rup->ru_utime.tv_sec*1000 + rup->ru_utime.tv_usec/1000
+ rup->ru_stime.tv_sec*1000 + rup->ru_stime.tv_usec/1000;
但添加一组括号解决了这个问题:
v = (rup->ru_utime.tv_sec*1000 + rup->ru_utime.tv_usec/1000
+ rup->ru_stime.tv_sec*1000 + rup->ru_stime.tv_usec/1000);
按照如下方式排版do-while语句:
do
{
a = foo (a);
}
while (a > 0);
请按照逻辑关系(而不是在函数中)使用走纸字符(control-L)以把程序划分成页。页
有多长并不重要,因为它们不必被放在一个打印的页中。走纸字符应该单独地出现在一
行中。
为你的工作写注释
每个程序都应该以一段简短地、说明其功能的注释开头。例如:``fmt - filter for si
mple filling of text''.
请为每个函数书写注释以说明函数做了些什么,需要哪些种类的参数,参数可能值的含
义以及用途。如果按照常见的方式使用C语言类型,就没有必要逐字重写C参数声明的含
义。如果它使用了任何非标准的东西(例如,一个类型为char *的参数实际上给出了一
个字符串的第二个字符,而不是第一个字符,的地址),或者是可能导致函数不能工作
的任何可能的值(例如,不能保证正确处理一个包含了新行的字符串),请确认对它们
进行了说明。
如果存在重要的返回值,也需要对其进行解释。
请在你的注释之后添加两个空格,以便Emacs句子命名进行处理。还有,请书写完整的句
子并且使头一个单词以大写字母开头。如果小写字母组成的标识符出现在句子的开头,
不要把它变成大写的!修改拼写就构成了不同的标识符。如果你不希望句子以小写字母
开头,可以写下不同的句子(例如,“The identifier lower-case is ...”)。
如果你使用参数名来说明参数值,关于函数的注释就会更清晰。变量名本身应该是小写
的,但在你说到它的值而不是变量本身的时候就使用大写字母。因此,“the inode nu
mber node_num” 比“an inode”要好。
通常在函数之前的注释中没有必要重新提到函数的名字,因为读者可以自己看到它。一
种可能的例外是:注释太长了,以至于函数本身被挤出了屏幕底端之外。
对于每个静态变量,也象下面那样应该提供注释:
/* Nonzero means truncate lines in the display;
zero means continue them. */
int truncate_lines;
除非``#endif''是一个没有嵌套而且很短(只有几行)的条件,每个 ``#endif''都应该含有
一个注释。注释应该说明它所结束的条件,包括它的含义。 ``#else''应该含有一个说明
条件与随后代码的含义的注释。例如:
#ifdef foo
...
#else /* not foo */
...
#endif /* not foo */
但相反,按照如下方式为``#ifndef''写注释:
#ifndef foo
...
#else /* foo */
...
#endif /* foo */
清晰地使用C语言成分
请显式地声明函数的所有参数。不要因为它们是整数就忽略它们。
对外部函数以即将随后出现在源文件中的函数的声明应该出现在靠近文件开头(在第一
个函数定义之前的某个地方)的同一个地方。或者其它的声明应该出现在头文件中。不
要在函数中放置外部声明。
在过去一种常见的做法是在同一个函数中把同一个局部变量(比如说名为tem的变量反复
地用于不同的值。但现在,更好的方式是为每个不同的目的分别定义局部变量,并且给
它们以更有意义的名字。这不仅仅是程序更容易理解,它还会被好的编译程序所优化。
你还可以把对局部变量的声明放到包含对它的使用的最小范围中。这可以把程序变得更
清晰。
不要使用可以遮蔽全局标识符的局部变量和参数。
不要在跨越了行的声明中声明多个变量。在每一行中都以一个新的声明开头。例如,不
应该:
int foo,
bar;
而应该:
int foo, bar;
或者:
int foo;
int bar;
(如果它们是全局变量,在它们之中的每一个之前都应该添加一条注释。)
当你在一个if语句中嵌套了另一个if-else语句,总是用花括号把if-else括起来。因此
,不要写:
if (foo)
if (bar)
win ();
else
lose ();
而总是要写:
if (foo)
{
if (bar)
win ();
else
lose ();
}
如果你在else语句中嵌套了一个if语句,即可以像下面那样写else if:
if (foo)
...
else if (bar)
...
按照与then那部分代码相同的缩进方式缩进else if的then部分代码,也可以在花括号中
像下面那样把if嵌套起来:
if (foo)
...
else
{
if (bar)
...
}
不要在同一个声明中同时说明结构标识和变量或者结构标试和类型定义(typedef)。单
独地说明结构标试,而后用它定义变量或者定义类型。
尽力避免在if的条件中进行赋值。例如,不要写:
if ((foo = (char *) malloc (sizeof *foo)) == 0)
fatal (""virtual memory exhausted"");
而要写:
foo = (char *) malloc (sizeof *foo);
if (foo == 0)
fatal (""virtual memory exhausted"");
不要为了通过lint的检查而把程序修改得难看。请不要加入任何关于void的强制类型转
换。没有进行类型转换的零作为空指针常量是很好的。
命名变量和函数
请在名字中使用下划线以分隔单词,以便Emacs单词命令对它们来说有用。坚持使用小写
;把大写字母留给宏和枚举常量,以及根据统一的惯例使用的前缀。
例如,你应该使用类似ignore_space_change_flag的名字;不要使用类似 iCantReadTh
is的名字。
用于标明一个命令行选项是否被给出的变量应该在选项含义的说明之后,而不是选项字
符之后,被命名。一条注释即应该说明选项的精确含义,还应该说明选项的字母。例如
,
/* Ignore changes in horizontal whitespace (-b). */
int ignore_space_change_flag;
当你需要为常量整数值定义名字的时候,使用enum而不是``#define''。 GDB知道枚举常量
。
使用14个字符或者少于14个字符的文件名,以避免无缘无故地在System V上导致问题。
使用非标准的特征
许多现有的GNU工具在兼容Unix工具的基础上提供了许多方便的扩展。在实现你的程序时
是否使用这些扩展是一个难以回答的问题。
一方面,使用扩展可以使程序变得清晰。但另一方面,除非人们可以得到其它的GNU工具
,人们就不能创建程序。这可能使得程序只能在较少类型的机器上工作。
对于某些扩展,可能可以很容易地应付上述两种选择。例如,你可以用“关键字” INL
INE定义函数并且把INLINE定义成一个宏,在根据编译器确定它是被扩展成inline或者扩
展成空。
一般地,如果你能够在没有它们的情况下直截了当地完成任务,可能最好的办法是不使
用扩展,但如果扩展可以大大地改进你的工作,就使用扩展。
对这一规则的一个例外是那些运行在大量不同系统上的大规模、已经创建的程序(例如
Emacs)。使用GNU扩展将破坏这些程序。
另一个例外是那些作为编译本身的一部分的程序:这包括必须用其它编译器进行编译以
构造GNU 编译工具的任何东西。如果它们需要GNU编译器,那么没有人可以在没有安装它
们的情况下编译它们。这将是不好的。
由于大部分计算机系统还没有实现标准C,使用标准C的特征,所以使用标准C就相当于使
用 GNU扩展,所以前面的考虑也适用于它。(除了那些令我们失望的标准特征,例如三
元组序列(trigraphs)--永远不要使用它们。)
三元组序列是标准C中为了弥补某些终端上可用字符的不足而提供的、用三个字符组合代
替一个特殊字符的方法。所有可用的三元组为:“??=”转换成“#”、“??/”转换成“
”、 “??''”转换成“^”、“??(”转换成“[”、“??)”转换成“]”、“??!”转换
成“|”、“??<”转换成“{”、 “??>”转换成“}”、“??-”转换成“~”。
---- 译者注
适用于所有程序的程序行为
通过动态地分配所有的数据结构来避免对任何数据结构,包括变量名、行、文件和符号
,的长度和数量施加任何限制。在大多数Unix工具中,“长行被没有提示地截断”了。
对于GNU工具来说,这是不可接受的。
读入文件的工具不应该放弃NUL字符、或者任何不可打印的字符,包括那些大于0177的字
符。唯一明智的例外是那些为访问与不能处理这些字符的特定类型的打印机的界面而设
计的工具。
为每个系统调用的返回值进行错误检查,除非你知道你希望忽略错误。把那些系统错误
文字(来自于perror或者它的等价物)包括在每个有失败的系统调用导致的错误消息中
,如果有的话还要包括文件名和工具名。仅仅给出“cannot open foo.c”或者“stat
failed” 是不够的。
检查每个对malloc或者realloc的调用以察看它是否返回0。即使在 realloc使块变小的
时候,也要检查它的返回值;在有些系统中总是把块的大小扩大到2的幂次。如果你申请
更少的空间,realloc可能得到一个不同的块。
在Unix中,如果realloc返回0,那么它就可以破坏存储块。GNU realloc 没有这个错误
:如果它失败了,原来的块不会被改变。放心地假定这个错误已经被修正了。如果你希
望在 Unix上运行你的程序,并且在这种情况下不希望失去内存块,你可以使用GNU mal
loc。
你必须假定free将改变被释放的块的内容。任何你希望从块中获得的东西,你必须在调
用free之前拿到它。
使用getopt_long对参数进行解码,除非参数的语法使得这样做变得不合情理。
当静态内存在程序执行的时候被写入的情况下,显式地使用C代码来初始化它。保留对那
些不会被改变的数据的C初始化声明。
尽力避免访问晦涩的Unix数据结构的低级界面(例如文件目录、utmp或者内核内存的分
布),因为它们通常会降低兼容性。如果你希望找到目录中的所有文件,使用readdir或
者其它高级的界面。GNU兼容将地支持它们。
在缺省状态下,GNU系统将提供BSD的信号处理函数和POSIX的信号处理函数。因此GNU软
件应该使用它们。
在错误中检测到“不可能”的条件是,只要退出就行。没有理由打印任何消息。这些检
查表明有bug存在。任何希望修正错误的人都必须阅读源代码并且运行调试器。所以在源
代码中通过注释给出问题的解释。相关的数据将储存在变量中,这些变量很容易被调试
器检测到,所以没有理由把它们转移的其它任何地方。
格式化错误信息
来自于编译器的错误信息应该使用格式:
source-file-name:lineno: message
如果有适当的源文件存在,则来自于非交互式程序的错误信息应该使用格式:
program:source-file-name:lineno: message
或者,如果没有相关的源文件,则应该使用格式:
program: message
在一个交互式程序(从终端读入命令的程序)中,不把程序名包括在错误信息中更好一
些。指明程序正在运行的地方应该是提示符或者屏幕的布局。(当相同的程序在运行时
从源文件中,而不是从终端中读取输入,它就不是交互式的了,并且最好按照非交互方
式风格打印错误信息。)
在字符串message被放置在程序名和/或文件名之后时,它不应该以大写字母开头。此外
,它也不应该以句点结尾。
来自于交互式程序的错误信息,以及其它的诸如使用信息的信息,应该以大写字母开头
。但它们不应该以句点结尾。
库的行为
试着使库函数成为可再入的。如果它们需要进行动态内存分配,至少要试图避免任何来
自 malloc本身的非可再入的方面。
这里给出了一些库的命名惯例,以避免名字的冲突。
为库选择一个多于两个字符的命名前缀。所有的外部函数和变量名都应该以这个前缀开
头。还有,在任何给定的库成员中,仅仅应该含有一个函数或者变量。这通常意味着要
把每个函数和变量都放在单独的源文件中。
一个例外是如果两个符号总是在一起使用,而使得没有任何合理的程序只需要使用其中
的一个而不使用另外一个;那么它们可以被放在同一个文件中。
标示没有在文档中给予说明的调用点的外部符号的名字应该以``_''开头。它们还要包括为
库选择的名字前缀,以防止与其它库可能产生的冲突。如果你愿意,它们可以和用户调
用点放在同一个文件中。
你可以按照你的意愿使用静态函数和静态变量并且它们不需要服从任何命名惯例。
适用于GNU的移植性
在Unix世界中,“移植性”往往指的是移植到不同的Unix版本中。对于GNU软件来说是次
要的,这是因为它们的主要目的是运行在GNU内核上而且仅仅运行与其上,使用GNU C编
译器编译并且仅仅由它进行编译。不同cpu上的各种GNU系统的变种数量将象不同cpu上的
Berkeley 4.3系统的变种一样多。
今天所有的用户都在非GNU系统上运行GNU软件。所以有必要支持各种非GNU系统;但不是
特别重要。在合理范围的系统上获得可移植性的最简单方式是使用Autoconf。由于大部
分程序需要知道的关于主机的知识已经由Autoconf写下来了,所以你的程序不太可能需
要知道比Autoconf所能够提供的知识的更多知识。
因为目前GNU内核还没有被完成,所以还难以确定GNU内核将提供那些工具。因此,就假
定你可以使用你在4.3中可以使用的任何东西;只要避免使用有更高级的替代结构(rea
ddir)存在的半内部的数据结构(例如,directories)就可以了。
你可以自由地假设任何合理C语言标准工具、库或者内核,因为我们将发现有必要在完整
的GNU系统中支持它们,而不论我们是否已经这样做了。一些现有的内核或者C编译器所
缺少的功能与GNU内核和 C编译器对它们的支持没有关系。
另外一个需要担心的是cpy类型间的不同,例如字节序(byte order)的不同和对齐限制
的不同。 16位的机器恐怕不会被GNU所支持,所以没有必要花费任何时间去考虑整数少
于32位的可能性。
你可以假定所有的指针都具有相同的格式,而不论它们指向什么,并且它们实际上都是
一个整数。在一些怪异的机器上不是这样,但它们并不重要;不要为迎合它们而浪费时
间。此外,我们最终将把函数原型放到所有的GNU程序中,而那将可能使你的程序即使在
怪异的机器上也能够工作。
因为一些重要的机器(包括68000)是高位开头(big-endian),不能假定整数对象的地
址就是最低位字节(least-significant)的地址是十分重要的。因此,不要犯如下的错
误:
int c;
...
while ((c = getchar()) != EOF)
write(file_descriptor, &c, 1);
你可以假定使用一兆内存是合理的。除非可以在那个层次可以做到,不要为减少内存的
使用而费力。如果你的程序创建了复杂的数据结构,就把它们存放在内核中,并且在ma
lloc返回0的时候给出一个致命错误就行了。
如果一个程序按照行工作并且可以被用于任何用户提供的输入文件,它就应该在内存中
仅仅保存一行,因为这并不十分困难并且用户将需要能够操作比内核一次能够处理的文
件更大的输入文件。
命令行界面标准
请不要让工具的行为依赖于调用它的名字。有时需要把一个工具连接到不同的名字,并
且这将不会改变它的功能。
最为替代,可以在运行时使用选项或者编译器选项,或者同时使用两者来选择不同的程
序行为。
服从POSIX关于程序的命令行选项的指导是一个好主意。这样做的最简单方式是使用get
opt 来分析选项。需要指出的是,除非使用了特殊参数``--'',GNU版本的getopt 通常允
许选项出现在参数中的任何位置。这不是POSIX的规定;它是GNU的扩展。
请为Unix风格的单字符选项定义等价的长名字选项。我们希望以这种方式使GNU对用户更
加友好。通过使用GNU函数getopt_long很容易做到这一点。
通常仅仅把最为普通参数给出的文件名当作输入文件是一个好主意;所有输出文件都应
该通过选项给出(使用``-o''更好)。即使你为了保持兼容性而允许把普通参数当作输出
文件名,你仍然可以试图为输出文件名提供一个合适的选项。这将为GNU工具带来更多的
一致性,以减少用户需要记忆的特征。
程序还应该支持一个用于输出程序的版本号的选项``--version'',以及支持一个用于输出
选项用法信息的选项``--help''。
为程序制作文档
为GNU程序制作文档请使用Texinfo。参见Texinfo手册,硬拷贝或者GNU Emacs Info子系
统中的版本都行。(C-h i)。作为例子,可以看看现有的GNU Texinfo文件(例如,在
GNU Emacs发布版本中``man/''目录下的Texinfo文件)。
手册的标题页应该说明本手册适用于程序的那个版本。手册的顶节点也应该包含这个信
息。如果手册比程序改变得还要快,后者与程序是无关的,请在上述两个地方说明手册
的版本号。
手册应该说明所有的命令行参数和所有的命令。手册应该给出使用它们的例子。但不要
把手册组织成一个特征的列表。相反,在手册中按照把用户需要理解的概念放在特征之
前的方式组织文档。说明用户可能需要达到的目的,并且解释如何完成它们。不要把Un
ix man手册作为书写GNU文档的模式;它们不是值得模仿的好例子。
手册应该有一个名为``program Invocation''或者 ``program Invoke''或者``Invoking pro
gram'' 的节点,其中program是该程序的程序名,也就是在shell中输入以运行程序的那
个名字。该节点(如果它有子节点,也包括子节点)应该说明程序的命令行参数以及如
何运行它(也就是人们将在man手册中看到的那一类信息)。以``@example''开头包含一个
程序可以使用的所有选项和参数的模板。
另一种方式是,在某些菜单中添加一个名字符合上述模式之一的菜单项。它表明由菜单
项指出的节点就是为此而创建的,而不管这个节点的实际名字是什么。
将会有一个自动的功能使得用户可以给出程序名并且只需要快速地阅读手册的这个部分
。
如果一个手册说明了多个程序,那么手册应该为每个它所说明的程序定义一个这样的节
点。
除了程序包的手册之外,包还应该包含一个名为``NEWS''的文件,它包含了一个对用户来
说是可见的、并且值得一提的修改。在每个新的发行版本中,在文件的前面添加新的条
目并且指出适用于它们的版本。不要删除原来的条目;把它们保留在新条目的后面。按
照这种方式,从以前的版本升级的用户就可以看到有那些新的功能。
如果``NEWS''文件变得太长了,可以把一些陈旧的条目放到一个名为``ONEWS'' 的文件中,
并且在``NEWS''文件的结尾加一个说明以告知用户参考``ONEWS''。
如果你愿意,你可以在提供Texinfo手册的同时提供man手册。但请记住维护man手册需要
在每次程序改变的时候都付出努力。你花费在man手册上的任何时间都消耗了你本来能够
用在贡献更有价值的东西上的时间。
因此,即使用户自愿提供man手册,你可能会发现这个礼物太麻烦而不值得接受。除非你
手头有时间,并且除非有志愿者愿意承担维护它的全部责任--以至于你可以把它完整地
交给他,拒绝提供man手册可能会更好一些。如果志愿者停止维护man手册,那么也不必
感到有责任让你自己承担它;在其它的出现志愿者维护它之前撤销man手册也许更好一些
。
另一种方式是,如果你希望man手册的内容与实际情况区别很小而使得man手册仍然有用
,可以在man 手册的开头给出显著的声明以说明你没有维护它并且Texinfo手册是更加权
威的,同时指出如何访问 Texinfo文档。
制作发行包
发行Foo的版本69.96的tar文件包的名字是``foo-69.96.tar''。它应该被解包到名为 ``fo
o-69.96''的子目录中。
对程序的创建和安装不应该修改发布版本中的任何文件。这意味着以任何方式作为程序
的正式部分的所有文件都必须被分成源文件和非源文件两类。源文件由人手工编写并且
不会被自动改变;非源文件则在Makefile的控制下由程序从源文件生成。
自然地,所有源文件必须出现在发布版本中。只有在非源文件不是过时的并且与机器是
无关的情况下,从而在创建发布版本时将不会需要修改它们,才能把非源文件包含在发
布版本中。我们一般把由Bison、 Lex、TeX和Makeinfo生成的非源文件包括进去;这有
助于避免在我们的发布版本中引入不必要的依赖性,以使得用户可以安装他们需要安装
的包。
永远不要把实际上可能在程序的创建和安装中被修改的非源文件包含在发布版本中。所
以如果你发布非源文件,在你作新的发布时,总是要确认它们没有过时。
确保从发布包中解开的目录(以及所有的子目录)对于所有人来说都是可写的(八进制
模式777)。这样做是为了使那些保留从tar包中取出的文件的所有权(ownership)和许
可权(permissions)的老版本的 tar,即使在用户没有授权的情况下也能够提取出所有
的文件。
确保在发布版本中的没有多于14个字符的文件名。同样地,由程序创建的文件都不含有
长于14个字符的文件名。这样做的原因是有些系统坚持POSIX标准的愚蠢解释,并且拒绝
打开长文件名,而不是象在过去那样把文件名截短。
不要在发布版本本身包含任何符号连接。如果tar文件包括符号连接,那么人们甚至不能
在那些不能支持符号连接的系统上打开包。还有,不要在不同的目录中为一个文件使用
多个名字,因为某些文件系统不能处理它并且这使得包不能在这类文件系统上被打开。
试着确保所有的文件名在MS-DOG下都是唯一的。在MS-DOG下文件名由8个字符组成,后面
还可以附加一个点和至多三个字符。MS-DOG将截断点之前和之后的多余字符。因此,``f
oobarhacker.c''和 ``foobarhacker.o''不会被混淆;它们被截断成``foobarha.c''和``foob
arha.o'' ,它们是截然不同的。
在你的发布版本中包含一个你用来测试打印所有``*.texinfo''文件的``texinfo.tex''的副
本。
同样地,如果你的程序使用了诸如regex、getopt、obstack或者termcap之类的小GNU软
件包,把它们包括在发布文件中。把它们排除在外将使发布文件小一些,其代价是给那
些不知道需要哪些额外文件的用户带来不便。