C缺陷与陷阱——词法陷阱

摘要

从较低的层面考察,程序是由符号(token)序列组成的,将程序分解成符号的过程,称为词法分析。

这节主要分析在程序被词法分析器分解成各个符号的过程中可能出现的问题。

编译器中负责将程序分解成一个一个符号的部分,一般称之为词法分析器。

C语言中符号之间的空白(包括空格符,制表符和换行符)将被忽略。

术语符号(token)

术语符号(token)指的是程序的一个基本组成单元, 其作用相当于句子中的单词。
(一个单词无论出现在什么句子,他所代表的意思大多数情况下不会有变化,是一个基本的表义单元)

以此类推,符号就是程序中的一个表义的基本单元。而组成符号的字符序列就不同,同一组字符序列在某个上下文环境中属于一个符号,而在另一个上下文环境中,可能属于完全不同的一个符号。

实例"->“与字符串”->"的区别

程序中单个字符孤立来看并没有什么意义,只有结合上下文才能体现出

//eg 1:
p->s="->";

上面语句两处出现了'-'字符,分别出现在等号左右,意义大相径庭。

上述两个字符分别是不同符号的组成部分。
等号左边的“-”代表"->"符号的组成部分,
等号右边的“-”是字符串“->”的组成部分。
符号“->”的含义也是组成该符号的字符"->"所没有的。

=不同于==

Pascal和Ada:
使用:=作为赋值运算符,符号==作为比较运算符;
C语言:
使用=作为赋值运算,符号 ==作为比较;
(一般而言,赋值运算符相对于比较运算符使用的更加频繁,因此字符数较少的=就被赋予了更常用的含义)

在C语言中赋值符号被作为一种操作符对待,故此重复进行赋值操作(a=b=c)可以很容易的书写,并且赋值操作还可以被嵌入到更长的表达式中。

潜在问题:本意是做比较时,误写成了赋值操作

//eg:1
if(x = y)
	break;
//该语句的本意似乎是要检查x和y是否相等
//在这里误写成了赋值操作,实际上是将y赋值给了x,然后检查该值是否等于0。

//eg:2
while(c= ' ' || c == '\t' || c =='\n');
	c=getc(f);
//该程序的本意是跳过文件中的空格,制表符,换行符
//由于在比较c和字符空格时,误将比较运算符写成了赋值运算符
//由于赋值运算符=的优先级低于逻辑运算符||,其实是将下表达式赋值给了c
c=  (' ' ||  c == '\t' || c == '\n');
//因为空格不等于0,空格的ASCII码为32,无论c此前为何值,上述表达式求值的结果都是1,因此文件将一直循环
//文件循环结束,如果getc库函数在文件指针到达文件结尾后是否允许继续读取字符,如果允许,循环一直进行,成为死循环

//eg:3
//某些c的编译器发现型如r1=r2的表达式出现在循环语句的条件判断语部分,会给出警告信息提示。
//当确实需要对变量进行赋值并检查该变量的新值是否为0,为了避免来自类编译器警告,我们不应该关闭,而应该使用显示比较
if(x = y)
	foo();
//应该写作
if((x = y) != 0)
	foo();

//eg:4
if((filedesc == open (argv[i] ,0 )) < 0)
	error();
//open 如果执行成功将返回0或者正数,如果执行失败将返回-1
//本意是将open函数的返回值放到变量filedesc,然后通过比较filedesc和0来检查open函数是否执行成功
//实际则是比较open的返回值和filedesc,然后检查比较的结果是否小于0,
//由于比较运算符的结果只可能是0/1,不会小于0,所以error函数将没有机会被调用
//如果代码被执行,似乎一切正常,除了变量的值不是open的返回值

词法分析中的“贪心法”

C语言的某些符号,例如“/”,“*”和“=”只有一个字节长,称为单字符符号。
而其他符号例如“/ *”和“==”以及标识符,包括了多个字符,称为多字符符号。

当C编译器读入一个字符“/”后又跟了一个字符“*”,那么编译器就必须做出判断:将其作为两个分别的符号对待还是合起来作为一个符号对待。

C语言的解决方案:每一个符号应该包含尽可能多的字符。

编译器将程序分解程序分解成符号的方法是:从左到右一个一个字符的读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能继续读入下一个字符,,重复上述判断,直到读入的字符组成的字符串不在可能组成一个有意义的字符。贪心法:如果编译器的输入流截止至某个字符之前都已经被分解成一个个符号,那么下一个符号将包括从该字符之后可能组成一个符号的最长字符串

除了字符串与字符常量,符号的中间不能嵌有空白(空格/制表/换行符)。 == 是单个符号,= =是两个符号

a---b;			//从左到右读入 先识别出最长自减为-- 剩下为减- 等同于 第二条语句
a -- -b;
a - --b;		//空格原因 先识别出减- 再识别出 --自减操作

// 如果/为判断下一个符号而读入的第一个字符,而且/后紧接着*,
//那么无论上下文如何,这两个字符都将被当作一个符号/*,表示一段注释的开始

//eg:1
y = x/*p;			//该语句的本意似乎是用x除以p指向的指,再把结果赋值给y
					//实际上/*被编译器理解为一段注释的开始,编译器将不断地读入字符,直到*/出现为止
y =x;				//该语句直接将x的值赋值给y,不考虑后面的字符 等同于本语句

//将上面语句重写.得到语义注释表达的原意
y = x / *p;
y = x / (*p);

//老版本编译器的乐趣(滑稽脸)
//允许=+类代表+=类
//eg:2
a=-1;			//从左到右读入 可得
a =- 1;			//等号和减号结合
a = a-1;		//如果原意是给a赋值,那么该语句完全偏离了原意。

a=/*b;			//编译器还会理解为注释码?不会的。。。滑稽脸
a =/ *b; 		//满足从左到右读入,a = a/ *b

整形常量

前面加0,该常量将被视为8进制
前面加0x,该常量将被视为16进制

字符和字符串

C语言中,单引号和双引号含义不同。
单引号引用的一个字符,实际上代表一个整数,值对应于该字符再编译器采用的字符集中的序列值。

双引号引用的字符串,代表了指向无名数组起始字符的指针,数组被双引号中的字符和二进制字\0’初始化

//单引号代表数值,双引号代表指针
char arr1[]={"ABC"};
char arr2[]={'A','B','C','\0'};
//上述两者等效

整形数的储存空间(16/32)一般可以容纳多个字符,有的编译器允许在一个字符常量/字符串常量中包括多个字符。‘yes’代替“yes”不会被编译器检测到,后者的定义是指向无名数组的首地址的指针,前者的含义不明。大多数编译器理解为:一个整数值,由字符代表的整数值按照特定编译器实现中定义的方式组合得到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值