C语言在许多不同的系统平台上都有实现。的确,使用C语言编写程序的一个首要原因就是,C程序能够方便地在不同的编程环境中移植。
  不同的系统有不同的需求,因此我们应该能够预料到,机器不同则其上的C语言实现也由细微差别。今天,一个C程序员如果希望自己写的程序在另一个编程环境也能够工作,他就必须掌握许多这类细小的差别。本章要讨论的是关于可移植性的几个最常见的错误来源,重点放在语言的属性上,而不是在函数库的属性上。
 

7.1 标识符名称的限制

  某些C语言实现把一个标识符中出现的所有字符都作为有效字符处理,而另一些C实现却会自动地截断一个长标识符名称的尾部。连接器也会对它们能够处理的名称强加限制,例如外部名称中只允许使用大写字母。C实现者在面对这样的限制时,一个合理的选择就是强制所有的外部名称必须是大写。
  因为这个原因,为了保证程序的可移植性,谨慎地选择外部标识符的名称是重要的。比如说,两个函数的名称分别为 printf_fields 与 printf_float,这样的命名方式就不恰当;同理,使用 State 与 STATE 这样的命名方式也不明智。
  下面这个例子多少有些让人吃惊,考虑以下函数:

    char    *Malloc(unisgned n)
    {
        char    *p, *malloc(unsigned);

        p = malloc(n);
        if (p == NULL)
        {
            printf("out of memory");
            exit(-1);
        }
        return p;
    }

  上面的例子程序演示了一个确保检测到内存耗尽的异常情况的简单办法:在程序中应该调用 malloc 函数分配内存的地方,改为调用 Malloc 函数。这样,客户程序就不必在每次调用 malloc 函数时都需要进行检查。
  然而,考虑以下如果这个函数在一个不区分外部名称大小写的C语言中实现,将会发生怎样的情况呢?此时,函数 Malloc 与 malloc 实际上是等同的,也就是说,库函数 malloc 将被上面的 Malloc 函数等效替换。当在 Malloc 函数中调用库函数 malloc 时,实际上调用的却是 malloc 函数本身!

7.3 整数的大小

  C语言中为编程者提供了3种不同长度的整数:short 型、int 型和 long 型,C语言中的字符行为方式与小整数相似。C语言的定义对各种不同类型整数的相对长度作了一些规定:

    1. short 型整数容纳的值肯定能够被 int 型整数容纳,int 型整数容纳的值也肯定能够被 long 型整数容纳。
    1. 字符长度由硬件特性决定。
        因此,不同机器的字符长度不同,有8位的,有9位的,也有16位的。假如一个数组需要存放的数值数量不确定,可能是几十个,也可能是千万数量级,它的类型应该选择什么呢?要定义一个这样的变量,可移植性最好的办法就是声明该变量为 long 型,但在这种情况下我们定义一个“新的”类型无疑更为清晰:
    typedef    long    tenmil;

  当数组需要存放的数量变为几十个时,只需修改 tenmil 的类型定义为 char 型即可。

7.4 字符是有符号整数还是无符号整数

  现代大多数计算机都支持8位字符,因此大多数现代C编译器都把字符实现为8位整数。然而,并非所有的编译器都按照同样的方式来解释这些8位数值。
  编译器在转换 char 类型到 int 类型时,需要做出选择:应该将字符作为有符号数还是作为无符号数处理?如果是前一种情况,编译器在将 char 类型的数扩展到 int 类型时,应该同时复制符号位;而如果是后一种情况,编译器只需在多余的位上直接填充0即可。
  与之相关的一个常见错误认识是:如果 c 是一个字符变量,使用 (unsigned) c 就可得到与 c 等价的无符号整数。这是会失败的,因为在将字符 c 转换为无符号整数时, c 将首先被转换为 int 型整数,而此时可能得到非预期的后果。
  正确的方式是使用语句 (unsigned char) c,因为一个 unsigned char 类型的字符在转换为无符号整数时无需首先转换为 int 型整数,而是直接进行转换。

7.5 移位运算符

  使用移位运算符的程序员经常对这样两个问题感到困惑:

    1. 在向右移位时,空出的位是由 0 填充,还是由符号位的副本填充?
    1. 移位计数(即移位操作的位数)允许的取值范围是什么?
        第一个问题的答案: 如果被移位的对象是无符号数,那么空出的位将被 0 填充。如果被移位的对象是有符号数,那么C语言实现既可以用 0 填充空出的位,也可以用符号位的副本填充空出的位。
        第二个问题的答案:如果被移位的对象长度是 n 位,那么移位计数必须大于或等于0,而严格小于 n。因此,不可能做到在单次操作中将某个数值中的所有位都移出。
        需要注意的是,有符号整数的向右移位运算并不等同于除以 2 的某次幂,例如, (-1)>>1,这个操作的结果一般不可能为0,但是 (-1)/2 在大多数C实现上求值结果都是0。

