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

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

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

第七章 可移植性缺陷

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容

  • 指针是C语言中广泛使用的一种数据类型。 运用指针编程是C语言最主要的风格之一。利用指针变量可以表示各种数据结构; ...
    朱森阅读 3,440评论 3 44
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,516评论 1 51
  • 假如有一天我消失在雨夜, 那是因为, 雨中有你的眼泪。 假如有一天我在风中迷失了方向, 那是因为, 风中有你的翅膀...
    捷悟阅读 283评论 0 3
  • 回家的时候 ,我站在街口,用眼睛把整条街拍进眼底,放进了心里。想起自己自始至终,每次走过这条街时,都是幸福的。——...
    八月晴阅读 651评论 0 3
  • 十年 (文/亦浓) 是早晨的百合花对露珠的眷念 是雨后的阳光对彩虹的依恋 或者 是夜空里璀璨的烟花么 是孩童放飞的...
    开在夜里的花儿阅读 176评论 17 16