[C陷阱和缺陷] 第2章 语法“陷阱”

Posted linuxandmcu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[C陷阱和缺陷] 第2章 语法“陷阱”相关的知识,希望对你有一定的参考价值。

第2章 语法陷阱


2.1 理解函数声明

当计算机启动时,硬件将调用首地址为0位置的子例程,为了模拟开机时的情形,必须设计出一个C语言,以显示调用该子例程,经过一段时间的思考,得出语句如下:

( (void() () )0 ) ();
像这样的表达式看起来很难理解,但只要将其一层一层地剥离,还是能够理解的。下面我将用几个例子来帮助大家逐渐理解这个表达式。

    void    *a();
    void    (*b) ();
因为()的优先级高于*,所以*a()为*(a()),a是一个函数,该函数的返回类型为void*。而b是一个函数指针,指向返回类型为void的函数。



一旦我们知道了如何声明一个给定类型的变量,那么该类型的类型抓换符就很容易得到:只需要把声明中的变量名和末尾分号去掉,再将剩余的部分用一个括号封装起来即可。例如,下面的声明:
void (b) ();
表示b为一个指向返回类型为void的函数的指针,因此
( void (
)() )
表示一个“指向返回类型为void的函数的指针”的类型转换符。


拥有了这些预备知识,我们可以分两步来分析表达式( (void() () )0 ) ()。
第一步,假定fp为一个函数指针,那么该如何调用fp指向的函数呢?调用方法如下: [ 声明为 void (*fp)(); ]

(fp) ();
因为fp是一个函数指针,那么
fp就是所指向的函数,所以(*fp) ()就是调用该函数的方法。ANIC标准允许将上式简写为fp(); 但是一定要记住这种写法只是一种简写形式。

第二步,如果C编译器能够理解我们大脑中对类型的认识,那么我们可以这样写: (0) ();
但是上式并不能奏效,因为
必须要一个指针来作为操作符,而且必须是函数指针,而0不是指针。所以在上式中必须对0进行类型转换,转换后的类型可以大致描述为“指向返回类型为void的函数的指针”。
因此将常数0转换为“指向返回类型为void的函数的指针”,可以这样写:
( void ()() )0
若fp为函数指针,要调用fp指向函数,则调用语句为(
fp)(); 所以要想调用首地址为0位置的函数,将 ( void ()() )0 替换 fp 即可:
(
( void (*)() )0 ) ();

也可以使用typedef来使表述更加清晰:

typedef    void (*funcptr) ()    //声明funcptr为 函数指针void (*) () 的别名
( *(funcptr)0 ) ();



2.2 运算符的优先级问题



假设要判定char类型的变量value的最高位是否为1,可以这样写:
if( value & 0x80 )...
如果要求对表达式的值是否为0能够显式地加以说明,可以这样写:
if( value & 0x80 != 0 )...
这个语句虽然更加好理解了,但却是个异常的语句。因为 != 的优先级 高于 & ,所以这个语句实际被编译器解释为: if( value & (0x80 != 0) )...


还有下面这个例子,本意是想让hi先左移4位,再加上low后赋给r:
r = hi<<4 + low;
但这样写是错误的,由于 + 的优先级高于 << ,所以实际会被解释为:
r = hi<<(4 + low);


