美文网首页C语言今日看点程序员
《C缺陷与陷阱》读书笔记

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

作者: RdouTyping | 来源:发表于2016-12-18 14:54 被阅读402次

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

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

    第七章 可移植性缺陷

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

    相关文章

      网友评论

        本文标题:《C缺陷与陷阱》读书笔记

        本文链接:https://www.haomeiwen.com/subject/oeaamttx.html