#define FOOTYPE struct foo FOOTYPE a; FOOTYPE b, c;
这允许程序员可以通过只改变程序中的一行就能改变a、b和c的类型,尽管a、b和c可能声明在很远的不同地方。
使用这样的宏定义还有着可移植性的优势——所有的C编译器都支持它。很多C编译器并不支持另一种方法:
typedef struct foo FOOTYPE;
这将FOOTYPE定义为一个与struct foo等价的新类型。
这两种为类型命名的方法可以是等价的,但typedef更灵活一些。例如,考虑下面的例子:
#define T1 struct foo * typedef struct foo * T2;
这两个定义使得T1和T2都等价于一个struct foo的指针。但看看当我们试图在一行中声明多于一个变量的时候会发生什么: T1 a, b; T2 c, d;
第一个声明被扩展为:
struct foo * a, b;
这里a被定义为一个结构指针,但b被定义为一个结构(而不是指针)。相反,第二个声明中c和d都被定义为指向结构的指针,因为T2的行为好像真正的类型一样。
7 可移植性缺陷
C被很多人实现并运行在很多机器上。这也正是在一个地方写的C程序应该能够很容易地转移到另一个编程环境中去的原因。
然而,由于有很多的实现者,它们并不和其他人交流。此外,不同的系统有不同的需求,因此一台机器上的C实现和另一台上的多少会有些不同。
由于很多早期的C实现都关系到UNIX操作系统,因此这些函数的性质都是专于该系统的。当一些人开始在其他系统中实现C时,他们尝试使库的行为类似于UNIX系统中的行为。
但他们并不总是能够成功。更有甚者,很多人从UNIX系统的不同版本入手,一些库函数的本质不可避免地发生分歧。今天,一个C程序员如果想写出对于不同环境中的用户都有用的程序就必须知道很多这
26
些细微的差别。
7.1 一个名字中都有什么?
一些C编译器将一个标识符中的所有字符视为签名。而另一些在存储标识符时会忽略一个极限之外的所有字符。C编译器产生的目标程序同将要被加载器进行处理以访问库中的子程序。加载器对于它们能够处理的名字通常应用自己的约束。
一个常见的加载器约束是所有的外部名字必须只能是大写的。面对这样的加载器约束,C实现者会强制要求所有的外部名字都是大写的。这种约束在C语言参考手册中第2.1节由所描述。
一个标识符是一个字符和数字序列,第一个字符必须是一个字母。下划线_算作字母。大写字母和小写字母是不同的。只有前八个字符是签名,但可以使用更多的字符。可以被多种汇编器和加载器使用的外部标识符,有着更多的限制:
这里,参考手册中继续给出了一些例子如有些实现要求外部标识符具有单独的大小写格式、或者少于八个字符、或者二者都有。
正因为所有这些,在一个希望可以移植的程序中小心地选择标识符是很重要的。为两个子程序选择print_fields和print_float这样的名字不是个好办法。
考虑下面这个显著的函数:
char *Malloc(unsigned n) { char *p, *malloc(); p = malloc(n); if(p == NULL)
panic(\ return p; }
这个函数是保证耗尽内存而不会导致没有检测的一个简单的办法。程序员可以通过调用Mallo()来代替malloc()。如果malloc()不幸失败,将调用panic()来显示一个恰当的错误消息并终止程序。
然而,考虑当该函数用于一个忽略大小写区别的系统中时会发生什么。这时,名字malloc和Malloc是等价的。换句话说,库函数malloc()被上面的Malloc()函数完全取代了,当调用malloc()时它调用的是它自己。显然,其结果就是第一次尝试分配内存就会陷入一个递归循环并随之发生混乱。但在一些能够区分大小写的实现中这个函数还是可以工作的。
7.2 一个整数有多大?
C为程序员提供三种整数尺寸:普通、短和长,还有字符,其行为像一个很小的整数。C语言定义对各种整数的大小不作任何保证:
整数的四种尺寸是非递减的。
普通整数的大小要足够存放任意的数组下标。
27
字符的大小应该体现特定硬件的本质。
许多现代机器具有8位字符,不过还有一些具有7位获9位字符。因此字符通常是7、8或9位。
长整数通常至少32位,因此一个长整数可以用于表示文件的大小。
普通整数通常至少16位,因为太小的整数会更多地限制一个数组的最大大小。
短整数总是恰好16位。
在实践中这些都意味着什么?最重要的一点就是别指望能够使用任何一个特定的精度。非正式情况下你可以假设一个短整数或一个普通整数是16位的,而一个长整数是32位的,但并不保证总是会有这些大小。你当然可以用普通整数来压缩表大小和下标,但当一个变量必须存放一个一千万的数字的时候呢?
一种更可移植的做法是定义一个“新的”类型:
typedef long tenmil;
现在你就可以使用这个类型来声明一个变量并知道它的宽度了,最坏的情况下,你也只要改变这个单独的类型定义就可以使所有这些变量具有正确的类型。
7.3 字符是带符号的还是无符号的?
很多现代计算机支持8位字符,因此很多现代C编译器将字符实现为8位整数。然而,并不是所有的编译器都按照同将的方式解释这些8位数。
这些问题在将一个char制转换为一个更大的整数时变得尤为重要。对于相反的转换,其结果却是定义良好的:多余的位被简单地丢弃掉。但一个编译器将一个char转换为一个int却需要作出选择:将char视为带符号量还是无符号量?如果是前者,将char扩展为int时要复制符号位;如果是后者,则要将多余的位用0填充。
这个决定的结果对于那些在处理字符时习惯将高位置1的人来说非常重要。这决定着8位的字符范围是从-128到127还是从0到255。这又影响着程序员对哈希表和转换表之类的东西的设计。
如果你关心一个字符值最高位置一时是否被视为一个负数,你应该显式地将它声明为unsigned char。这样就能保证在转换为整数时是基0的,而不像普通char变量那样在一些实现中是带符号的而在另一些实现中是无符号的。
另外,还有一种误解是认为当c是一个字符变量时,可以通过写(unsigned)c来得到与c等价的无符号整数。这是错误的,因为一个char值在进行任何操作(包括转换)之前转换为int。这时c会首先转换为一个带符号整数再转换为一个无符号整数,这会产生奇怪的结果。
正确的方法是写(unsigned char)c。
7.4 右移位是带符号的还是无符号的?
这里再一次重复:一个关心右移操作如何进行的程序最好将所有待移位的量声明为无符号的。
28
7.5 除法如何舍入?
假设我们用b除a得到商为q余数为r:
q = a / b; r = a % b;
我们暂时假设b > 0。
我们期望a、b、q和r之间有什么关联?
最重要的,我们期望q * b + r == a,因为这是对余数的定义。
如果a的符号发生改变,我们期望q的符号也发生改变,但绝对值不变。
我们希望保证r >= 0且r < b。例如,如果余数将作为一个哈希表的索引,它必须要保证总是一个有效的索引。
这三点清楚地描述了整数除法和求余操作。不幸的是,它们不能同时为真。
考虑3 / 2,商1余0。这满足第一点。而-3 / 2的值呢?根据第二点,商应该是-1,但如果是这样的话,余数必须也是-1,这违反了第三点。或者,我们可以通过将余数标记为1来满足第三点,但这时根据第一点商应该是-2。这又违反了第二点。
因此C和其他任何实现了整数除法舍入的语言必须放弃上述三个原则中的至少一个。
很多程序设计语言放弃了第三点,要求余数的符号必须和被除数相同。这可以保证第一点和第二点。很多C实现也是这样做的。
然而,C语言的定义只保证了第一点和|r| < |b|以及当a >= 0且b > 0时r >= 0。 这比第二点或第三点的限制要小,实际上有些编译器满足第二点或第三点,但不太常见(如一个实现可能总是向着距离0最远的方向进行舍入)。
尽管有些时候不需要灵活性,C语言还是足够可以让我们令除法完成我们所要做的、提供我们所想知道的。例如,假设我们有一个数n表示一个标识符中的字符的一些函数,并且我们想通过除法得到一个哈希表入口h,其中0 <= h <= HASHSIZE。如果我们知道n是非负的,我们可以简单地写:
h = n % HASHSIZE;
然而,如果n有可能是负的,这样写就不好了,因为h可能也是负的。然而,我们知道h > -HASHSIZE,因此我们可以写:
h = n % HASHSIZE; if(n < 0)
h += HASHSIZE;
同样,将n声明为unsigned也可以。
29
7.6 一个随机数有多大?
这个尺寸是模糊的,还受库设计的影响。在PDP-11[10]机器上运行的仅有的C实现中,有一个称为rand()的函数可以返回一个(伪)随机非负整数。PDP-11中整数长度包括符号位是16位,因此rand()返回一个0到215-1之间的整数。
当C在VAX-11上实现时,整数的长度变为32位长。那么VAX-11上的rand()函数返回值范围是什么呢?
对于这个系统,加利福尼亚大学的人认为rand()的返回值应该涵盖所有可能的非负整数,因此它们的rand()版本返回一个0到231-1之间的整数。
而AT&T的人则觉得如果rand()函数仍然返回一个0到215之间的值 则可以很容易地将PDP-11中期望rand()能够返回一个小于215的值的程序移植到VAX-11上。
因此,现在还很难写出不依赖实现而调用rand()函数的程序。
7.7 大小写转换
toupper()和tolower()函数有着类似的历史。他们最初都被实现为宏:
#define toupper(c) ((c) + 'A' - 'a') #define tolower(c) ((c) + 'A' - 'a')
当给定一个小写字母作为输入时,toupper()将产生相应的大写字母。tolower()反之。这两个宏都依赖于实现的字符集,它们需要所有的大写字母和对应的小写字母之间的差别都是常数的。这个假设对于ASCII和EBCDIC字符集来说都是有效的,可能不是很危险,因为这些不可移植的宏定义可以被封装到一个单独的文件中并包含它们。
这些宏确实有一个缺陷,即:当给定的东西不是一个恰当的字符,它会返回垃圾。因此,下面这个通过使用这些宏来将一个文件转为小写的程序是无法工作的: int c;
while((c = getchar()) != EOF) putchar(tolower(c));
我们必须写: int c;
while((c = getchar()) != EOF)
putchar(isupper(c) ? tolower(c) : c);
就这一点,AT&T中的UNIX开发组织提醒我们,toupper()和tolower()都是事先经过一些适当的参数进行测试的。考虑这样重写这些宏:
30