7.6 内存位置0

  NULL 指针并不指向任何对象。因此,除非是用于赋值或比较运算,出于其他任何目的使用NULL指针都是非法的。例如,如果 p 或 q 是一个NULL指针,那么strcmp(p,q)的值就是未定义的。
  在这种情况下究竟会得到什么结果呢?不同的编译器有不同的结果。某些C语言实现对内存位置 0 强加了硬件级的读保护,在其上工作的程序如果错误使用了一个NULL指针,将立即终止执行。其它一些C语言实现对内存位置 0 只允许读,不允许写。
  严格来说,这并非一个可移植问题:在所有的C程序中,误用NULL指针的效果都是未定义的。然而,这样的程序有可能在某个C语言实现上“似乎”能够工作,只有当该程序转换到另一台机器上运行时才会暴露出问题来。
  要检查出这类问题的最简单办法就是,把程序移到允许读取内存位置 0 的机器上运行。下面的程序将揭示出某个C语言实现是如何处理内存地址 0 的:

    #include <stdio.h>

    main()
    {
        char    *p;
        p = NULL;
        printf("location 0 contains %d\n",*p);
    }

  在禁止读取内存地址 0 的机器上,这个程序将会执行失败。在其他机器上,这个程序将会以10进制的格式打印出内存位置 0 中存储的字符内容。

7.7 随机数的大小

  C语言提供了一个称为 rand 的函数,该函数的作用是产生一个(伪)随机非负整数。随机数的取值范围跟机器的整数长度有关,不同机器的随机数取值范围不同。当机器的整数长度为16位时, rand 函数将返回一个介于 0 到 2^15-1 之间的整数。
  这样造成的后果时,如果我们的程序中用到了 rand 函数,在移植时就必须根据特定的C语言实现作出“裁剪”。ANSI C标准中定义了一个常数 RAND_MAX,它的值等于随机值的最大取值。

7.8 大小写转换

  库函数 toupper 和 tolower 也有与随机数类似的历史。它们起初被实现为宏。有一次,AT&T软件开发部门的一个极具创新精神的人注意到,大多数 toupper 和 tolower 的使用都需要首先进行检查以保证参数是合适的。慎重考虑之后,他决定把这些宏重写如下:

    #define    toupper(c)    ( (c)>='a' && (c)<='z' ? (c)+'A'-'a' : (c) )
    #define    toupper(c)    ( (c)>='A' && (c)<='Z' ? (c)+'a'-'A' : (c) )    

  他又意识到这样做有可能在每次宏调用时,导致 c 被求值 1 到 3 次。如果遇到类似 toupper(p++)这样的表达式,可能造成不良后果。因此,他决定重写 toupper 和 tolower 为函数,重写后的 toupper 函数看上去大致像这样:

    int    toupper(int    c)
    {
        if( c>='a' && c<='z' )
            return    c+'A'-'a';
        return    c;
    }

  这样改动之后程序的健壮性无疑得到了加强,而代价是每次使用这些函数时却又引入了函数调用的开销。他意识到某些人也许不愿意付出效率方面损失的代价,因此他又重写引入了这些宏,不过使用了新的宏名:

    #define    _toupper(c)    ( (c)>='a' && (c)<='z' ? (c)+'A'-'a' : (c) )
    #define    _toupper(c)    ( (c)>='A' && (c)<='Z' ? (c)+'a'-'A' : (c) )    

  这样,宏的使用者就可以在速度与方便之间自由选择。

  看到这里,读者也许会叹一口气,为了满足可移植性,需要做的工作太多了!我们为什么要如此不辞辛劳地精益求精地修改呢?因为我们所处的是一个编程环境不断改变的世界,尽管软件看上去不像硬件那么实在,但大多数软件的生命周期却要长于它运行其上的硬件。而且,我们很难预测未来硬件的特性。因此,努力提高软件的可移植性,实际上是延迟了软件的生命周期。

