《C陷阱与缺陷》里面介绍了一些自己不知道和以前理解不深的东东,现总结如下:
1.词法分析的陷阱(本书第9页)
y = x/*p; /* p指向除数 */
上述语句的本意是:用x除以指针p所指向的值,然后把商赋给y;但是/*被编译器理解为一段注释的开始,编译器将不断地读入字符,直到*/出现为止。也就是说该语句实际的执行效果只是将x的值赋给y而已;
可以将上面的语句重写成如下格式:
y = x / *p /* p指向除数 */
【备注】:我们的项目组中,明确规定在运算符与变量之间必须添加空格,就是为了避免上面的错误;
2.运算符优先级(本书第22页)
关于运算符优先级,我们需要记住两点:
I.任何一个逻辑运算符的优先级低于任何一个关系运算符;
II.移位运算符的优先级比算术运算符要低,但是比关系运算符要高;
3.数组的边界计算与不对称边界(本书第45页)
int i, a[10]; for (i=1; i<=10; i++) a[i] = 0;
上述代码的错误非常明显,数组a中并不存在a[10]这个元素,而在for循环中却引用了这个变量。如果用来编译这段程序的编译器按照内存地址递减的方式来给变量分配内存,那么内存中数组a之后的一个字(word)实际上是分配给了整型变量i。此时,本来循环计数器i的值是10,循环体内将并不存在的a[10]设置为0,实际上却是将计数器i的值设置为0,这就是陷入了一个死循环(本人使用gcc测试了一下,运行该程序确实为以死循环)。这是C语言将数组的下表定义为从0开始的引起的一个弊端。
但是C语言数组的这种不对称边界会对程序设计的简化带来许多方便之处:
I.数组的长度就是上届与下届之差。
II.如果数组的取值范围为空,那么上界等于下界;
III.即使数组的取值范围为空,上界也永远不可能小于下界;
这种不对称边界的思考方式使用,是把上界视作某序列中第一个被占用的元素,而把下界视作序列中第一个被释放的元素。当处理各种不同的类型的缓冲区时,这种看待问题的方式就特别有用。例如,考虑这样一个函数,该函数的功能是将长度无规律的输入数据送到缓冲区(即一块能够容纳N个字符的内存)中去,每当这块内存被“填满”时,就将缓冲区的内容写出。因此,该函数就可以这样实现:
#define N 1024 static char buffer[N]; static char *bufptr; /* 指向缓冲区的当前位置 */ void bufwrite (char *p, int n) { bufptr = &buffer[0]; while (--n >= 0) { if (bufptr == &buffer[N]) flushbuffer(); /* 清空缓冲区 */ *bufptr++ = *p++; } }
虽然不存在buffer[N]这个元素,但是我们并不需要引用这个元素,而只需要引用这个元素的地址,并且这个地址在我们遇到的所有C语言实现中又是“千真万确”存在的。而且ANSI C标准明确允许这种做法:数组中实际不存在的“溢界”元素的地址卫浴数组所占内存之后,这个地址可以用于进行赋值和比较。当然,如果要引用该元素,那就是非法的了。
4.数组和指针的区别(本书第36页)
int a[10]; int *p = a;
上述两个声明,sizeof(a) =10*sizeof(int),而sizeof(p)=4。
5.关于scanf的参数越界(本书第76页)
#include <stdio.h> int main () { int i; char c; for (i=0; i<5; i++) { scranf("%d", &c); printf("%d ", i); } printf("\n"); return 0; }
上段代码问题关键在于,这里c被声明为char类型,而不是int类型。当scanf读入一个整数,应该传递给它一个指向整数的指针。而程序中scanf函数得到的却是一个指向字符的指针,scanf函数并不能分辨这种情况,它只是将这个指向字符的指针作为指向整数的指针而接受,并且在指针指向的位置存储一个整数。因为整数所占的存储空间要大于字符所占的存储空间,所以c附近的内存将被覆盖。c附近的内存中存储的内容是由编译器决定的,本例中它存放的是整数i的低端部分。因此,每次读入一个数值到c时,都会将i的低端部分覆盖为0,而i的高端部分本来就是0,相当于i每次被重新设置为0,循环将一直进行下去。
5.文件的读写顺序(本书第85页)
如果要同时进行输入和输出操作,必须在其中插入fseek函数的调用。如fwrite之后如想立即fread,则必须在fread之前fseek一下,及时fseek只是将指针偏移了0个字节。如下所示的代码:
while (fread((char *)&rec, sizeof(rec), 1, fp) == 1) { /* 对rec执行某些操作 */ if ( /* rec必须被重新写入 */ ) { fseek(fp, -(long)sizeof(rec), 1); fwrite( (char *)&rec, sizeof(rec), 1, fp); fseek(fp, 0L, 1); } } /* 第二个fseek函数看上去什么也没做,但它改变了文件的状态,使得文件现在可以正常地进行读取了 */
6.尽量不要使用宏来定义新的数据类型(本书第101页)
#define T1 struct foo * typedef struct foo *T2; T1 a, b; T2 a,b;
考虑以上代码,第一个声明被扩展为:struct foo *a, b; 在这个语句中a被定义为一个指向结构的指针,而b却被定义为一个结构(而不是指针)。而第二个声明,则将a和b都定义为了指向结构的指针,因为第二种T2的行为完全与一个真实的类型相同。