最近因为工作需要开始重新拾起C语言,虽然说基本语法什么的没有太大问题(不行就网上搜索),但复习巩固下C语言也是不错的。正好身边有《C缺陷与陷阱》这本书,于是就有了这篇读书笔记。
第一章 语法“陷阱”
这一章没有太多“干货”,唯一比较有趣的就是 1.3 语法分析中的“贪心法” 所讲内容。这个“贪心”就是编译器会读入字符,如果能新读入的字符和之前所读入字符能组成符号,则编译器会继续读入下一个字符,直到读入的字符不能和之前的字符组成符号。
比如,
/* a---b和(a--)-b等价 */
/* a+++++b和((a++)++)+b等价 */
第二章 语法“陷阱”
这一章一上来就讲了一个函数指针的例子,第一遍看的时候我还真没看懂,直到后来看了第二遍、第三遍之后才明白了是什么意思。在这个函数指针之后该章节给出了一些简单的语法错误例子。
这里我跳过函数指针,从后面例子开始,然后最后回到函数指针上来。
2.2 运算符的优先级问题
运算符优先级虽然简单,但经常会有bug就是由于它而产生。虽然我们可以通过添加括号来解决优先级问题,但记住一些优先级也是有帮助的。比如最高的是括号,数组下标,->, .等非真正意义上的运算符。其次是单目运算符,比如!, ~, *, &, (type)等。这个之后就是/, *, %, +, -等算数运算符。算数运算符之后就有移位,关系,逻辑,赋值等运算符。一般说来,我们记住:
单目比双目运算符优先级高,算数运算符比其他双目运算符优先级高就行了。
这章节有个例子还是比较有代表性,
while( c = getc(in) != EOF )
putc( c,out );
由于=优先级低于!=,该例子会先比较getc(in)和EOF,然后将比较的值赋给c。显然,这并不是大家所期待的结果。我们需要给c = getc(in)加上括号才能达到我们的目的。
2.3 注意作为语句结束标志的分号
这节给了if和while语句的例子,东西不难,但是还是可能导致出错。说实话,最近一个月我就犯了这节中所讲的错误。
if( STATUS_SUCCESS != (s = foo( arg1,
arg2,
arg3)));
do something
这种例子,尤其是在args很多的时候,还真有可能忘了这是一条if语句而犯了上面这个错误。同理,如果这个if是while,也很有可能犯同样的错误。
2.1 理解函数声明
这个小节,作者给出一个有趣的函数用
(*(void (*)())0)();
当我第一次看到这个函数调用的时候,直接就懵了,完全不知道它要干啥。其实这个函数就是为了调用在地址0处的返回值为void类型的函数指针的函数。我知道这个中文解释也特别绕,下面我就一步步的分析这个语句。
第一,返回值为void类型的函数指针
void (*pfun)()
这个就是上面那个语句中的
void(*)()
而
void(*)()0
便是将0这个地址转换成void (*)()类型。如果这个不理解,这个语句该懂吧
(int *)0
对,这个例子就是将0这个地址转化成int类型。读和写这个地址都是按照32bit或者16bi进行操作(由操作系统是32bit还是16bit决定)。
第二,通过指针访问函数
一般而言,我们使用func()来调用函数,如果是使用函数指针pfun的话,我们应该这样使用
(*pfun)()
而不是
*pfun()
因为()的优先级高于*,如果是后者的话,该语句就等价于
*(pfun()) == *((*pfun)())
这并不是我们想要的结果。说了这么多,只要我们结合一和二就很容易理解这个语句是做什么的了。说实话,他这个用法也比较奇葩,因为他不是用函数的间接地址(函数名)而是用直接地址(这个例子中是0)来调用函数,因此理解起来比较费力。对于函数指针本身,我将在之后的文章中详细讲解如何使用。
第三章 语义“陷阱”
3.1 指针和数组
这节给出了C中数组两个特别需要注意的地方:
第一,C语言只有一维数组,其元素可以为任何数据类型。第二,对于一个数组,我们只知道其大小以及第0个元素的地址。
除此之外,这章还简单介绍了指针数组和指向数组的指针。对于数组和指针,我会单独写一篇文章的。
3.2 非数组的指针
字符串常量最后都会有一个"0",如果要用malloc分配一段空间然后将两个字符串常量复制到这个空间,所分配的空间要考虑最后的"0"。
如下面这个例子,s大小应该为(strlen(r) + strlen(t) + 1),因为strlen(),是取非"0"后字符串常量的长度。
/* strcpy()会复制"\0" */
strcpy(s, r);
/* strcat()会寻找s中的"\0",然后再将t复制到这个位置 */
strcat(s, t);
3.5 空指针并非空字节字符串
对于NULL指针来说,我们不能直接用该指针直接访问内存空间。文中举出一个例子,
if( strcmp( p, ( char * )NULL ) == 0 )
这个例子之所以不对是因为strcmp()会去访问NULL指向的内存空间,这是绝对要禁止的事情。
3.6 边界计算与不对称边界
这一节用了不少篇幅来说明一个很简单的问题:[a, b]中有b+1-a个元素!
3.7 求值顺序
C语言中只规定了四个运算符有明确规定的求值顺序,它们分别是&&, ||, ?:和,。所以=左右两边是没有规定求值顺序的。这节给出一个例子:
i = 0;
while( i < n )
y[ i ] = x[ i++ ];
由于没有说明到底是先算左边还是先算右边,所以可能左边用y[ i+1 ]前的结果接收了右边x[ i++ ]后的结果。当然,也可能左边用y[ i+1 ]的结果接收右边x[ i++ ]后的结果。这和编译器有关,我们应该避免这种写法。
3.9 整数溢出
这节讲了如何避免有符号数的溢出问题,比如两个有符号非负数a和b,如何判断相加是否溢出?文中给了两个方法,我准备在日后写篇如何防止溢出的文章详细讨论更多情况。
/* 方法0 错误方法 */
if( a + b < 0 )
/* 方法1 */
if( ( unsigned )a + ( unsigned )b > INT_MAX )
/* 方法2 */
if( a > INT_MAX - b )
为什么方法0不正确?因为对于有些系统,对于有符号数的溢出,它并不会在状态寄存器中标记“负”,而是会标记“溢出”。这样a+b其实就没有小于0,因此这种判断方式不正确(至少某些情况不正确)。
第四章 连接
4.2 声明与定义
4.3 名字冲突与static修饰符
全局变量在不同文件中不能多次定义,我们定义了一次以后,在其他文件中使用extern修饰符进行访问。为了避免在不同文件中定义同名的全局变量,我们应该使用static修饰符。static修饰的变量和函数的作用域仅限于其所在的。
4.4 形参,实参和返回值
为避免错误,在函数调用前应该先声明或者定义。
4.5 检查外部类型
在不同文件中定义同名的全局变量需要小心,即使类型不一样也要避免。同时,声明一个全局变量后,在其他文件中使用extern访问时候要保证类型,名字完全一样。
4.6 头文件
我们可以通过把extern修饰的变量放入头文件,只要include这个头文件的文件都可以访问这个全局变量。
第五章 库函数
这章看了下没什么意思,所以就略过了。
第六章 预处理器
预处理用得好事半功倍,用得不好bug满天。在这章,作者给出了一些比较常见的错误使用,比如用宏错误定义函数或者函数参数,用宏错误定义数据类型。
/* 多了空格 */
#define f (x) ((x) - )
/* 优先级考虑不周到,如果x = a - b结果不对*/
#define abs(x) x>=0?x:-x
/* 正确使用应该全部添加括号,包括最外面也要添加括号,这是为了避免一些比较特殊情况,比如 abs(a) + 1 */
#define abs(x) (((x)>=0)?(x):-(x))
/* 错误的在数据类型上使用宏定义 */
#define T1 struct foo *
T1 a, b;
/* 正确的方法 */
typedef struct foo * T2
T2 c, d;
除了上面这些易错点,在使用宏定义的时候,尤其需要注意++以及--的情况。当遇到++/--的时候,宏定义出错的概率会高很多。
第七章 可移植性缺陷
这章主要讲了在不同编译器,不同硬件环境下程序运行结果可能会完全不同。其中包括函数命名,数据长度,默认是有符号数还是无符号数,移位运算,除法截取的不同的例子。