《C缺陷与陷阱》读书笔记

来源:互联网 发布:学校资产管理系统源码 编辑:程序博客网 时间:2024/04/28 13:54

最近因为工作需要开始重新拾起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 * T2T2 c, d;

除了上面这些易错点,在使用宏定义的时候,尤其需要注意++以及–的情况。当遇到++/–的时候,宏定义出错的概率会高很多。

第七章 可移植性缺陷

这章主要讲了在不同编译器,不同硬件环境下程序运行结果可能会完全不同。其中包括函数命名,数据长度,默认是有符号数还是无符号数,移位运算,除法截取的不同的例子。

0 0