By Eric Wei http://www.myfavor.org/ 第1章 语法“陷阱” 符号之间的空白(包括空格,制表符,换行符)将被忽略,因此 if (x > big) big = x; 等价于 if ( x > big ) big = x ; 1.1 =不同于== 赋值运算相对比较运算出现得更频繁,因此字符数较少的符号=就被赋予了更常用的含义:赋值操作。 错误: while (c = ' ' || c == '\t' || c == '\n') c = getc(f); 1.2 &和|不同于&&和|| 1.3 词法分析中的“贪心法” 当C编译器读入一个字符'/'后又跟了一个字符'*',那么编译器就必须做出判断:是将其作为两个分别的符号对待,还是合起来作为一个符号对待。 原则:每个符号应该包含尽可能多的字符。 除了字符串与字符常量,符号的中间不能嵌有空白: == 是单个符号,而 = = 则是两个符号。 a---b 与 a-- - b 是相同的, 与 a - --b是不同的。 y = x/*p /* p指向除数 */; 编译器会将/*解释为注释的开始,正确的写法是: y = x / *p /* p指向除数 */; 或更清楚地: y = x/(*p) /* p指向除数 */; 老版本的C语言中允许使用=+来代表现在的+=的含义。这种老版本的C编译器会将 a=-1; 理解为 a =- 1; 亦即: a = a - 1; 如果程序员本意是 a = -1; 那将会出错。 老版本的编译器会将 a=/*b; 当作 a =/ *b; (贪心法则:从左到右尽量确定最长的符号) 而现在的编译器会将/*视为注释开始,因为=/不再使用。 1.4 整形常量 如果一个整形常量第一个字符是0,那么视为八进制数。 10与010是不同的。 例:不要误将十进制数写成了八进制数: struct { int part_number; char *description; } parttab[] = { 046, "left-handed widget", 047, "right-handed widget", 125, "frammis" }; 1.5 字符与字符串 用单引号引起的一个字符实际上代表一个整数。 用双引号引起的字符,代表一个指向无名数组起始字符的指针。 练习1-1 编程判断编译器是否支持嵌套注释 代码 允许嵌套 不允许嵌套 /*/**/" /*" " /*/**/"*/" " "*/" /*/**/"*/"/*"/**/ "/*" "*/" /*/*/0*/ /* 0*/ /*/*/0*/**/1 1 0*1 练习1-3 n-->0 将解释为 n-- > 0 练习1-4 a+++++b 解释为 a++ + ++b 不能解释为 ((a++)++)+b 因为(a++)不能作为左值 第2章 语法“陷阱” 2.1 理解函数声明 (* ((void)(*)()) 0) () 将数值0 强制转换类型为:函数指针 然后调用该指针所指的函数 更清楚的表达为: typedef (void)(*funcptr)(); (*(funcptr)0)(); signal函数接受一个用户自定义函数的指针,其返回值为用户自定义函数的指针。 用户自定义函数指针: void (*sfp) (int); signal函数: void (*signal(int, void(*)(int))) (int); 用typedef简化: typedef void (*HANDLER) (int); HANDLER signal(int, HANDLER); 2.2 运算符的优先级问题 if (flags & FLAG != 0) ... 相当于 if (flags & (FLAG != 0)) ... r = hi<<4 + low; 相当于 r = hi << (4 + low); a.b.c 相当于 (a.b).c 而不是 a.(b.c) *p() 相当于 *(p()) 而不是 (*p)() *p++ 相当于 *(p++) 而不是 (*p)++ a < b == c < d 相当于 (a<b) == (c<d) 下面的语句错误: while (c=getc(in) != EOF) putc(c, out); 应为: while ((c=getc(in)) != EOF) putc(c, out); 2.3 注意作为语句结束标志的分号 struct logrec{ int date; int time; int code; } main () { ... } 定义struct结尾少了个";",结果main的返回值被定义为struct logrec 2.4 switch语句 注意break; case '\n': linecount++; /* no break here */ case '\t': case ' ': ... 2.5 函数调用 f(); 而 f; 则是计算函数f的地址,然后什么也不做。 2.6 “悬挂”else引发的问题 问题语句: if (x == 0) if (y == 0) error(); else { z = x + y; f(&z); } 第3章 语义“陷阱” 3.1 指针与数组 int a[3]; 除了a被用作运算符sizeof的参数这一情形,在其他所有的情形中数组名a都代表指向数组a中下标为0的元素的指针。 sizeof(a)返回的是整个数组的大小,而不是指针的大小。 *a = 84; //将a[0]赋值为84 a[ i ] 同 a + i 同 i + a 同 i[a] 3.2 非数组的指针 在C语言中,字符串常量代表了一块包含字符串中所有字符以及一个空字符'\0'的内存区域的地址。 问题语句: char *r; r = malloc(strlen(s) + strlen(t)); strcpy(r, s); strcat(r, t); 正确: char *r; r = malloc(strlen(s) + strlen(t) + 1); if (r) { complain(); exit(1); } strcpy(r, s); //由此看出: strcat(r, t); //strcpy, strcat都会自动处理末尾的'\0' free(r); 3.3 作为参数的数组声明 int strlen(char s[]) { ... { 与下面是完全相同的: int strlen(char* s) { ... } 但是,不要将以下两种方式混用: extern char *hello; extern char hello[]; 虽然两种写法在技术上讲是通用的,但是很容易引起误导。 区别:例如, 0x100 *hello -----> 0x200 "This is a test line" 指针hello位于地址0x100处,其中存放的是实际字符串的地址0x200(字符串存储于0x200处) 0x200 (hello[]) "This is a test line" 而hello[]中的hello,实际上就是字符串的首地址(0x200) 3.4 避免“举隅法” 区分指针与指针指向的数据 复制指针并不会复制指针指向的内容 3.5 空指针并非空字符串 当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容: if (p == (char*) 0) ... //完全合法 但是: if (strcmp(p, (char*)0) == 0) ... //错误 因为strcmp会去读0地址处的内存内容 3.6 边界计算与不对称边界 原则: (1)首先考虑最简单情况下的特例,然后将得到的结果外推; (2)仔细计算边界,决不掉以轻心 void bufwrite(char *p, int n) { while (--n >= 0) { if (bufptr == &buffer[N]) flushbuf(); *bufptr++ = *p++; } } void bufwrite(char *p, int n) { while (n > 0) { int k, rem; if (bufptr == buffer[N]) flushbuf(); rem = N - (bufptr - buffer); k = n > rem ? rem : n; memcpy(bufptr, p, k); bufptr += k; p += k; n -= k; } } 3.7 求值顺序 i = 0; while (i < n) y[ i ] = x[i++]; i = 0; while (i < n) y[i++] = x[ i ]; 以上两种方法都会出现问题,因为在不同的编译器里,不能保证i的值的变化是在复制之前,还是之后。 应写为: i=0; while (i < n) { y[ i ] = x[ i ]; i++; } 3.8 运算符&& || ! 3.9 整数溢出 判断整数是否溢出: if (a + b < 0) complain(); 有问题,因为溢出之后的结果完全无法预料的,各种机器的实现不同。 应该为: if ((unsigned)a + (unsigned)b > INT_MAX) complain(); 或者以下也可行: if (a > INT_MAX - b) complain(); 3.10 为函数main提供返回值 大多数C语言实现都通过函数main的返回值来告知操作系统该函数的执行是成功还是失败。 一般返回0表是成功,非0表是失败。 第4章 连接 4.1 连接器 连接器通常把目标模块看成是由一组外部对象组成。 程序中的每个函数和每个外部变量,如果没有被声明为static,就都是一个外部对象。 每个外部对象代表着机器内存中的某个部分。 在多个目标模块整合成一个载入模块时,这些目标模块可能就包含了同名的外部对象。 连接器的一个重要工作就是处理这类命名冲突。 4.2 声明与定义 如果出现 extern int a; 则在程序的另一个地方一定会有: int a; 4.3 命名冲突与static修饰符 如果一个函数只在本源文件中使用,就应该将他声明为static 4.4 形参实参与返回值 错误程序: #include <stdio.h> main() { int i; char c; for (i=0; i<5; i++) { scanf("%d", &c); printf("%d ", i); } printf("\n"); } c为char型,但是以%d(整数)读入,内存中会出错 4.5 检查外部类型 char filename[] = "abc"; 与 extern char * filename; 是错误的,它们是不同的。 要保证类型严格一致。 C语言中的规则是,如果一个未声明的标识符后跟一个括号,那么他将被视为一个返回整型的函数。 第5章 库函数 5.1 返回整数的getchar函数 问题语句: #include <stdio.h> main() { char c; while ((c=getchar()) != EOF) putchar(c); } c为char型,而EOF的定义为任意,有可能c容不下EOF c为int,则正确 5.2 更新顺序文件 问题语句: FILE *fp; struct record rec; ... fp = fopen(file, "r+"); while (fread((char*)&rec, sizeof(rec), 1, fp) == 1) { /* processing rec */ if (/* rec must be written back */) { fseek(fp, -(long)sizeof(rec), 1); fwrite((char*)&rec, sizeof(rec), 1, fp); } } 正确: ... while (fread((char*)&rec, sizeof(rec), 1, fp) == 1) { /* processing rec */ if (/* rec must be written back */) { fseek(fp, -(long)sizeof(rec), 1); fwrite((char*)&rec, sizeof(rec), 1, fp); fseek(fp, 0L, 1); } } 因为fread和fwrite不能连在一起使用。 第2个fseek看上去什么也没做,但是它却改变了文件的状态,使得文件能够重新被正确地读取 5.3 缓冲输出与内存分配 问题语句: #include <stdio.h> main () { int c; char buf[BUFSIZ]; setbuf(stdout, buf); while ((c=getchar()) != EOF) putchar(c); } 由于在main结束前buf已经释放掉,所以输出还未来得及显示。 修改:改为: static char buf[BUFSIZ]; 或定义为全局变量。 5.4 使用errno检测错误 问题语句1: /* 调用库函数 */ if (errno) /* 处理错误 */ 运行成功时,errno不一定为0 问题语句2: errno = 0; /* 调用库函数 */ if (errno) /* 处理错误 */ 因为库函数中有可能调用其它库函数, 结果,有可能导致本库函数没错,但是errno仍然被修改。 正确: /* 调用库函数 */ if (返回的值) 检查 errno 5.5 库函数signal 要使得signal 所使用的信号处理函数尽量简单 第6章 预处理器 6.1 不能忽视宏定义中的空格 错误: #define f (x) ((x)-1) 正确: #define f(x) ((x)-1) 但是,使用时,f(2) 与 f (2) 是相同的 6.2 宏并不是函数 注意宏定义中的括号 #define max(a,b) ((a)>(b)?(a):(b)) #define toupper(c) \\ (((c) >= 'a' && (c) <= 'z') ? ((c) + ('A' - 'a')) : (c)) 6.3 宏并不是语句 考虑assert(e)的定义 当e为真时,什么也不做。当e为假时终止程序,并给出错误信息。 问题语句: #define assert(e) if (!e) assert_error(__FILE__, __LINE__) 在if嵌套展开时会出现问题 正确: #define assert(e) \\ ((void) ((e) || _assert_error(__FILE__, __LINE__))) 6.4 宏并不是类型定义 #define T1 struct foo * typedef struct foo * T2; T1 a, b, c; // a是结构指针,而b,c都是结构体 T2 a, b, c; // a,b,c均为结构指针 第7章 可移植性缺陷 (略) |
文章评论
共有 0 位网友发表了评论 此处只显示部分留言 点击查看完整评论页面