[原创]《C陷阱与缺陷》笔记

时间:2006-09-11 15:22:11  来源:快乐IT  作者:Eric
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  位网友发表了评论 此处只显示部分留言 点击查看完整评论页面