《C陷阱与缺陷》 阅读总结

这本短小但却经典的书籍,确实指出了C语言编程中的不少值得关注的陷阱。在此只是将一些常见的缺陷列举出来,并对一些容易混淆、遗忘的知识点做一个记录。在以后的程序员生涯中,确实有必要再次阅读该书。

第一章 词法“陷阱”

1.1 =不同于==

注意不要=和==混淆使用,特别要注意“笔误”的发生。

1.2 & 和 | 不同于 && 和 ||
1.3 词法分析中的“贪心法”

编译器将程序分解为符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,并判断这两个字符能否组成一个有效的符号,如果可以,继续读入下一个字符并重复该判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。

1.4 整型常量

注意若一个常量的首字符是0,该常量会被视为8进制数。

1.5 字符与字符串

注意:’ ‘表示一个字符,而“ ”则表示字符串。前者是一个整数,而后者却是一个指向无名数组起始字符的指针。

第二章 语法“陷阱”

2.1 理解函数声明

typedef简化函数的声明:
声明signal函数:

void (*signal(int,void(*)(int)))(int);

使用typedef简化函数声明:

typedef void (*HANDLER)(int);
HANDLER signal(int,HANDLER);

typedef定义了HANDLER,其是一个返回void型的函数指针,且该函数指针的参数为int型。

2.2 运算符的优先级问题

运算符的优先级可以利用口诀进行记忆,即“单算移关与,异或逻条赋”
■“单”表示单目运算符:逻辑非(!),按位取反(~),自增(++),自减(–),取地址(&),取值(*);
■“算”表示算术运算符:乘、除和求余(*,/,%)级别高于加减(+,-);
■“移”表示按位左移(<<)和位右移(>>);
■“关”表示关系运算符:大小关系(>,>=,<,<=)级别高于相等不相等关系(==,!=);
■“与”表示按位与(&);
■“异”表示按位异或(^);
■“或”表示按位或(|);
■“逻”表示逻辑运算符:逻辑与(&&)级别高于逻辑或(||);
■“条”表示条件运算符(? :);
■“赋”表示赋值运算符(=,+=,-=,*=,/=,%=,>>=,<<=,&=,^=, |=,!=);
◆逗号运算符(,) 级别最低,口诀中没有表述
◆(),[],->这些其实不算运算符,级别最高

注意:单目运算符和条件运算符、赋值运算符采用自右至左的结合方式,
其余运算符采用自左至右的结合方式。

2.3 注意作为语句结束标志的分号

注意在条件判断语句后,不能多写一个分号,这样会造成逻辑处理上的bug。
注意在返回void型的函数中的return语句后不能少写一个分号,这样可能造成返回值错误。
注意在声明结构体、类等数据类型时,不要少写分号。

2.4 switch语句

switch中的case语句结束时不要忘记break语句,确实不需要break语句的时候应明确标注,方便代码维护。

2.5 函数调用
2.6 “悬挂”else引发的问题

注意else总是和最接近的if进行结合的特性。
编程规范,应对任何一个if及else都加{}处理。

第三章 语义“陷阱”

3.1 指针与数组

1.C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。C语言中数组的元素可以是任何类型的对象。
2.对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针。其他有关数组的操作,哪怕它们乍看上去是以数组下标进行的操作,其实都是通过指针进行的。

3.2 非数组的指针

非数组的指针

char *r,*s,*t;
r=malloc(strlen(s)+strlen(t));
strcpy(r,s);
strcat(r,t);

这段代码有三处错误:
1. malloc之后未检查返回值是否为NULL ;
2. 注意sizeof与strlen的区别,strlen计算字符串所包括的字符数目,不包含结尾的’\0’字符,所以分配内存时应该是strlen(s)+1;
3. 分配完的内存应及时释放,避免发生内存泄漏。

3.3 作为参数的数组声明

C语言中会自动将形参中作为参数的数组声明转化为对应的指针声明。

3.4 避免“举隅法”

注意:复制指针并不同时复制指针指向的数据。

3.5 空指针并非空字符串

编程时应注意空指针和空字符串的区别。

3.6 边界计算与不对称边界

注意数组的边界声明,比较好的方法是:用第一个入界点和第一个出界点来表示一个数值范围。入界点要包含在取值范围之内,而出界点不包含在取值范围内。

在循环中利用操作数组元素非常容易出现越界问题,需要特别注意。

3.7 求值顺序

在C语言中,定义了||、&&、?:、, 的求值顺序。

C语言规定,必须首先对左侧操作数求值,然后根据需要对右侧操作数求值;
对于逻辑操作符,根据需要利用了短路性质,即:
A&&B, 当A为false时,不去计算B的值而直接返回false;当A为true时,计算B的值。
A || B,当A为true时, 不去计算B的值而直接返回true;当A为false时,计算B的值。
比如:

if(count!=0 && (a/count)>2)
{
}

先计算左侧的表达式判断count是否等于0,count不为0才计算a/count。在这个例子中,求值顺序至关重要,否则会引发除0异常。
再比如一个例子:

char *p;
if(p!=NULL && *p)
{
}

首先判断p是否是空指针,然后判断是否是空字符串。

对于?:这个条件运算符,比如:a?b:c

操作数a先被求值,根据a的值再求操作数b或者操作数c的值,只计算其中一个值。

逗号运算符,首先对左侧操作数求值,然后该值被”丢弃”,在对右侧操作数求值。

C语言中其他所有运算符对其操作数求值的顺序是未定义的。特别指出,赋值运算符并不保证任何求值顺序。
例如:

i=0;
while(i<n)
{
    y[i++]=x[i];
}