对于上面的这些情况,最简单的解决办法是加括号。但是如果表达式中有了太多的括号,反而不容易理解,因此最好记住运算符的优先级。
遗憾的是,C语言运算符的优先级有15之多,记住它们并不容易。完整的C语言操作符的优先级表如表2-1所示:
技术分享图片
如果把这些运算符恰当分组,并且理解了各组运算符之间的相对优先级,那么这张表也不难记住。

  • 优先级最高者(前述操作符)并不是真正意义上的运算符,包括:函数调用操作符()、数组下标[]、结构成员选择操作符.以及->,它们都是自左向右结合,所以a.b.c,实际上是(a.b).c。
  • 单目运算符的优先级仅次于前述操作符,在和++的优先级相同的情况下,考虑到单目运算符是自右向左结合,所以p++实际会被解释为*(p++)。
  • 优先级比单目运算符要低的,接下来就是双目运算符了。在双目运算符中,算术运算符的优先级最高,移位操作符次之,其次是关系运算符,紧接着是逻辑运算符,最低是条件运算符(本质是三目运算符)。
    我们最需要记住的就是下面两点:
  • 1 任何一个关系运算符的优先级都要高于逻辑运算符;
  • 2 移位运算符的优先级比算术运算符要低,但比关系运算符要高。
    另外要注意的是,同一优先级栏的几个运算符优先级相同,比如乘法、除法和求余的优先级相同,加法和减法的优先级相同,两个移位运算符的优先级也相同。注意1/2a的含义是(1/2)a,而不是1/(2*a)。
    但是6个关系运算符的优先级不同,<、<=、>、>=的优先级要高于==和!=,所以a<b == c<d会被编译器解释为(a<b) == (c<d)。
    任意两个逻辑运算符的优先级不同。所有的按位运算符优先级要比顺序运算符的优先级高。

    2.3 注意作为语句结束标志的分号

    注意不要多写一个分号,考虑下面这个例子:

    if( x[i] > y );
    y = x[i];
    编译器在这种情况下不会报错,上面这个例子实际相当于下面这样:
    if( x[i] > y ) { }
    y = x[i];


    也要注意不要少些一个分号,比如下面这样:
    if( a < 3 )
    return
    logrec.data = x[0];
    logrec.time = x[1];
    此处的return后面遗落了一个分号;然而编译器仍然不会报错,只是会把logrec.data = x[0]作为返回值返回。

    2.4 switch语句

    看下面这个例子:
switch(color)
{
    case    1:    printf("red");
    case    2:    printf("yellow");
    case    3:    printf("blue");
}
又进一步假定变量color的值为2。最后,程序会打印出
yellowblue
因为在执行完第2个print函数后,在没有break的情况下,即使color不等于3,程序也会继续往下执行下去。



switch语句的这个特性,即是它的优势所在,也是它的一大缺点。一大缺点在于,程序员很容易遗漏各个case部分的break语句,造成一些难以理解的程序行为。而优势在于,如果程序员有意遗漏一个break语句,就能表达出一些采用其它方式
很难实现的程序控制结构。比如下面这个例子,它的作用是一个编译器在查找字符时,跳过程序中的空白字符。这里,空格、制表符和换行符的处理都是相同的,除了遇到换行符时程序的代码行计数器递增一次:

    case    ‘
‘:
                    lineCount++;
                    /****注意此处没有break******/
    case    ‘	‘:
    case    ‘ ‘:
                ......

2.6 悬挂else引发的问题



考虑下面的程序片段:

    if( x == 0 )
        if( y == 0 )    error();
    else
    {
        z = x + y;
    }



这段代码的本意应该是:当x==0的情况下,y==0,则执行error()函数,否则在x!=0的情况下,执行z=x+y。然而实际上与编程者的意图相去甚远。原因在于C语言中规定,else始终与同一对括号内最近的未匹配的if相结合。
如果按照上面程序实际的执行逻辑来调整缩进,应该是下面这样:

    if( x == 0 )
    {
        if( y == 0 )   
             error();
        else
        {
            z = x + y;
        }
    }


































































以上是关于[C陷阱和缺陷] 第2章 语法“陷阱”的主要内容,如果未能解决你的问题,请参考以下文章

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

《C陷阱和缺陷》读书笔记-其三:预处理连接库函数可移植性与建议

C陷阱与缺陷----语法陷阱

《C陷阱与缺陷》笔记

c陷阱与缺陷第一章

初学必看 C陷阱与缺陷(第二版)读书笔记