读C陷阱和缺陷(C Traps and Pitfalls)(一)

近日,抽空读了一下C陷阱和缺陷(C traps and pitfalls)。大家都说这本书好,总结了好多前人的经验。

自己看了以后,发现并不是这么神。20多年前作者Andrew Koenig在写此书时,ANSI C尚未定型,而今天C已发展的如此成熟,更何况一系列面向对象语言的兴起。作者书中举的许多陷阱,一位成熟的C/C++程序员都早已碰到过,都会注意到,而且很多书中所述的问题,现代的编译环境都有了相应的一些帮助提示信息来HELP程序员解决之。

20多年来,本书只所以几乎没有被作者改过,我觉得一个原因是如果要更改,作者也不知道改什么好,因为今天C的发展,无论是标准,还是成熟度,亦或是编译环境,都不是20年前可以相比的,很可能将全书147页减成47页了。

当然,作为C/C++界的专家,Andrew Koenig老师还是给出我了们很我益的提示。我将本书的重点和关键点作了笔记,如下示。

1、词法陷阱

从较低的层面考察,程序是由符号(token)序列所组成的,正如一本书是由一个个单词所组成的一样。将程序分解成符号的过程,称为“词法分析”。编译器中负责将程序分解为一个个符号的部分称为“词法分析器”。他有一个简单原则:每一个符号应该包括尽可能我的字符,如/=将被视为一个字符。

C语言中,符号间的空白(空格,制表符,换行符)将被忽略。

常见的词法错误有:==和=的混用;&、|和&&、||的混用;表示字符和字符串时""、''的混用。

2、语法陷阱

任何C变量的声明都由两部分组成:类型及类似表达式的声明符。构造复杂表达式的简单规则:按照使用的方式来声明。声明符和表达式相似,故可以在声明符中任意使用括号,如:

Float f=>float (f);//...

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

float (*h)();

表示h是一个指向返回值为float的函数的指针,则

(float (*)())表示一个“指向返回值为float的函数的指针”的类型转换符。当然我们可以用typedef来进行重定义:

typedef float (*d)();//则此时d就相当于(float (*)())。关于这一部分,可以参见:
http://blog.163.com/zhoumhan_0351/blog/static/39954227201002083420616一文。

关于优先级

最高者为数据下标,函数调用操作符,结构成员选择操作符,接下来为单目操作符,再者为双目操作符(其中,算术运算符最高,移位运算符次之,关系运算符再次之,接着是逻辑运算符,赋值运算符),再下来是三目的条件运算符,再是赋值运算符,最后是逗号运算符。

记住两点:关系运算符优先级比逻辑运算符高;位移运算符比算术运算符低,但是比关系运算符高。关于优先级的具体,参见:

http://blog.163.com/zhoumhan_0351/blog/static/39954227200972425425488

可以通过引入括号来解决不清楚优先级的问题。

如果声明后忘记分号,也会引来问题,如:

struct log

{

//...

}

main()

{

return 1;

}

编译器会把结构体当作main函数的返回值。

此外,还有switch中忘记break,及else悬挂等,都要留心。

3、语义陷阱

1)int calendar[12][31]; //定义二维数据,是数组的数组,则calendar是一个指向数据的指针。所以如下所表达是不对的:

int *p;//p是一个指向整形的指针。

p=calendar;

如下表达是正确的:

int (*monthp)[31];//monthp是一个指向一个有31个数据元素的数组的指针。

monthp=calendar;

这样,monthp就指向了calendar的第一个元素,也就是calendar数组12有着31个数据元素的数组类型元素之一。

int calendar[12][31];

int (*monthp)[31];

for(monthp=calendar;monthp<(calendar+12);monthp++)

{

int *dayp;

for(dayp=*monthp;dayp<((*monthp)+31);dayp++)

 *dayp=0;

}

2)以前我们定义malloc的转换时,一般用:

int *p;

p=(int*)malloc(sizeof(int));

还可以有另外一种表达:

int *malloc();

int *p;

p=malloc();

当然,我还是推荐用第一种。

3)编译器保证由0转换而来的指针不等于任何有效的指针,常数0常用NULL替代。当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存的内容。如如下表达

if(strcmp(p,(char*)0)==0)是非法的。而

if(p==(char*)0)则是正确的。

4)边界不对称原则

   如下代码,有可能进入死循环,如果编译器按照内存地址递减的方式来给变量分配内存。

int i,a[10];

for(int i=1;i<=10;i++)

 a[i]=0;

处理这种问题,有两个原则:

(1)考虑最简单情况下的特例,将结果处推。仔细计算边界。

(2)用第一个入界点和第一个出界点来表示范围。注意:下界是“入界点”,包含取值范围,而上界是“出界点”,不包括取值范围中,即我们所说的不对称边界原则。如在上面的表达中,i不取值范围是[0,10),所以在表达时写成如下是最好的:

int i,a[10];

for(int i=1;i<10;i++)

 a[i]=0;

   而不是写成如下的:

for(int i=1;i<=9;i++)

 a[i]=0;

还有一个需要注意的是:

   虽然对数组a[10]操作a[10]元素是不正常的,但是,取地址是可以的,如&a[10]。ANSI C允许这种用法:数组中实际不存在的“溢界”元素的地址位于数组所占内存之后。

5)算法运算

有符号运算与无符号运算两类。无符号数没有溢出一说。两个数中有一个数为无符号数时,另一个数也会被转成无符号数。当两个数都是有符号数,则可能发生溢出,作出任何假设都是不安全的。一种解决方法是将有符号数转成无符号数。

发布了208 篇原创文章 · 获赞 30 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/hopegrace/article/details/104168381