在这个例子中,左侧操作数中的i++有可能比x[i]先执行,也有可能比x[i]后执行。

3.8 运算符&&、|| 和 !

注意逻辑与和位与、逻辑或和位或的区别。
逻辑运算有短路性质,而位运算则没有此性质。

3.9 整数溢出

在进行算术运算时,应考虑整数溢出的影响。如果溢出对结果有影响,就应该进行判断。

3.10 为函数main提供返回值

main函数如果未声明返回值,默认返回整数值。这个返回值用来告知操作系统执行结果,返回0表示成功,返回非0表示失败。
大部分情况下这样做没有问题。但如果系统关注这个执行结果,就必须明确返回一个有意义的值。

第四章 连接

4.1 什么是连接器

连接器不懂C语言,编译器的责任是把C源程序“翻译”成对连接器有意义的形式,然后连接器把由编译器或汇编器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体,该实体能够被操作系统直接执行。

连接器通常把目标模块看成由一组外部对象组成的。每个外部对象代表着机器内存中的某个部分,并通过一个外部名词来识别。因此,程序中的每个函数和每个外部变量,如果没有被声明为static,就都是一个外部对象。

连接器读入目标模块和库文件,同时生成载入模块。对每个目标模块中的每个外部对象,连接器都要检查载入模块,看是否已有同名的外部对象。如果没有,连接器就将该外部对象添加到载入模块中;如果有,连接器就要开始处理命令冲突。

4.2 声明与定义

使用extern关键字进行声明。每个外部变量只能定义一次。

4.3 命名冲突与static修饰符

使用static关键字,将变量定义为只能在本文件中使用的变量,可以有效减少与外部同名变量之间的命名冲突。

4.4 形参、实参与返回值
4.5 检查外部类型

保证一个特定名称的所有外部定义在每个目标模块中都有相同的类型,一般来说是程序员的责任。而且,“相同类型”应该是严格意义上的相同。
如:一个文件中的

char filename[] ="/etc/passwd";

与另一个文件中的

extern char* filename;

类型就不同。

4.6 头文件

第五章 库函数

5.1 返回整数的getchar函数
5.2 更新顺序文件

C语言中调用fread和fwrite函数交替对文件进行操作时,应该在两个函数调用之间调用一次fseek。

5.3 缓冲输出与内纯分配
int main()
{
    char buf[512];
    setbuf(stdout,buf);
    while((c=getchar())!=EOF)
        putchar(c);
}

以上代码是错误的。
原因在于,程序调用库函数setbuf,通知输入/输出的数据首先缓存在buf中,main函数返回时,buf被释放,控制权交给操作系统之前C运行时库要进行清理工作,会引用已经被释放的buf。
解决办法:1.将缓冲数组成为静态数组;2.把buf声明完全移到main函数之外。

5.4 使用errno检测错误

在调用库函数是,我们应该首先检测作为错误指示的返回值,确定程序执行已经失败。然后,再检查errno,来搞清楚错误原因。

5.5 库函数signal

第六章 预处理器

6.1 不能忽视宏定义中的空格
6.2 宏并不是函数

C语言中的宏的本质是编译之前的文本替换。因此大多数宏定义都用括号括起来,防止在文本替换之后由于运算符优先级问题使得运算顺序与预期不一致。
尽量避免在宏定义中使用++和–操作符,或者在宏定义中改变指针的值。
宏展开可能产生非常庞大的表达式,占用的空间远远超过了编程者所期望的空间。

6.3 宏并不是语句

若assert语句被定义为如下的宏:

#define assert(e) if (!e) assert_error(_FILE_,_LINE_)

那么,在将assert应用于条件语句中时,可能会照成else悬挂。
assert实际上不是类似于一个语句,而是类似一个表达式。

#define assert(e) ((void) ((e)|| _assert_error(_FILE_,_LINE_)))
6.4 宏并不是类型定义

将宏定义与类型定义typedef区分开来

第七章 可移植性缺陷

7.1 应对C语言标准变更
7.2 标识符名称的限制
7.3 整数的大小

在不同的编译器上,整数的长度并不完全相同,可能是2个字节,也可能是4个字节,甚至可能是8个字节,移植代码的时候要注意。
一个建议就是自己在代码中定义整数的类型,这样即能统一,又方便修改。

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

把一个char型变量转化为int变量,编译器会自动进行符号位扩展。尤其当字符变量的最高位为1时,要特别注意这个问题。

unsigned int a = 0; 
int b = 0; 
int d=0; 
char c = 0x80; 
a = c; //c将首先被转换为int类型,由于存在符号位扩展,a=0xffffff80 
b = c; //c将首先被转换为int类型,由于存在符号位扩展,b=0xffffff80 
d = (unsigned char)c;//c是无符号类型,不会进行符号位扩展,d=0x00000080
7.5 移位运算符

当操作数是有符号数,>>将以符号位填充空出的位;当操作数是无符号数,>>将以0填充空出来的位。

在C语言中,移位位数必须在大于等于0小于n(移位对象的位数),否则编译器会报错。
有符号数中的 负数 向右移位运算并不等同于除以2的某次幂。对于非负数,移位运算相比除法,将大大提高运行速度。

7.6 内存位置0

null指针并不指向任何对象。因此,除非是用于赋值或比较运算,出于其他任何目的的使用null指针都是非法的。

7.7 除法运算时发生截断
7.8 随机数的大小
7.9 大小写转换
7.10 首先释放,然后重新分配

malloc函数,realloc函数与free函数的使用方法。

7.11 可移植性问题的一个例子

参考:
《C陷阱与缺陷》

猜你喜欢

转载自blog.csdn.net/hmxz2nn/article/details/80150195