[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    ‘\n‘:
                    lineCount++;
                    /****注意此处没有break******/
    case    ‘\t‘:
    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;
        }
    }

原文地址:https://www.cnblogs.com/linuxAndMcu/p/9965761.html

时间: 2024-10-01 19:49:35

[C陷阱和缺陷] 第2章 语法“陷阱”的相关文章

第二章 语法陷阱

变量fp是一个函数指针,*fp就是该指针指向的函数,所以 (*fp)()就是调用该函数的方式,ANSI C标准允许程序员将上式简写为fp(). 在表达式(*fp)()中,*fp两侧的括号很重要,因为函数运算符()的优先级高于单目运算法*. 第二章 语法陷阱

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

第一章 词法"陷阱" 1. =不同于== if(x = y) break; 实际上是将y赋给x,再检查x是否为0. 如果真的是这样预期,那么应该改为: if((x = y) != 0) break; 2. &和| 不同于 && 和 ||   3.词法分析中的"贪心法" 编译器将程序分解成符号的方法是:从左到有一个一个字符的读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符床是否可能是一个符号的组成部分:如

阅读《C陷阱与缺陷》的知识增量

看完<C陷阱与缺陷>,忍不住要重新翻一下,记录一下与自己的惯性思维不符合的地方.记录的是知识的增量,是这几天的流量,而不是存量. 这本书是在ASCI C/C89订制之前写的,有些地方有疏漏. 第一章 词法陷阱 1.3 C语言中解析符号时使用贪心策略,如x+++++y将被解析为x++ ++ +y,并编译出错. 1.5 单引号引起的一个字符代表一个对应的整数,对于采用ASCII字符集的编译器而言,'a'与0141.97含义一致. 练习1.1 嵌套注释(如/*/**/*/)只在某些C编译器中允许,如

C陷阱与缺陷学习笔记

本书的介绍 作者以自己1985年在Bell实验室时发表的一篇论文为基础,结合自己的工作经验扩展成为这本对C程序员具有珍贵价值的经典著作.写作本书的出发点不是要批判C语言,而是要帮助C程序员绕过编程过程中的陷阱和障碍. 全书分为8章,分别从词法分析.语法语义.连接.库函数.预处理器.可移植性缺陷等几个方面分析了C编程中可能遇到的问题.最后,作者用一章的篇幅给出了若干具有实用价值的建议. 本书适合有一定经验的C程序员阅读学习,即便你是C编程高手,本书也应该成为你的案头必备书籍. 前言 N年读过这本书

《C陷阱与缺陷》学习笔记(一)

前言和导读 "得心应手的工具在初学时的困难程度往往超过那些容易上手的工具."比较认同这句话.我至今觉得自己其实还是个刚入了门的初学者. 第一章 "词法"陷阱 由于之前学过编译原理,对编译器词法分析(主要是符号识别过程)比较了解,理解起来不困难. 在讲到"="和"=="."|"和"||"."&"和"&&"时,联想起以前见过一些

C陷阱与缺陷 —— 读书笔记-1、词法“陷阱”

<C陷阱与缺陷>是由Andrew Koenig所著,高巍译.Andrew Koenig是AT&T大规模程序研发部(前贝尔实验室)成员,不仅有着多年的C++开发,研究和教学经验,而且还亲身参与了C++的演化和变革,对C++的变化和发展起到重要的影响. 第一章    词法陷阱 编译器中负责将程序分解为一个一个符号的部分,一般称为"语法分析器". 1.1    = 不同于 == while (c='' || c==' ' || c==' ') c=getc(f); 这个循

读书笔记--C陷阱与缺陷(七)

第七章 1.null指针并不指向任何对象,所以只用于赋值和比较运算,其他使用目的都是非法的. 误用null指针的后果是未定义的,根据编译器各异. 有的编译器对内存位置0只读,有的可读写. 书中给出了一种判断编译器如何处理内存0的代码: 1 #include <stdio.h> 2 int main() 3 { 4 5 char *p; 6 p=NULL; 7 printf("location 0 contains: %d\n", *p); 8 9 return 0; 10

读书笔记--C陷阱与缺陷

要参与C语言项目,于是作者只好重拾C语言(之前都是C++,还是C++方便). 看到大家都推荐看看  C陷阱与缺陷(C traps and pitfalls),于是好奇的开始了这本书的读书之旅. 决定将书中重要的知识点和易错点记录下来方便自己复习和他人学习~~不多说了,下面开始. 第一章:词法陷阱 在C语言中,符号(程序文字)之间的空白(包括空格符.制表符.换行符)将被忽略.书中举了一例: 1 if (x > big) big = x; 2 可以写成: 3 if 4 ( 5 x 6 > 7 bi

C陷阱与缺陷 读书笔记

C陷阱与缺陷 1.  typedef用法: ①    定义一种类型别名,而不是简单的宏替换: char *pa,pb;(注意:pb并没有定义为指针,虽然你可能想这么定义它) typedef char* PCHAR PCHAR pa, pb; ②   用在旧的C代码中,帮助struct.以前的代码中,声明struct新对象时,必须带上struct,即形式为:struc结构名对象名,如: struct tagPOINT1 { Int x; Int y; }; struct tagPOINT1 p1;