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

最近由于工做须要开始从新拾起C语言,虽说基本语法什么的没有太大问题(不行就网上搜索),但复习巩固下C语言也是不错的。正好身边有《C缺陷与陷阱》这本书,因而就有了这篇读书笔记。数组

第一章 语法“陷阱”

这一章没有太多“干货”,惟一比较有趣的就是 1.3 语法分析中的“贪心法” 所讲内容。这个“贪心”就是编译器会读入字符,若是能新读入的字符和以前所读入字符能组成符号,则编译器会继续读入下一个字符,直到读入的字符不能和以前的字符组成符号。
好比,函数

/* a---b和(a--)-b等价 */
/* a+++++b和((a++)++)+b等价 */

第二章 语法“陷阱”

这一章一上来就讲了一个函数指针的例子,第一遍看的时候我还真没看懂,直到后来看了第二遍、第三遍以后才明白了是什么意思。在这个函数指针以后该章节给出了一些简单的语法错误例子。
这里我跳过函数指针,从后面例子开始,而后最后回到函数指针上来。操作系统

2.2 运算符的优先级问题

运算符优先级虽然简单,但常常会有bug就是因为它而产生。虽然咱们能够经过添加括号来解决优先级问题,但记住一些优先级也是有帮助的。好比最高的是括号,数组下标,->, .等非真正意义上的运算符。其次是单目运算符,好比!, ~, *, &, (type)等。这个以后就是/, *, %, +, -等算数运算符。算数运算符以后就有移位,关系,逻辑,赋值等运算符。通常说来,咱们记住:指针

单目比双目运算符优先级高,算数运算符比其余双目运算符优先级高就好了。code

这章节有个例子仍是比较有表明性,内存

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,也颇有可能犯一样的错误。get

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;

除了上面这些易错点,在使用宏定义的时候,尤为须要注意++以及--的状况。当遇到++/--的时候,宏定义出错的几率会高不少。

第七章 可移植性缺陷

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

相关文章
相关标签/搜索