您的位置:首页 > 其它

读书摘要—C缺陷与陷阱

2007-02-09 21:01 211 查看
第一章 词法"陷阱"

    词法分析中的"贪心法":每一个词法符号应该包含尽可能多的字符。

第二章 语法陷阱

    复杂类型的声明

   一旦我们知道了如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到了,只需把变量声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一对括号整个"封装"起来即可。

   若变量fp是函数指针,那么如何调用fp指向的函数呢?标准方法如下:

   (*fp) ();

   ANSI C允许程序员将上式简写为fp(),但是一定要明白,这只是简写形式,最标准的形式还是 (*fp)()。

    运算符之间的优先级关系

   我们需要记住的最重要的两点是:
   
    1.任何一个逻辑运算符的优先级都低于任何一个关系运算符。
    2.移位运算符的优先级低于算术运算符,但比关系运算符要高。   
  
   特别注意,6个关系运算符的优先级并不相同。运算符==和!=的优先级要高于其他关系要高于其他关系运算符。

第三章 语义陷阱
 
   C语言中的数组必须注意以下两点:
   
    1.C语言只有一维数组,且数组大小须在编译阶段就确定(C99中支持VLA(变长数组))。
    2.对于一个数组,我们只能做两件事:确定该数组的大小,以及获得数组下标0的元素的指针。其它和数组相关的操作,哪怕乍看上去是以数组下标尽心运算的,实际上都是通过指针进行的。

   C语言中数组下标范围取0~n-1 带来的好处之一是能很方便的避免"栏杆错误"。

   "不对称边界"

   ANSI C标准允许:数组中实际上不存在的"溢界"元素的地址位于数组所占内存之后,这个地址可以用于赋值和比较晕眩,但是若用于引用,则就是非法的了。

   求值顺序和运算符的优先级是两个听起来类型但实际上完全不同的概念。
  
   C语言中只有四个运算符对操作数规定了明确的求指顺序:&& 、||、?和,。其它所有运算符对操作数求值的顺序都是未定义的。特别地,赋值运算符不保证任何求指顺序。

整数溢出

   C语言关于整数操作的上溢或下溢定义得非常明确。
   只要有一个操作数是无符号的,结果就是无符号的,并且以2n为模,其中n为字长。如果两个操作数都是带符号的,则结果是未定义的。
    例如,假设a和b是两个非负整型变量,你希望测试a + b 是否溢出。一个明显的办法是这样的:
    if(a + b < 0)
        complain();
   通常,这是不会工作的。
   一旦a + b 发生了溢出,对于结果的任何赌注都是没有意义的。例如,在某些机器上,一个加法运算会将一个内部寄存器设置为四种状态:正、负、零或溢出。 在这样的机器上,编译器有权将上面的例子实现为首先将a和b加在一起,然后检查内部寄存器状态是否为负。如果该运算溢出,内部寄存器将处于溢出状态,这个测试会失败。

   使这个特殊的测试能够成功的一个正确的方法是依赖于无符号算术的良好定义,既要在有符号和无符号之间进行转换:
    if((int)((unsigned)a + (unsigned)b) < 0)
        complain();
 
   C语言中存在两类整数算术运算:有符号运算和无符号运算。在无符号算术运算中,没有所谓的"溢出"一说:所有的无符号运算都是以2的n次方为模,这里n是结果中的位数。如果算术运算符的一个操作数是有符号整数,另一个是无符号整数,那么有符号征税会被转换称为无符号整数,"溢出"也就不可能发生。但是,当两个操作数都是有符号整数时,"溢出"就有可能发生,而且"溢出"的结果十未定义的。当一个运算的结果发生"溢出"时,作出任何假设都是不安全的。

   检验溢出的一种正确方式是将a和b都强制转换为无符号整数:
   
   if ( (unsigned)a + (unsigned)b >INT_MAX )
    complain();
   此处的INT_MAX定义为可能的最大整数值。ANSI C标准规定在<limits.h>中定义INT_MAX。
  
   另外一种不需使用无符号运算的可行方法是:
    if(a > INT_MAX -b)
       complain();

第四章 链接器

   可以大致想象链接器是如何工作的:链接器的输入是一组目标模块和库文件,输出是一个载入模块。对每个目标模块中的每个外部对象,链接器都要检查载入模块,看是否已有同名的外部对象。如果没有,链接器就将该外部对象添加到载入模块;如果有,链接器就需要处理命名冲突。

第五章 库函数

  getchar()的返回值类型为int而不是char,这是一个常忽略而且会导致错误的地方。

  在ANSI C中,很遗憾的是,在执行文件I/O时,一个输入操作与一个输出操作不能连续进行,除非在两者之间插入对fseek()函数的调用——这是为了保持和过去不能同时进行读写操作的程序的向下兼容性。

  关于全局变量errno使用上常犯的错误

A. ANSI C标准中,并没有强制要求库函数在调用成功的情况下,置errno的值为0
B. 反过来,库函数在调用成功的情况下,并未被禁止修改errno的值。
C. 总结前两条:标准对库函数与errno的唯一明确要求是在调用失败的情况下,将errno置为合适的值;其他情况下均属未定义。

    因此,使用errno的正确方法是:在调用库函数时,首先检测返回值是否标志着有错误发生,在确定存在错误的前提下,再检查errno的值,弄清楚出错的原因。

第六章 预处理器

   特别注意宏定义中的空格

   宏不是函数
   宏不是语句
   宏不是类型定义

第七章 可移植性

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

    一个常见的错误:如果c是一个char类型变量,使用(unsigned)c就可得到与c等价的无符号整数。实际上这是会失败的,因为在将字符c转换为无符号整数时,c将首先被转换为int类型的整数,而此时可能得到非预期的结果。
   正确的方式时使用(unsigned char)c。

   使用移位运算符常感到困惑的两个问题

   A.当向右移位时,空出的位是由0填充还是由符号位的副本填充?

    答案很简单,但有时与具体的C语言实现有关。如果操作数是无符号数,空位将被0填充。如果操作数是有符号数,那么C语言实现既可以用0填充空位,也可以用符号位的副本填充空位。
  
   B.移位位数的合法取值范围是什么?

    答案也很简单:若操作数位数为n,则移位位数必须大于等于0,而严格小于n。因此,不可能在单次操作中将操作数中的所有位移出。

   除法运算

    先不考虑特定的程序语言,假定我们让a除以b,商为q,余数为r:

     q = a/b;
     r = a%b;

    这里不妨假定b大于0。

    我们希望a、b、q、r之间能维持如下的关系:

    1 .最重要的,我们希望q*b+r==a ,因为这符合除法的定义。
    2. 如果改变a的正负号,我们希望这会改变商q的符号,但不会改变q的绝对值。
    3. 当b>0时,我们希望保证 0<= r < b。

    这三条性质是我们期望整数除法所应该具备的。很遗憾,它们不可能同时成立。
    因此,C语言或其他程序语言在实现整数除法运算是,必须放弃上述三条原则中的至少一条。大多数程序语言选择放弃了第3条,而改为要求余数与被除数的正负号相同,这样就可以同时满足性质1和性质2。大多数C编译器也是这么做的。
 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息