[C陷阱和缺陷] 第7章 可移植性缺陷的更多相关文章

  1. c缺陷与陷阱笔记-第七章 可移植性代码

    1.移位运算符 如果被移位的对象长度是n位,那么移位计数必须>=0,并且<n,例如对于1个32位的数,移位运算n<<31和n<<0是OK的,n<<32和 ...

  2. [C陷阱和缺陷] 第3章 语义“陷阱”

    第3章 语义"陷阱"     一个句子哪怕其中的每个单词都拼写正确,而且语法也无懈可击,仍然可能有歧义或者并非书写者希望表达的意思.程序也有可能表面上是一个意思,而实际上的意思却相 ...

  3. [C陷阱和缺陷] 第1章 词法“陷阱”

    有感自己的C语言在有些地方存在误区,所以重新仔细把"C陷阱和缺陷"翻出来看看,并写下这篇博客,用于读书总结以及日后方便自身复习. 第1章 词法"陷阱" 1.1 ...

  4. [C陷阱和缺陷] 第6章 预处理器

      在严格意义上的编译过程开始之前,C语言预处理器首先对程序代码作了必要的转换处理.因此,我们运行的程序实际上并不是我们所写的程序.预处理器使得编程者可以简化某些工作,它的重要性可以由两个主要的原因说 ...

  5. [C陷阱和缺陷] 第2章 语法“陷阱”

    第2章 语法陷阱 2.1 理解函数声明   当计算机启动时,硬件将调用首地址为0位置的子例程,为了模拟开机时的情形,必须设计出一个C语言,以显示调用该子例程,经过一段时间的思考,得出语句如下: ( * ...

  6. [C陷阱和缺陷] 第5章 库函数

      有关库函数的使用,我们能给出的最好建议是尽量使用系统头文件,当然也可以自己造轮子,随个人喜好.本章将探讨某些常用的库函数,以及编程者在使用它们的过程中可能出错之处.   5.1 返回整数的getc ...

  7. [C陷阱和缺陷] 第4章 连接

    一个C程序可能是由多个分别编译的部分组成,这些不同部分通过连接器合并成一个整体.在本章中,我们将考查一个典型的连接器,注意它是如何对C程序进行处理的,从而归纳出一些由于连接器的特点而可能导致的错误. ...

  8. 软件测试价值提升之路- 第三章"拦截缺陷 "读书笔记

    作为一个测试团队,基本的职责是:测试产品,发现缺陷,报告结果,使每个版本的测试水准稳步提升.这些价值是作为一个测试所必须具备的,发挥这些价值能够让测试获得研发团队的基本信任.这类价值分为3部分: 1) ...

  9. 交换机安全学习笔记 第五章 DHCP缺陷攻击

    关于DHCP攻击有如下几类攻击方式:   一.耗尽DHCP地址池    通过随机生成源MAC地址,然后伪造DHCPDISCOVER数据包.耗尽DHCP服务器地址池.   免费的攻击工具:  Yersi ...

随机推荐

  1. 582. Kill Process

    Problem statement: Given n processes, each process has a unique PID (process id) and its PPID (paren ...

  2. ie下php session不能用(域名的合法定义)

    今天遇到了一个奇怪的问题.应用程序的后台ie下居然无法登陆,老是提示验证码不正确,明明输入是正确的.于是抓包.测试.调试,最终发现罪魁祸首phpsessionid在ie下没有办法写入.研究了一下,发现 ...

  3. [NOIP2005] 提高组 洛谷P1051 谁拿了最多奖学金

    题目描述 某校的惯例是在每学期的期末考试之后发放奖学金.发放的奖学金共有五种,获取的条件各自不同: 1) 院士奖学金,每人8000元,期末平均成绩高于80分(>80),并且在本学期内发表1篇或1 ...

  4. Thinkphp5.0 的使用模型Model删除数据

    Thinkphp5.0 的使用模型Model删除数据 一.使用destory()删除数据 //删除id为3的记录 $res = User::destroy(3); //返回影响的行数 dump($re ...

  5. 在fragment中获取activity的组件

    在fragment中使用getActivity()即可获取activity的引用

  6. AtCoder Grand Contest 012 B Splatter Painting(记忆化搜索)

    题意: 给一个包含N个顶点,M条边,无自环和重边的简单无向图,初始每个点颜色都为0,每条边的长度为1,连接着ai,bi两个节点.经过若干个操作, 每次将与某个点vi距离不超过di的所有点染成某种颜色c ...

  7. ubuntu语言设置成汉语

    打开设置system setting,进入语言支持,有语言和地区格式.下载须要的语言并应用到整个系统. 按说明来就可以 这样的方法使得部分英语变为汉语.

  8. Java中Comparator接口和Comparable接口的使用

    普通情况下在实现对对象元素的数组或集合进行排序的时候会用到Comparator和Comparable接口,通过在元素所在的类中实现这两个接口中的一个.然后对数组或集合调用Arrays.sort或者Co ...

  9. hdu 5253 连接的管道(kruskal)(2015年百度之星程序设计大赛 - 初赛(2))

    连接的管道 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Subm ...

  10. 关于jiffies回绕以及time_after,time_before

    系统中有非常多变量用来记录一个单调递增的现实,典型的有两个,一个是TCP的序列号.还有一个就是jiffies,可是由于计算机内表示的数字都是有限无界的,所以不论什么数字都不能做到全然意义的单调递增,它 ...