http://antkillerfarm.blog.sohu.com/102734995.html
Notes On Writing Portable Programs In C
用C语言编写可移植程序的注意事项
1990年6月,第五版
A. Dolenc、A. Lemmke、D. Keppel著
antkillerfarm译
内容提要
* 前言
* 介绍
* 标准化的影响
o ANSI C
+ 移植限制
+ 未确定和未定义的行为
o POSIX
* 预处理器
* 语言
o 语法
o 语义
* Unix系列: System V 和 BSD
* 头文件
o ctype.h
o fcntl.h and sys/file.h
o errno.h
o math.h
o strings.h vs. string.h
o time.h and types.h
o varargs.h vs. stdarg.h
* 运行时库
* 编译器限制
* 使用浮点数
o 机器常量
o 浮点参数
o 浮点计算
o 异常
* VMS
o 文件描述符
o 杂谈
* 一般方针
o 机器体系结构, 类型兼容, 指针等
o 编译器的差异
o 文件
o 杂谈
* 致谢
* 版权声明
* 参考文献
* 关于本文
* 译者后记
前言
在正文开始之前,先说一下本文针对的读者人群。本文主要针对那些从未把程序移植到其他平台(包括特定的硬件和软件环境)的程序员,特别是那些打算编写跨主机的大系统的程序员。
如果你已经做过一些移植工作的话,你或许会觉得这些内容没什么用。
我们建议结合着文献[Can89]阅读本文。强烈推荐订阅comp.lang.c新闻组。[Hor90, Koe89]
免责声明:这里所示的代码片段只是为了使程序移植性“更”好,也就是说这些代码可能会在某些编译器或环境下失败。
本文件可以通过匿名的FTP在sauna.hut.fi [130.233.251.253]的~ftp/pub/CompSciLab/doc文件夹下获得。文件portableC.tex、portableC.bib 和 portableC.ps.Z分别是本文的LaTeX、BibTeX 和 compressed PostScript版本。
介绍
本文的目的是总结几位资深程序员在各种平台移植C语言程序的经验。
为了保证本文内容的合理性,我们限定这些程序必须运行在类UNIX操作系统上,且有基本的类UNIX环境。唯一的例外是,我们还会讨论VMS操作系统。
我们可以从那些已有的跨平台程序中获得一些有用的信息。例如Free Software Foundation 和 MIT X Consortium开发的那些公用软件。
我们讨论可移植性主要关注以下两点:
语言。包括预处理器、语法和语义。
环境。包括头文件和运行时库的位置和内容。
我们还将讨论语言和环境的标准化,并特别关注浮点数的表示与计算,特定编译器的限制,以及VMS。
我们主要关注于boiler-plate问题。即将成为标准的文献[X3J88]所提到的twisted code问题以及系统编程问题在本文中不会过多涉及。
标准化的影响
所有的标准都有好的一面和坏的一面,这是由它的本质决定的。我们将在下文着重探讨这一点。
美国国家标准委员会(ANSI) 即将完成C语言标准的制定。标准主要规定了语言的语法和语义,并定义了一个最小环境(包括一些头文件的名字和内容和一些运行时库函数的定义)。
可以从以下地址获得ANSI C标准的副本:
American National Standards Institute
Sales Department
1430 Broadway
New York, NY 10018
电话 (212) 642-4900
传真 (212) 302-1286
ANSI C
移植限制
我们首先关注一下标准状态的环境限制。这些限制是移植的下限,它意味着超过这些限制的在某个编译器上正确的程序,可能不能被另一个正确(符合标准)的编译器编译。
以下是我们认为最重要的限制。我们首先列出与预处理器相关的。
* 最多8层嵌套的条件包含。(译注:例如#ifdef...#endif等。)
* 最多8层嵌套的#included语句。
* 在一个完整的表达式中最多32层括号表达式。这通常发生在使用宏的时候。
* 同时最多1024个宏。这在某个文件包含了太多的头文件时可能发生。
* 一个源代码行最多509个字符。如果对预处理之后的行做这样的严格限制的话,又会因为一般的宏通常是展开在一行,而影响到宏的最大尺寸的定义。标准在这一点上并不明确,因此在绝大多数的实现中,这个限制是针对于宏展开之前的行。
* 一个外部标识符最多6个字符。通常这个规定是由链接器,而不是编译器限制的。
译注:C89 规定,编译器至少应该能够处理 31 个字符(包括 31)以内的内部标识符(internal identifier);而对于外部标识符(external identifier),编译器至少应该能够处理 6 个字符(包括 6)以内的外部标识符。所谓标识符,是指我们为变量(variable)、宏(macro),或者函数(function)等等取的名字。例如 int num; 这个语句中的 num 就是一个标识符。
最新的 C99 标准规定,编译器至少应该能够处理 63 个字符(包括 63)以内的内部标识符;编译器至少应该能够处理 31 个字符(包括 31)以内的外部标识符。
事实上,我们可以使用超出最大数目限制的字符来命名标识符,不过编译器会忽略超出的那部分字符。也就是说,如果我们用 35 个字符来命名变量,而那个编译器最多只能处理 31 个字符的变量名的话,那么多出的那 4 个字符就会被编译器忽略,只有前面的 31 个字符有效。(摘录自http://www.jb51.net/article/7209.htm)
* 在一个结构体或共用体内最多127个成员。
* 一个函数调用最多拥有31个参数。这在使用参数个数不定的函数的时候会带来麻烦。因此,最好在设计函数时将参数的个数限制在合理的范围之内,或者使用诸如数组之类的可变接口。
不幸的是这些限制中的其中一些可能会迫使程序员以一种不优雅的方式书写代码。如果标准中的这些限制符合“好”的编程实践的话,我们当然是遵守为好。
然而在某些情况下,我们只能以不优雅的方式书写代码,因为这些限制可能会中断那些生成C代码的程序,例如编译器的编译器和许多C++编译器。
未确定和未定义的行为
以下是几个未确定和未定义的行为的例子。
1. 函数调用中,函数指针和自变量的执行次序。
译注:例如在以下的函数调用中
(*pf[f1()]) (f2(), f3() + f4())
f1(), f2(), f3()和f4()可能以任意的次序被执行。
(摘录自https://www.securecoding.cert.org/confluence/display/seccode/EXP10-C.+Do+not+depend+on+the+order+of+evaluation+of+subexpressions+or+the+order+in+which+side+effects+take+place)
2. 当操作符#和##连在一起时,预处理器对它们宏替换的执行次序。
3. 浮点类型的表示。
4. 使用在当前范围不可见的标识符。(译注:原文为An identifier is used that is not visible in the current scope,我也不清楚其具体所指为何。)
5. 一个指针被转换为除了整数和指针类型之外的类型。
这些例子还有很多。标准没有明确定义这些行为的主要原因之一是为了允许C环境的实现者更有效率的使用它。
POSIX
POSIX工作组P1003.1的目标是为UNIX定义一个通用接口。尽管ANSI C标准的确定义了一些头文件的内容和一些库函数的行为,但这并不足以定义一个有实用价值的环境。而这就是P1003.1的任务。
我们不知道P1003.1是如何处理本文所列出的这些问题,因为目前我们还缺乏这方面的文档。希望这些在将来的文档中能得到解决。
预处理器
预处理器在以下的情况下可能会有不同的行为。
1. -I命令项的解释在不同系统中是不一样,而且标准也没有涉及这一点。例如,在使用-I..的情况下,#include "dir/file.h"语句,在绝大多数的类Unix环境中,预处理器会在../dir下寻找file.h,但在VMS下,却只在./dir下寻找file.h。
2. 我们不认为下面的语句能在所有的预处理器上执行。
#define D define
#D this that
标准不允许这样的语法。(参见[X3J88]3.8.3 §20)
3. 指示符在所有的预处理器中都是差不多的,只是有些预处理器可能不支持在#if指示符中的#pragma指示符。
可以缩进#pragma指示符,使之在老的预处理器上也没有问题。进一步的说,你最好像下面这样用#ifdef将#pragma括起来以区分特定的平台。
#ifdef <platform-specific-symbol>
#pragma ...
#endif
4. 连接符号有两种方式。一种是老式的K&R方式,它的实现原理是预处理器会去掉/**/这样的注释。显然,如果输出中有空格的话,这样就不管用了。ANSI C标准定义了操作符##来表示相邻字符串的连接。因此像下面这样将两种形式都包含在一个头文件中是很有用的。
#ifdef __STDC__
# define GLUE(a,b) a##b
#else
# define GLUE(a,b) a/**/b
#endif
如果有需要的话,我们也可以仿照GLUE宏,定义多个参数的宏。
5. 一些预处理器对引号中的符号做替换,但另一些却不是这样。因此这是一种内在的不可移植。标准不允许这样做,并提供了相应的机制来达到相同的效果。以下的代码既可以在遵循ANSI标准的预处理器上工作,又可以在那些对引号中的符号做替换预处理器上工作。
#ifdef __STDC__
# define MAKESTRING(s) # s
#else
# define MAKESTRING(s) "s"
#endif
有许多优秀公开有用的预处理器遵循ANSI C标准。例如MIT X联盟随着X Window系统发布的预处理器。
注意一下#pragma指示符,它能够改变程序的语义,但有些特殊的编译器可能不认识这些语义。很显然,如果程序的行为依赖于这些语义的正确解释的话,为了使程序可移植,所有的目标平台都必须认识它。
最后,我们还要知道标准中已经包含了#error指示符的确切语义。缩进#error,因为有些老的预处理器不认识它。
语言
语法
标准定义的语法是K&R定义的一个超集。它意味着严格按照之前的标准的程序在ANSI C兼容的编译器下是没有问题的。标准主要扩展了以下语法:
1. 包含了关键字const和volatile。
2. 用省略号...表示可变数量的参数。
3. 函数原型。
4. 用Trigraph表示特定的宽字符串。(译注:Trigraph的含义参见http://en.wikipedia.org/wiki/C_trigraph)
我们鼓励使用保留字const和volatile,因为它们有助于代码的书写。如果代码要在那些不兼容的编译器上编译的话,将下面的片段添加到头文件中,将是很有用的。
#ifndef __STDC__
# define const
# define volatile
#endif
然而,我们还必须保证程序的行为并不依赖于这些关键字。
语义
语法没有解释的问题,因为它可以被准确定义。然而,编程语言通常是用自然语言描述的,例如英语,而这会导致同样的文字会有不同的解释。
很明显的C语言的定义不该有歧义,否则的话,标准也就没有存在的必要了。[KR78]尽管标准已经很精确了,但仍然存在解释不相同的地方。例如f(p=&a, p=&b, p=&c)。到底它的意思是f(&a,&b,&c)还是f(&c,&c,&c)?即使是简单的例子,例如a[i] = b[i++],其行为也是依赖于编译器的。[Can89]
在这篇介绍中我们不再涉及这个主题。读者可以直接到USENET的新闻组comp.std.c或comp.lang.c查找相关内容,上面的例子也是从那里找来的。《The Journal of C Language Translation》是一篇很好的参考文献。另外一个方法是从标准委员会获得这方面的内容精华,它的地址是X3 Secretariat, CBEMA
311 1st St NW Ste 500
Washington DC, USA
Unix系列: System V 和 BSD
Unix最初于1969年诞生于AT&T(这里后来叫做贝尔实验室,或者昵称为Ma Bell)的PDP-11主机上。我们现在看到的很多系统都和它很相似,它的应用也很广泛,这得益于它设计和实现的质朴。(当然它的绝大部分是C写的。)
然而,这也导致它们中的每一个都发展出了各自的方言。最主要的有加州大学伯克利分校发布的BSD Unix和AT&T发布的System V Unix。其他的版本都是这两个主要方言的变种。
这两个主要系列之间的差异本不应该影响大多数程序的运行。但事实上我们甚至可以说大多数的差异都是令人讨厌的。
BSD Unix加强了信号处理的能力并实现了套接字。然而值得注意的是所有的Unix系统的原始i/o接口(例如ioctl系统调用)都是不同的,应该尽可能的避免使用它。
想对Unix的过去和未来有更多了解的读者,可以参考[Man89, Int90]。
头文件
许多有用的系统头文件在不同的系统中所在的位置不同,定义的符号也有不同。下文中,我们将假设程序是在类BSD Unix系统上开发的,并要移植到类System V Unix系统、VMS系统或者其他头文件遵守标准的类Unix系统上。
在以下的章节中,我们将演示一下怎样处理实践中出现的一些最简单的情况。在下面所示的这些代码中,有一部分来源于MIT开发的X Window系统的Xos.h文件。我们也会修改一些代码以支持VMS。
很多头文件在很多系统中都是无保护的,特别是那些BSD4.2版之前的头文件。无保护的含义是如果试图包含某个头文件多于一次的话,会导致编译错误(例如,可能归结为递归包含)或者在某些实现中,被预处理器认定为符号重定义。保护头文件是一个好习惯。
ctype.h
它在所有的系统中提供的功能都是一样的,除了某些符号必须被重命名。
#ifdef SYSV
# define _ctype_ _ctype
# define toupper _toupper
# define tolower _tolower
#endif
不过,我们还应该知道<ctype.h>中的定义,并不是跨字符集可移植的。
fcntl.h and sys/file.h
BSD系统中很多sys文件夹下的文件,在System V中是放在/usr/include中的,其他像VMS的系统甚至没有sys文件夹。
这两类系统中定义的open函数调用所在的头文件也不相同,
#ifdef SYSV
# include <fcntl.h>
#else
# include <sys/file.h>
#endif
errno.h
错误号的语义在不同的系统中是不同的,它的数量也是不同的。(例如,BSD系统的错误号就比System V的多)某些系统,例如SunOS,使用全局符号errno来保存运行时库检测到的最近的错误。这个符号在大多数的系统上是不可用的,尽管标准要求定义这个符号。(参见[X3J88]的4.1.3节)
最通用的打印错误信息的方法是使用perror。
math.h
在这个头文件中,System V比类BSD系统有更多的定义。相应的库也有更多的函数。这个头文件在 VMS和Cray系统中是无保护的,所以我们必须自己来添加保护。
#if defined(CRAY) || defined(VMS)
# ifndef __MATH__
# define __MATH__
# include <math.h>
# endif
#endif
strings.h vs. string.h
某些系统可能有特殊之处,使之既不能被当做是System V,也不能被当做是BSD。我们可以像下面这样处理:
#ifdef SYSV
#ifndef SYSV_STRINGS
# define SYSV_STRINGS
#endif
#endif
#ifdef _STDH_ /* ANSI C Standard header files */
#ifndef SYSV_STRINGS
# define SYSV_STRINGS
#endif
#endif
#ifdef macII
#ifndef SYSV_STRINGS
# define SYSV_STRINGS
#endif
#endif
#ifdef vms
#ifndef SYSV_STRINGS
# define SYSV_STRINGS
#endif
#endif
#ifdef SYSV_STRINGS
# include <string.h>
# define index strchr
# define rindex strrchr
#else
# include <strings.h>
#endif
我们可以很容易的发现,类System V Unix系统对index和rindex使用了其他的名字,所处的头文件也不相同。尽管VMS支持的特性比System V更好,但它仍被认为是个特例。
time.h and types.h
使用time.h时,必须包含types.h,如以下代码所示。
#ifdef macII
# include <time.h> /* on a Mac II we need this one as well */
#endif
#ifdef SYSV
# include <time.h>
#else
# ifdef vms
# include <time.h>
# else
# ifdef CRAY
# ifndef __TYPES__ /* it is not protected under CRAY */
# define __TYPES__
# include <sys/types.h>
# endif
# else
# include <sys/types.h>
# endif /* of ifdef CRAY */
# include <sys/time.h>
# endif /* of ifdef vms */
#endif
以上这些并不足以保证代码是可移植的,因为定义时间值的数据结构在所有系统中并非都相同的。不同的系统在time_t的值的表达方式上是不同的。标准只要求它是个数值。意识到这个难题,标准定义了difftime函数来比较两个time_t值的差,mktime函数来接收一个字符串并产生一个time_t值。
varargs.h vs. stdarg.h
在某些系统中,这两个头文件中的定义是冲突的。例如,以下的代码在VMS下会有编译错误。
#include <varargs.h>
#include <stdio.h>
因为stdio.h包含了stdarg.h,而stdarg.h重定义了那些在varargs.h定义的符号(例如va_start, va_end)。我们选择的解决方案是总是在最后包含varargs.h,并且不要在使用varargs.h的模块或使用省略号的函数中定义这些符号。
运行时库
getlogin:
VMS未定义该函数,我们可以使用getenv("HOME")代替。
scanf:
scanf在不同平台下的行为是不同的。因为标准中对它的描述在某些情况下会有不同的解释。最可移植的输入解释器需要你自己写。
setjmp and longjmp:
从comp.std.c得到的匿名消息称:“草案X3.159对setjmp和longjmp的实现与标准的要求不符,甚至也与它自己的文档定义不符。而且这些定义在每个系统中也是不同的。因此,最好不要太依赖这些功能自身超出标准的那些语义。”
换句话说,你不是不应该使用它们,而是应该小心的使用它们。此外,从嵌套的信号处理中调用longjmp的行为是未定义的。
最后说一下,_setjmp和_longjmp只定义在SunOS, BSD和HP-UX上。
编译器限制
实际上,我们可以举一反三的列举出很多这样的未申明的编译器限制。
* 这些限制中有些只是编译器的BUG。很多这样的BUG都与优化器有关,所以当我们面对一个新环境时,最好明确的关闭优化选项,直到程序能够正常运行为止。
* 某些编译器不能处理大的模块或者大的语句。因此,我们最好将模块的大小限制在一个合理的范围内。顺便说一下,大模块更难编辑和理解。
译注:模块的定义,我倾向于以下文章所说的:http://blog.ednchina.com/wnhb/17958/message.aspx
使用浮点数
实现数字算法并使其能够跨越各种平台表现出相同的行为是困难的,而这还只是保守的说法。这一章节对这个问题的作用十分有限,但我们还是希望它能值得一读。我们将展开这一节,来讨论一些额外的建议和信息。
机器常量
获得机器常量是编写数字算法时的一个问题。我们需要的值一般有:
* 浮点表示的基数。
* 浮点表示的小数部分的位数。
* 浮点表示的指数部分的位数。
* 小数部分最小的正浮点数。
* 最小的指数。
* 最大的浮点数。
译注:浮点数可以表示成A*(B^C)的形式,其中A为小数部分,B为基数,C为指数部分。
在Sun的系统上,这些可以在values.h中获得。ANSI C标准要求这些常量定义在头文件float.h中。
Sun和标准的不同,导致这些值并不总是可读的,例如,在运行UTek的Tektronix工作站上。一种解决办法是从machar网络中获得程序的修改版。Machar在[Cod88]中有描述,并可以通过ftp从netlib中获得。
可以直接修改C版本的machar以生成C预处理器文件,这些文件可以直接被C程序包含。
还有一个叫做config.c的程序,它的可用范围较广。它尝试定义了C编译器的很多属性,以及运行它的机器。这个程序可以从comp.sources.misc获得。
浮点参数
在K&R[KR78]时代,鼓励使用float和double。因此那时使用浮点数的表达式几乎都是使用double表示的。而这对于那些用C实现的数字算法来说是一个噩梦。实际上,这个规则依赖于浮点参数,但大多数的编译器并不在乎参数是float,还是double。
根据ANSI C标准,即使没有声明函数原型,这个程序仍会继续执行。因此我们必须确保在编译函数定义时,包含了函数原型。这样编译器就能够检查参数是否匹配了。
浮点计算
在比较浮点类型,使用==和!=运算符时要小心。诸如if (float_expr1 == float_expr2)之类的表达式因为舍入误差的关系而很难满足。如果想感性的了解一下舍入误差的话,可以使用你喜欢的C编译器计算一下下面的表达式:[KM86]
10^50 + 812 - 10^50 + 10^55 + 511 - 10^55 = 812 + 511 = 1323
如果程序使用float或double的话,大多数的计算机会忽略这些误差而产生0。所以尽管这里的错误绝对是很严重的,但相对的影响却很小,很多程序都可以正常运行。
跟好的办法是使用诸如|float_expr1 - float_expr2| <= K或||float_expr1/float_expr2| - 1.0| <= K(if float_expr2 != 0.0)之类的表达式。这里K的取值范围是0 < K < 1,它可以是以下几种取值之一:
1. 浮点数,例如float或double。
2. 与机器体系结构相关的(就是在之前的章节定义的机器常量)。
3. 输入的值的精确度或算法引入的舍入误差的精确度。
其他可能存在的问题与程序可能选择的解决方案
开发可靠并高效的算法是非常困难的。我们通常需要验证结果在合理的范围内是否正确。参考文献[PFTV88]是很有用的。
* 要注意使用double表示并不一定能提高精度。实际上,在大多数的算法实现中,降低了精度,来增大数表示的范围。
* 不要在不必要的地方使用double,因为在大多数情况下,这会有很大的性能损失。而且如果不需要额外的数位参与计算的话,也就没有理由使用更高精度。需要的精度更多取决于输入数据的精度和使用的算法。
异常
浮点异常(超出上限,低于下限,除以0等)在某些系统中并不会被自动标记。在那种情况下,这些功能必须被显式的打开。
通常总是打开浮点异常的功能,因为这可能是某些方法不稳定时的一种提示。否则的话,就必须保证这些事件不会影响输出。
VMS
在这一节中,我们将讨论向VMS环境移植C程序所要面对的一般性问题,以及一些之前我们未曾提及的地方。
文件描述符
在VMS下你可以使用两种受欢迎的命令解释器:DCL和DEC/Shell。DCL下的文件描述符的语法与Unix的语法有明显的不同。
在VMS下的某些C运行时库中,那些以文件描述符为参数或者返回文件描述符的函数可以接受一个表示使用的语法的附加参数。因此像下面这样通过宏使用这些运行时库函数是很有用的。
#ifdef VMS
#ifndef VMS_CI /* Which Command Interpreter flavour to use */
# define VMS_CI 0 /* 0 for DEC/Shell, 1 for DCL */
#endif
# define Getcwd(buff,siz) getcwd((buff),(siz),VMS_CI)
# define Getname(fd,buff) getname((fd),(buff),VMS_CI)
# define Fgetname(fp,buff) fgetname((fp),(buff),VMS_CI)
#else
# define Getcwd(buff,siz) getcwd((buff),(siz))
# define Getname(fd,buff) getname((fd),(buff))
# define Fgetname(fp,buff) fgetname((fp),(buff))
#endif /* of ifdef VMS */
从用户或环境变量(例如,使用getenv函数)那里获得文件描述符的操作可能潜藏了更多的缺陷。
杂谈
end, etext, edata:
这些全局符号在VMS中是不可用的。
结构体赋值:
VAX C允许在两个大小相同的结构体之间赋值,但该特性是不可移植的。
系统函数:
VMS下的系统函数和Unix版的相同函数具有相同的功能,除此之外,命令解释器也具有相同的功能。如果用户正在使用DCL的话,那么程序也必须发送类DCL命令。
链接器:
链接器只允许链接存储在库中的模块。(译注:这里的库,我猜测或许指的是该系统的标准库,也或许是某个专门放库的路径。希望懂行的人,能够指正。)如果模块的全局函数没有被显式调用(被其他模块引用),那么该模块就根本不会被链接。系统并不关心某个全局变量是否被使用,因此这些变量也没有初始化。
最简单的解决方案是使用/INCLUDE参数,强制链接器链接模块。当然,这样做也有可能使命令行超过256字节。(汗!)(译注:原文如此,看来老外也有幽默的时候。)
一般方针
机器体系结构, 类型兼容, 指针等
1. 绝不要假定某种给定类型的大小,特别是指针类型。[Can89]诸如x&=0177770之类的语句,其实暗中用到了x的大小。如果该语句的目的是为了清除低三位的话,那么最好使用x&=~07。因为如果x是32位的话,第一种方法也会清除掉高16位。
2. 在某些体系结构中,字节序是相反的。这通常被称作小端(little-endian)结构和大端(big-endian)结构。这个问题,可以通过以下代码展示:
long int str[2] = {0x41424344, 0x0}; /* ASCII "ABCD" */
printf ("%s\n", (char *)&str);
小端设备(如VAX)将打印"DCBA",大端设备(如MC68000微处理器)将打印"ABCD"。
3. 当分配内存或使用指针时,要注意对齐限制。某些体系结构限制某些操作数赋值的地址。(这些地址通常是2的k次方的整数倍,这里k>0)
4. [Can89]指向对象的指针可能会有相同的大小和不同的格式。正如以下代码所示:
int *p = (int *) malloc(...); ... free(p);
这个代码在那些int*与char*的表示方法不同的体系结构中可能存在缺陷,因为free需要后一种形式的指针。
5. [Can89]对于某一类型的指针来说,只有==和!=运算符是有定义的。其余的比较运算符(<, <=, >, and >=)只对指向同一个数组或数组后的第一个元素的指针有意义。这对那些作用在指针上的算术运算符也是一样的。
6. 绝不要重定义NULL符号。NULL符号应该总是等于常数0。一个给定类型的空指针总是和常数0比较,而非空指针则既可以和值为0的变量比较,也可以和非0常量比较以执行特定的行为。
如果显式或隐式执行类型转换的话,一种类型的空指针总可以转换成另一种类型的空指针。(参见上面的第4条)
空指针可以指向任意内容,废除它会导致发生一些奇怪的事。
编译器的差异
1. 在表达式中使用的char类型在绝大多数实现中,会被当成是无符号数,但在有些系统(如VAX C和HP-UX)中却会被当成是有符号数。建议在算术表达式中使用它时,总是进行类型转换。
2. 不要信赖自动变量的初始化以及malloc函数返回的内存。
3. 某些编译器(如VAX C)要求结构中的位域是int型或unsigned型。而且,位域长度的上限在不同的实现中也是不同的。
4. sizeof的结果可能是unsigned型的。
文件
1. 保证文件的大小在合理的范围内,以使之不超过某些编译器的上限。
2. 文件名不要超过14字节。(许多System V派生的系统有这个限制,而在BSD派生的系统中这个限制通常是15字节。)在某些系统中这个限制甚至只有8字节。这些限制通常并不是由操作系统限制的,而是由诸如ar之类的系统应用限制的。
3. 不要使用特殊的字符,特别是多个点。(点在VMS中有特殊的含义)
杂谈
作为参数的函数:
当使用函数作为参数的使用,通常不要使用指针。换句话说,如果F是个函数指针,使用(*F)代替F,因为某些编译器可能不认识后者。
系统依赖:
隔离系统依赖的代码到单独的模块并使用条件编译。
工具:
诸如make之类的编译及链接工具可以简化程序移植的任务。
名字空间的污染:
减少程序中全局变量的数量。这样做的好处之一就是减少系统定义的函数之间发生冲突的可能性。
字符串常量:
不要修改字符串常量,因为在很多实现中,会将之放在只读内存中。而且这也是标准要求的常量本身固有的行为。
译者后记
最近对跨平台移植比较感兴趣,故而翻译了这篇文章。个人感觉作者的经验非常丰富,但文章的内容过于细节,缺少方法论和思辨。加之所讨论的平台都是UNIX平台,不能代表当今的主流平台,所以其参考意义大于实际功用。权当是译者练习科技英语翻译的小成果吧。