码字不易,对你有帮助 点赞/转发/关注 支持一下作者
微信搜公众号:不会编程的程序圆
看更多干货,获取第一时间更新
代码,练习上传至:
https://github.com/hairrrrr/C-CrashCourse
了解更多有关可移植可以参考《How to Write Portable Software in C》(Prentice-Hall)。
本章主要讨论几个常见的错误来源,重点放在语言属性上,而非函数库属性上。
一 可移植性缺陷
1. 应对 C 语言标准变更
这种语言标准的变更使得 C 程序的编写者面临一个两难境地:程序中是否应该用到新的特性呢? 如果使用它们,程序无疑更加容易编写,而且不大容易出错,但是那样做也有代价,那就是这些程序在较早的编译器上将无法工作。
本书的 4.4节讨论了一个这类例子:函数原型的概念。让我们回想一下 4.4 节中提到的 square 函数:
double
square (double x){
return x * x;
}
如果这样写,这个函数在很多编译器上都不能通过编译。如果我们按照旧风格来重写这个函数,因为 ANSI 标准为了保持和以前的用法兼容也允许这种形式,这就增强了它的可移植性:
double
square (x)
double x;
{
return x*x;
}
这种可移植性的获得当然也付出了代价。为了与旧用法保持一致, 我们必须在调用了 square 函数的程序中作如下声明:
double square();
函数声明中略去参数类型的说明,这在 ANSI C 标准中也是合法的。因为这样的声明并没有对参数类型做出任何说明,就意味着如果在函数调用时传入了错误类型的参数,函数调用就会不声不响地失败:
double square();
main(){
printf("%g\n",square(3));
}
函数square的声明中并没有对参数类型做出说明,因此在编译 main 函数时,编译器无法得知函数 square 的参数类型应该是 double,而不是 int 。这样,程序打印出的将是一堆 “垃圾信息”。要检测这类问题,有一个办法就是使用 lint 程序,前提是编程者的 C 语言实现提供了这一工具。
如果上面的程序被写成了这样:
double square (double);
main(){
printf("%g\n", square(3));
}
这里,3 会被自动转换为double类型。
另种改写的方式是,在这个程序中显式地给函数 square 传入一个 double 类型的参数:
double square() ;
main()
{
printf ("%g\n", square(3.0));
}
这样做程序就能得到正确的结果。即使是对于那些不允许在函数声明中包括参数类型的旧编译器,第二种写法也仍然能够使程序照常工作。
许多有关可移植性的决策都有类似的特点。一个程序员是否应该使用某个新的或特定的特性?使用该特性也许能给编程带来巨大的方便,但代价却是使程序失去了一部分潜在用户。
2. 标识符名称的限制
某些 C 语言实现把一个标识符中出现的所有字符都作为有效字符处理,而另一些 C实 现却会自动地截断一个长标识符名称的尾部。连接器也会对它们能够处理的名称强加限制,例如外部名称中只允许使用大写字母。C实现者在面对这样的限制时,一个合理的选择就是强制所有的外部名称必须是大写。事实上,ANSI C标准所能保证的只是,C实现必须能够区别出前 6 个字符不同的外部名称。而且,这个定义中并没有区分大写字母与其对应的小写字母。
因为这个原因,为了保证程序的可移植性,谨慎地选择外部标识符的名称是重要的。比方说,两个函数的名称分别为 print_fields
与 print_float
这样的命名方式就不恰当;同理, 使用 State 与 STATE 这样的命名方式也不明智。
考虑以下函数:
char*
Malloc (unsigned n)
{
char *p, *malloc (unsigned) ;
p = malloc(n) ;
if (p == NULL)
panic("out of memory") ;
return p;
}
上面的例子程序演示了一个确保检测到内存耗尽的异常情况的简单办法。编程者的想法是,在程序中应该调用 malloc 函数分配内存的地方,改为调用 Malloc 函数。如果 malloc 函数调用失败,则 panic 函数将被调用,panic 函数终止程序,并打印出一条恰当的出错消息。这样,客户程序就不必在每次调用malloc函数时都要进行检查。
然而,考虑一下如果这个函数的编译环境是不区分外部名称大小写的 C 语言实现,将会发生怎样的情况呢? 此时,函数malloc 与Malloc 实际上是等同的。也就是说,库函数 malloc将被上面的 Malloc 函数等效替换。当在 Malloc 函数中调用库函数 malloc 时,实际上调用的却是 Malloc 函数自身!当然,尽管函数 Malloc 在那些区分大小写的C语言实现上仍然能够正常工作,但在这种情况下结果却是:程序在第一次试图分配内存时对 Malloc 函数的调用将引起一系列的递归调用, 而这些递归调用又不存在一个返回点,最后引发灾难性的后果!
3. 整数的大小
C语言中为编程者提供了3种不同长度的整数: short
型、int
型和 long
型,C 语言中的字符行为方式与小整数相似。C语言的定义中对各种不同类型整数的相对长度作了一些规定:
-
3种类型的整数其长度是非递减的。也就是说,short 型整数容纳的值肯定能够被 int 型整数容纳,int 型整数容纳的值也肯定能够被 long 型整数容纳。对于一个特定的 C 语言实现来说,并不需要实际支持 3 种不同长度的整数,但可能不会让 short 型整数大于 int 型整数,而 int 型整数大于 long 型整数。
-
一个普通(int 类型)整数足够大以容纳任何数组下标。
-
字符长度由硬件特性决定。
ANSI 标准要求 long 型整数的长度至少应该是 32 位,而 short 型和 int 型整数的长度至少应该是 16 位。因为大多数机器中字符长度是8位,对这些机器而言最方便的整数长度是 16 位和 32 位,因此所有早期的C编译器也都能够满足这些限制条件。
程序员当然可以用一个 int 型整数来表示一个数据表格的大小或者数组的下标。但如果一个变量需要存放可能是千万数量级的数值,又该如何呢?
要定义这样一个变量,可移植性最好的办法就是声明该变量为 long 型,但在这种情况下我们定义一个“新的”类型无疑更为清晰: