外观
01.词法和语法“陷阱”
约 2805 字大约 9 分钟
词法与语法C个人随笔
2022-06-08
第一章 :词法“陷阱”
术语“符号”指的是程序的一个基本组成单元(就像一个句子中的单词,无论在哪个句子中,表达意思都一样),而组成符号的字符序列就不同,同一组字符序列在某个上下文环境中属于一个符号,而在另一个上下文环境中可能属于完全不同的另一个符号。
编译器中负责将程序分解为一个一个符号的部分,一般称为“词法分析器”。
在C语言中,符号之间的空白(包括空格符、制表符和换行符)将被忽略。如下:
if(x > big) big = x;//变成下面
if
(
x
>
Big
….
1.1= 不同于 ==
建议:在写比较时,将常量放在左边,当不小心将==写成=时,编译器会报错。
1.2按位运算符&和|不同于逻辑运算符 && 和 ||
1.3词法分析中的“贪心法”
(1)单字符符号:/、*、和 =
(2)多字符符号:/*、==
(3)编译器需要作出判断是多字符符号还是单字符符号,遵循策略:贪心法、大嘴法。尽可能多的读入字符,直到不能组成一个有意义的符号。
(4)注意:除了字符串与字符常量,符号的中间不能嵌有空白(空格符、制表符和换行符)
1.4整形常量
(1)整型常量的第一个字符是数字0,那么该常量被视为八进制数。因此10和010完全不同。
(2)有时候为了上下文格式对齐的需要,可能会产生误判。
046
047
125
1.5字符与字符串
(1)单引号引起的一个字符实际上代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值。
(2)有很多编译器采用ASCII字符集。
(3)双引号引起的字符串,代表的是指向无名数组起始字符的指针。
(4)少数的编译器:整型数(一般为16位或32位)的存储空间可以容纳多个字符(一般为8位),因此有的C编译器允许在一个字符常量(以及字符串常量)中包括多个字符。(‘yes’代替“yes”不会被编译器检测到,‘yes’按照各自所代表的整数值组合得到)
1.6练习
练习1-1 某些C编译器允许嵌套注释。请写一个测试程序,要求:无论是对允许嵌套注释的编译器,还是对不允许嵌套注释的编译器,该程序都能正常通过编译(无错误消息出现),但是这两种情况下程序执行的结果却不相同。
提示:被双括号括起的字符串中,注释符/*属于字符串的--部分,而在注释中出现的双引号”"又展于注释的一部分。
/*/*/0*/**/1
这个解法主要利用了编译器作词法分析时的“大嘴法”规则。

练习1-2 如果由你来实现一个C编译器,你是否会允许嵌套注释?如果你使用的C编译器允许嵌套注释,你会用到编译器的这一特性吗?你对第二个问题的回答是否会影响到你对第一个问题的回答?
C语言定义并不允许嵌套注释。
练习1-3 为什么n-->0的含义是n-- >0,而不是n-->0?
根据“大嘴法”规则,在编译器读入>之前,就已经将--作为单个符号了。
练习1-4 a+++++b的含义是什么?
a ++ + ++ b
第二章 :语法“陷阱”
2.1 理解函数声明
(1)当计算机启动时,硬件将调用首地址为0位置的子例程。为了模拟开机启动时的情形,设计出一个C语句,以显式调用该子例程。(*(void(*)( ))0)( ) ;
(2)构造这类表达式只有一条简单的规则:按照使用的方式来声明。
(3)任何C变量的声明都由两部分组成:类型以及一组类似表达式的声明符(declarator)。声明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果。
float *pf ;
这个声明的含义是*pf是一个浮点数,也就是说,pf是一个指向浮点数的指针。
float *g ( ) , (*h) ( );
表示*g()与(*h)()是浮点表达式。因为()结合优先级高于*,*g()也就是*(g());g是一个函数,该函数的返回值类型为指向浮点数的指针。同理,可以得出h是一个函数指针,h所指向函数的返回值为浮点类型。
(4)类型转换符:只需要把声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个“封装”起来即可。
例如:

(5)假定变量fp是一个函数指针,那么如何调用fp所指向的函数呢?
(*fp)( ); 等于 fp( );
因为fp是一个函数指针,那么*fp就是该指针所指向的函数,所以(*fp)( )就是调用该函数的方式。ANSI C标准允许程序员将上式简写为fp()。
在表达式(*fp)( )中,*fp两侧的括号非常重要,因为函数运算符()的优先级高于单目运算符*。如果*fp两侧没有括号,那么*fp( )实际上与*(fp( ))的含义完全一致,ANSI C把它作为*((*fp)( ))的简写形式。
(6)对一个常数进行类型转换,将其转型为该变量的类型:只需要在变量声明中将变量名去掉即可。(比如将一份空内存,将数据强制转化为自定义类型的格式,我们也是将其变量声明时,把变量名去掉。)

(void (* )() ) 0 将0强制转换为(void (* )() )类型的变量(常量)。
(* ( void ( * )() ) 0 ) () ; 传入合适的函数变量(常量)。
(7)使用typedef 能够使表述更加清晰:
typedef void (*funcptr)();
( *( funcptr)0 ) () ;
(8)C 库函数 void (*signal(int sig, void (*func)(int)))(int) 设置一个函数来处理信号,即带有 sig 参数的信号处理程序。
typedef void (*HANDLER) (int ) ;
HANDLER signal (int,HANDLER);
2.2 运算符的优先级问题

(1)优先级最高者其实并不是真正意义上的运算符,包括:数组下标、函数调用操作符各结构成员选择操作符。它们都是自左于右结合,因此 a.b.c 的含义是(a.b).c,而不是a.(b.c)。
(2)单目运算符的优先级仅次于前述运算符。单目运算符是自右至左结合,因此*p++会被编译器解释成*(p++),即取指针p所指向的对象,然后将p递增1:而不是(*p)++,即取指针p所指向的对象,然后将该对象递增1。
(3)接下来就是双目运算符。在双目运算符中,算术运算符的优先级最高,移位运算符次之,关系运算符再次之,接着是逻辑运算符,赋值运算符,最后是三目运算符。
(4)两个重点
a.任何一个逻辑运算符的优先级低于任何一个关系运算符。
b.移位运算符的优先级比算术运算符要低,但是比关系运算符要高。
(5)任何两个逻辑运算符都具有不同的优先级。所有的按位运算符优先级要比顺序运算符的优先级高,每个“与”运算符要比相应的“或”运算符优先级高,而按位异或运算符(^运算符)的优先级介于按位与运算符和按位或运算符之间。
(6)三目条件运算符优先级最低。
这就允许我们在三目条件运算符的条件表达式中包括关系运算符的逻辑组合,例如:
tax_rate = income>40000 && residency<5 ? 3.5:2.0;
(7)上例其实还揭示了:赋值运算符的优先级低于条件运算符的优先级是有意义。此外,所有的赋值运算符的优先级是一样的,结合方式是从右到左。
(8)在所有的运算符中,逗号运算符的优先级最低。
2.3 注意作为语句结束标志的分号
(1)多写了一个分号可能不会造成什么不良后果;
(2)也有可能造成的错误可会是一个潜伏很深、极难发现的程序Bug。
2.4 switch语句
(1)程序员很容易就会遗漏各个case部分的break 语句。
2.5 函数调用
(1)C语言要求:在函数调用时即使函数不带参数,也应该包括参数列表。
2.6 “悬挂”else引发的问题
(1)原因在于C语言中有这样的规则,else始终与同一对括号内最近的未匹配的 if结合。
2.7 练习
练习2-1 C语言允许初始化列表中出现多余的逗号,例如:int days [ ] = {31,28,31,30,31,30,31,31,30,31,30,31,} ; 为什么这种特性是有用的?
我们可以把上例的缩排格式稍作改动如下:
int days [] = {
31,28,31,30,31,30,
31,31,30,31,30,31,
} ;
现在我们可以很容易看出,初始化列表的每一行都是以逗号结尾的。正因为每一行在语法上的这种相似性,自动化的程序设计工具(例如,代码编辑器等)才能够更方便地处理很大的初始化列表。
练习2-2 本章的第3节指出了在C语言中以分号作为语句结束的标志而带来的一些问题。一个代码行的含义要受到其后续代码行的影响,这一点多少显得有些“怪异”。因此,某些程序语言改为在第n行代码中使用某种指示标志,以表示第n+1行代码应该被当作同一个语句的一部分。例如,Unix系统的Shell (如bash、ksh、csh等)在代码行的结尾使用字符\来作为指示标志,表示下一个代码行是同一个语句的一部分。C语言在预处理器中以及字符串内部,沿用了Unix系统中的这一惯例。
