《C陷阱与缺陷》这本书我想很多人都看过,上周末花了两天时间断断续续的把神作看完。看这本书的心情往往是这样“靠,这个我之前就弄错过,原来是这样”,"这个简单,不就是那什么嘛",“妈的原来这样写会导致这样啊,下次看来我得注意点”。虽然书中介绍的大部分出错点都见识过,并且在多次的折磨后,现在很多已经是烂熟于心,但还是觉得这本书写的相当不错。但是并不推荐代码经验极少的人来看这本书,否则你会有点现在一堆错误里的感觉,得等你有了一定的代码经验后再来看这本书,就会有不同的感受。
书中提到的错误基本都是经典的,像那些等于和赋值的就不写了,这里就记录一些我觉得比较有意思的吧。
1.词法分析中的“贪心法”。这个我想稍微看过编译原理的理解起来就是秒杀了(要不说自己尝试写过编译器的debug能力会狂飙呢)。举例说明,当c编译器读入一个字符'/',后跟一个'*',那么编译器是怎么判断的?是把它当成两个字符还是当成一个,其实有一个简单基本原则:每个符号应该包含尽可能多的字符。编译器将程序分解成符号的方法是,从左到右一个个字符读入,如果该字符可能组成一个字符,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是是一个符号的组成部分,如果可能,继续读入下一个字符,重复上述判断,知道读入的字符组成的字符串不再可能组成一饿有意义的符号。如y = x /* p
到底是什么?a---b又什么?其实我心里想,谁让我看到这样的代码我就拿起他的键盘敲死他。
2.运算符优先级问题。其实这个问题,有点不想加进来我基本不会装逼不加括号,加上括号虽然有人觉得可能有点但往往增加了可读性,而且也没必要记那么清楚用多了就记住了。书中提到有两点需要记住:1.任何一个逻辑运算符的优先级低于任何一个关系运算符(如 x > y && a > b)。2.移位运算符的优先级比算术运算符要底,但是比关系运算符要高。
3.数组和指针。 这个必须要写,简直就是不少人头疼的问题,这里建议先看看《c专家编程》中第四章和第九章的部分。《c陷阱和缺陷》中基于下面这两个原则来分析:1.c语言中只有一维数组,且数组的大小必须要在编译期间作为一个常熟确定下来。数组中的元素可以为任何类型的对象。2.对于一个数组,我们只能做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针。理解了这两点就可以解决了。
第一点使二维数组的问题得以解决,第二点可以联想到数组作为函数参数传参,其原则是:c语言为自动将参数的数组省委转换为相应的指针声明。
4.不对称边界。即x < n 和 x <= n - 1的选择,这我想都懂,但是这是一个好的编程习惯。
5.字符的char型和int型。这个问题之前在看K&R的《c语言程序设计》中有提到,看到一篇文章介绍的不错(链接)。总之你把它声明为int就好
当有这么一段程序时:
#include <stdio.h>
int main(void) {
char c;
while((c = getchar()) != EOF) {
putchar(c);
}
return 0;
<span style="font-size:18px;">}</span>
由于,声明为了char型,有可能发生截断,则程序就可能发生死循环。如下面的程序和结果是有点出乎意料的:
6.EOF和getchar()。这有点老生长谈,但是还是的注意。下面这是摘抄的说明:
<span style="font-size:18px;">EOF is NOT:
A char
A value that exists at the end of a file
A value that could exist in the middle of a file</span>
详细的可以看看getchar和EOF总结和getchar是宏7.让人蛋疼的宏。这我其实也不想写,说实在我及其不想用宏来代替函数虽然可能会有效率上的差距,但是很容易出错。简单记录几点:
1.不能忽略宏定义中空格。如#define f (x) ((x) -1)会出其不意的被解释为#define f (x)((x) - 1).(看看编译原理就会很明白)。
2.宏的展开可能会得到庞大的表达式,这样反而导致效率的下降。如#define max(a,b) ((a) > (b) ? (a) : (b)),使用max(a,max(b,max(c,d)))时
3.总之,展开很蛋疼容易出错,简单使用即可。
8.关于大小写转换。toupper()和tolower()函数,注意是函数,不是宏,虽然它以前可能一度是宏定义的。这里涉及到了前人在效率和程序健壮性的考量和纠结,后来就提供了两个另外的供使用者选择:_toupper() 和 _tolower()这两个是宏定义,在传入参数前要进行判断,即相应的相应的就传入的大写或小写进行转换,否则返回的可能不是预期结果。
9.其他:1.初始化表中多余的逗号有助于编译器解析,想到了python中如果只有一个元素时必须在后面加逗号。2.指针乱指是允许的,但是如果想取出其中的值就是不允许的,和null有点类似,null指针并不指向任何对象,除非是赋值和比较运算,出于其他目的的操作都是非法。a++不能作为左值即不能出现((a++)++) + b(解释)
疑问:书中程序如下
<span style="font-size:18px;">#include <stdio.h>
int main(void) {
int arr[5];
int i;
for(i = 0; i <= 5; i++) {
a[i] = 0;
}
for(i = 0; i <= 5; i++) {
printf("%d\t",a[i]);
}
return 0;
}
</span>
输出结果:
<span style="font-size:18px;">0 0 0 0 0 0</span>
书中对这样的边界是这样解释的:有些编译器按照内存地址递减的方式给变量分配内存,则数组arr后的一个地址分配给了i则i变为0,程序陷入死循环,这里程序没有陷入思循环, 说明不是这样子,可是为什么却没有越界访问呢?
总结:天干物燥,小心火烛。这是本书,我只是对其中一些做总结,书中的关于库和链接我想是每一个码代码的已经非常熟悉的,关于可移植性的问题我现在也没有切身的体会,故不写,书的后面还简单介绍了两个头文件,因为以前看过c标准库也觉得没什么。有人说看完这本书后代码会变得更长,我想有些情况是这样的。此书应常备枕边,常翻常新。