这本书超薄,但含金量很高,里边一些常见的陷阱也是面试的时留的坑,适合没事翻一翻,指不定什么时候还有意外惊喜。

词法陷阱

词法分析中的『贪心法』
编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。
比如:y = x/*py = x / *p一个将/* 理解为注释的开始,一个理解为指针。

双引号与单引号
也就是字符与字符串的区别,在C语言里,单引号引起的一个字符实际上代表的是一个张数,与ASCII严格对应。而双引号引起的字符串,代表的是一个指向无名数组起始字符的指针,该数组被双引号之间的字符以及一个额外的二进制值为0的字符\0 初始化。

语法陷阱

(*(void(*)())0)()所代表的含义
实际上一层一层看即可,void(*)()是一个函数指针,该函数返回void型数据
`(void()())0*就是将常数0转换为指向返回值为void的函数的指针 而((void()())0)();就是一个表达式,调用0`地址的函数

而它的等价表达式为:

1
2
typedef void (*funcptr)();
(*(funcptr)0)();

运算符优先级问题
这类问题例子很多,比如 if (A & B != 0) 其中的&优先级更低,所以先执行 非等判断,再进行与操作。所以解决这类问题的方式,就是在不知道优先级的情况下,加上括号就好了。记住几点:

几个小陷阱注意

语义陷阱

非数组的指针
下面这个方法想要合并s, t两个字符串,变成一个单字符串r,但是这种方法是行不通的,因为不能确定r指向了何处。不仅要让r 指向一个地址,还应该有内存空间可供容纳字符串。

1
2
3
char *r;
strcpy(r, s);
strcat(r, t);

在这里引入一个库函数malloc 实现这个操作。

1
2
3
4
char *r, *malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r, s);
strcat(r, t);

但是这样仍然是不对的,原因有三个。

1
2
3
4
5
6
7
8
9
10
char *r, *malloc();
r = malloc(strlen(s) + strlen(t) + 1);
if(!r){
complain();
exit(1);
}
strcpy(r, s);
strcat(r, t);
//一段时间以后
free(r);

(所以说,c简直就是个坑,入大Python教)

空指针
编译器保证由0 转化而来的指针不等于任何有效的指针,通常被null 这个值代替,所以任何企图使用该指针所指向的内存中存储的内容的行为都是非法的。

求值顺序
C语言中只有四个运算符(&& , || , ?: , ,)存在规定的求值顺序.运算符&&和运算符||首先对左侧操作数求值,只在需要时才对右侧操作数求值.运算符?:有3个操作数:在a?b:c中,操作数a首先被求值,根据a的值再求操作数bc的值.而逗号运算符,首先对左侧操作数求值,然后该值被”丢弃”,再对右侧操作数求值.其他所有运算符对其操作数求值的顺序都是未定义的.特别的,赋值运算符并不保证任何求值顺序.

连接

这部分是C语言的特性,所以就直接粗略的看了一下跳过去。
static 修饰符与命名冲突:他是一个能有效减少命名冲突的方式,static int a; 之后,a 的作用域限制在了一个源文件里,其他文件是不可见的。static 同样适用于函数。

库函数

作者刁刁的,写这本书的时候,ANSI C 标准还没有最后定案。所以书中考虑了很多现在不需要考虑的问题,比如兼容各种风格,兼容K&R 风格的函数定义形式。
getchar()函数
getchar()函数读取的字符,如何直接赋值给一个char型的 c ,就会出现问题。因为c无法容下所有可能的字符,特别是,可能无法容下读文件结束时候的EOF,这样就会出现问题,如果getchar()读入的字符无法存入char c中去,那么就会『截断』,结果可能正确,但是这样做是不对的。

更新文件顺序
一个输入操作不能随后直接紧跟一个输出操作,输入fread和输出fwrite同时操作,需在其中插入fseek函数的调用,之后文件就可以正常的读取或写入了。

缓冲输出setbuf(stdout, buf) 将写入刀stdout 的输出都使用buf 作为输出缓冲区,知道buf 缓冲区已满,或者是直接调用 fflush buf 缓冲区的内容才会实际写入stdout中去。

errno 检测错误: 在库函数调用没有失败的情况下,并没有强制要求库函数一定要设置errno 为0,也可能是上一个执行失败的库函数设置的值。甚至在函数看似正确执行的情况下,也会将errno 设置一个值,所以用 if(errno) 来处理这个错误的方式是不可以的,我们可以用 if (返回错误的值) 检查 errno 这种方式,也就是我们检测作为错误指示的返回值,确定程序执行已经失败,再检查errno,来搞清楚错误原因。

signal :信号这个函数非常的复杂,因为在其他的库函数中,可能已经有信号在执行过程中,我们再使用signal处理函数,极有可能导致数据完全崩溃。所以,信号非常的复杂和棘手,具有一些从本质上不可移植的特性,所以要让signal处理函数尽可能的简单。

预处理器

C语言里的宏,和C++中的泛型模板,在精神上有相仿之处,也有人用C 的宏实现通用容器。众多C++书籍都告诉我们,宏是万恶之首,所以没达到得心应手的时候,所以这一块,当做了解来看。

不要忽视宏定义中的空格:注意这里说的是宏定义。一个宏如果不带参数,则只需要调用宏名即可,括号无关紧要,所以宏定义后边的空格可能就会引起下面的麻烦。
#define f (x) ((x)-1)
f代表(x) ((x)-1),而不是f(x)代表((x)-1),后者应写为
#define f(x) ((x)-1)
这一规则不适用于宏调用,只对宏定义适用.因此上面完成宏定义后,f(3)与f (3)求值后都等于2.

宏不是函数:最好在宏定义中把每个参数都用括号括起来,整个结果表达式也用括号括起来,防止当宏用于一个更大一些的表达式中可能出现的问题。
宏不是语句:assert(e)的正确定义

1
2
#define assert(e) /
((void)((e) || _assert_error(__FILE__,__LINE__)))

这个定义实际上利用了||运算符对两侧的操作数依次顺序求值的性质.
宏不是类型定义
宏的一个常见用途是,让多个不同变量的类型可以在一个地方说明:

1
2
3
#define FOOTYPE struct foo
FOOTYPE a;
FOOTYPE b,c;

这样,我们只需要在程序中改动一行代码,就可以改变a, b, c 的类型了,而与a, b, c 在程序中的什么地方声明无关。这与typedef 的方式相同,但是后者更通用一些。而宏的方式某些时候,可能存在一些问题。

可移植性缺陷

作者大牛在这里超前讨论了C语言在移植的时候小心的一些坑和缺陷。

建议

最后作者提出了几个建议,说的挺有意思。比如作者推荐我们在编程的时候,一定要尽可能的清晰表达意图,该用括号的时候,都尽量用括号,以防不该有的错误出现。同时要考察一些最简单的特例,测试程序的工作情况。一定要注意一些边界问题,比如数组的下标。以及一些潜伏在暗处的Bug。

最后作者提出了防御性编程,其实就是在说,啊,C语言有很多很多的坑,我们不知道什么时候就会踩到了坑,所以再变成的时候,一定要采用最稳妥的方式编程,意思就是一定要走那些被证明是康庄大道的平坦的道路呀,少年,不然你就要被坑惨了~~

以上。

script>