[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*a
的含义是(1/2)*a
,而不是1/(2*a)
。
但是 6 个关系运算符的优先级不同,<
、<=
、>
、>=
的优先级要高于==
和!=
,所以a<b == c<d
会被编译器解释为(a<b) == (c<d)
。
任意两个逻辑运算符的优先级不同。所有的按位运算符优先级要比顺序运算符的优先级高。
2.3 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.4 悬挂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陷阱和缺陷] 第3章 语义“陷阱”
第3章 语义"陷阱" 一个句子哪怕其中的每个单词都拼写正确,而且语法也无懈可击,仍然可能有歧义或者并非书写者希望表达的意思.程序也有可能表面上是一个意思,而实际上的意思却相 ...
- [C陷阱和缺陷] 第1章 词法“陷阱”
有感自己的C语言在有些地方存在误区,所以重新仔细把"C陷阱和缺陷"翻出来看看,并写下这篇博客,用于读书总结以及日后方便自身复习. 第1章 词法"陷阱" 1.1 ...
- C陷阱与缺陷代码分析之第2章语法陷阱
作者:刘昊昱 博客:http://blog.csdn.net/liuhaoyutz 陷阱1 理解函数声明 作者提出一个问题:有一个首地址为0的函数,该函数返回值类型为void,没有参数.怎样用C语言的 ...
- c缺陷与陷阱笔记-第二章 语法陷阱
1.函数的调用和番薯返回值是函数指针的声明 定义一个函数指针,例如 int (*fp)(float),这个函数的返回值是Int,参数是1个float类型,调用这个函数的方法是 (*fp)(),还有f ...
- [C陷阱和缺陷] 第6章 预处理器
在严格意义上的编译过程开始之前,C语言预处理器首先对程序代码作了必要的转换处理.因此,我们运行的程序实际上并不是我们所写的程序.预处理器使得编程者可以简化某些工作,它的重要性可以由两个主要的原因说 ...
- [C陷阱和缺陷] 第7章 可移植性缺陷
C语言在许多不同的系统平台上都有实现.的确,使用C语言编写程序的一个首要原因就是,C程序能够方便地在不同的编程环境中移植. 不同的系统有不同的需求,因此我们应该能够预料到,机器不同则其上的C语 ...
- [C陷阱和缺陷] 第5章 库函数
有关库函数的使用,我们能给出的最好建议是尽量使用系统头文件,当然也可以自己造轮子,随个人喜好.本章将探讨某些常用的库函数,以及编程者在使用它们的过程中可能出错之处. 5.1 返回整数的getc ...
- [C陷阱和缺陷] 第4章 连接
一个C程序可能是由多个分别编译的部分组成,这些不同部分通过连接器合并成一个整体.在本章中,我们将考查一个典型的连接器,注意它是如何对C程序进行处理的,从而归纳出一些由于连接器的特点而可能导致的错误. ...
- 《C陷阱与缺陷》之1词法"陷阱"
编译器中负责将程序分解为一个一个符号的部分,一般称为"词法分析器".在C语言中,符号之间的空白(包括空格符.制表符或换行符)将被忽略. 1.=不同于== C语言使用符号" ...
随机推荐
- hdu 4435 bfs+贪心
/* 题意:给定每个点在平面内的坐标,要求选出一些点,在这些点建立加油站,使得总花费最少(1号点必须建立加油站).在i点建立加油站需要花费2^i. 建立加油站要求能使得汽车从1点开始走遍全图所有的点并 ...
- codevs 3971 航班
题目描述 Description B 国有N 座城市,其中1 号是这座国家的首都. N 座城市之间有M 趟双向航班.i 号点的转机次数定义为:从1 号点到i ,最少需要转机几 次.如果1 根本无法到达 ...
- json数组显示格式
{“colorAndImg”:[{"颜色":“红色”,"地址":“www.sohu.com”}, {“颜色”:“绿色,“地址”:“www.sohu.com”}] ...
- iOS 远程推送原理及实现
关于iOS 实现消息推送的原理: 1.provide[server]把要发送的消息,目的IOS设备标识打包.发送给APNS 2.APNS在自身已注冊Push服务的IOS设备列表中.查找有对应标识的IO ...
- seajs入门使用
使用 Sea.js 进行模块化开发还能够带来非常多优点: 模块的版本号管理. 通过别名等配置,配合构建工具,能够比較轻松地实现模块的版本号管理. 提高可维护性.模块化能够让每一个文件的职责单一,很有利 ...
- Erlang 又生虫了
好久不玩Erlang了.近期想鼓捣Eresye,下了个最新版OTP 17,结果.发现了问题. 安装这个最新版的Erlang (erl 6.0)后,用erlc编译了Eresye 1.2.5,并放入其li ...
- [Typescript Kaop-ts] Use AOP in Vue Components with TypeScript and Kaop-ts
Aspect Oriented Programming, AOP, allows to reuse logic across an entire app in a very neat way, dec ...
- linux c 获取当前执行进程总数
获取当前执行进程总数的命令为: ps auxw | wc -l 获取当前执行进程总数的源代码例如以下: #include <stdio.h> #include <stdlib.h&g ...
- VBS 操作Word
VBS 操作Word 1.新建Word文档 '使用Add方法 Dim ObjWD,ObjDOC Set ObjWD=CreateObject("Word.application" ...
- Redis3.0--集群安装部署
准备环境 操作系统:CentOS6.5 Redis3.0.0 192.168.3.154 192.168.3.158 192.168.3.160 192.168.3.162 一.安装 安装文件夹 / ...