《C陷阱与缺陷》----词法“陷阱”

简介: 由于在C语言中赋值操作相对于比较出现更加频繁,所以将字符较少的符号=赋予更常用的含义—赋值操作。

导言:


由于一个程序错误可以从不同层面采用不同方式进行考察,而根据程序错误与考察程序的方式之间的相关性,可以将程序错误进行划分为各种陷阱与缺陷:

①.词法“陷阱”

②.语法“陷阱”

③.语义“陷阱”

④.连接问题

⑤.库函数问题

⑥.预处理器问题

⑦.可移植性缺陷

我们知道程序是由符号序列所组成的。本篇主要考察在程序被词法分析器分解成各个符号的过程中可能出现的问题,也就是词法“陷阱”。


1.=不同于==


符号 =作为赋值运算符


符号==作为比较运算符


由于在C语言中赋值操作相对于比较出现更加频繁,所以将字符较少的符号=赋予更常用的含义—赋值操作。


不过这种便利的使用可能导致一个潜在的问题:当程序员本想使用比较运算符却使用了赋值运算符。


1.1 案例1


if(x=y)
break;


该句的本意是想检查x是否等于y;但实际上却是将y的值赋给了x,然后检查表达式结果是否为0。

1.2 案例2


再比如下面这个例子:

例子中循环语句本想跳过空格符,制表符,和换行号;

   while(i=' '||i=='\t'||i=='\n')
   i=getchar();


但是却把比较运算符误写成赋值运算符,因为赋值运算符=的优先级是低于逻辑运算符||,所以语句先将’ ‘赋给了i,然后发先i结果不为0(’ 'ASCII码值是32)根据短路求值原则,左边为真,则右边不再进行,此语句也就是真,因此循环将一值进行下去直到整个文件结束。或者变成一个死循环。

1.3 启示


某些C编译器在发现形如a=b的表达式出现在循环语句的条件判断部分时,会给出警告以提醒程序员。当确实需要对变量进行赋值并检查该变量的新值是否为0时,为了避免该类编译器的警告,我们应该进行显式地比较,比如


if(x=y)
{
  Function();
}


应该写成


if((x=y)!=0)
{
  Function();
}


这样的写法就使得代码的意图一目了然。

1.4 案例3


前面一直谈的是将比较运算符误写成赋值运算符,另一方面,如果把赋值运算符误写成比较运算符,也会造成混淆。

if ((i == fun(a, b)) < 0)
    error();


本语句的原本意思是如果fun函数执行成功则返回0,执行失败则返回-1或者正数,然后再将返回值赋给i,通过比较i的大小来确定是否fun函数执行成功。但实际上是fun函数的返回值与i进行比较,结果只有真与假也就是0或1,永远不可能小于0,所以函数error没有机会调用。也就不知道fun函数调用是否失败。

2.&和 |不同于&&和 ||


3.词法分析中的“贪心法”


C语言有许多符号,比如只有一个字符长的单字符符号,和多个字符长的多字字符。比如== 和 /*。

当C编译器读取符号时,是如何读取的呢?

比如读入一个字符’/‘后又跟了一个字符’ *',那编译器是将其作为两个分别的符号对待,还是合起来作为一个符号对待?

3.1 读取规则—“贪心法”:


每个符号应该包含尽可能多的字符。


也就是说,编译器将程序分解成符号的方法是:


从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成字符串不再可能组成一个有有意义的符号。


这个处理策略就被称为“贪心法”。

3.2 注意:


需要注意的是,除了字符串与字符常量,符号的中间不能嵌有空白(空格符,制表符和换行符)。


比如:

a---b 与表


3.3 “贪心法”读取案例1


本代码想表达的意思是,x先除以指针p指向的数,然后再将所得的商,赋给y.


y=x / *p;// *p指向的是被除数


而实际上,/*被编译器理解为一段注释的开始,编译器将不断的读入字符,直到另一个 * /出现为止。


也就是说该代码,直接将x的值赋给了y,根本不会顾及后面出现的p。


而正确的表达意思应该是这样:


y=x/(*p);//*p指的是被除数


4.整形常量


我们知道一个整形常量第一个字符是数字0,则这个常量代表的是八进制位数。


因此10与010的意思不一样。


而有的C编译器会把8,9也作为八进制数字来处理,感觉很奇怪,所以ANSI C标准禁止了这种用法,以免混乱。


不过有时候,在上下文中为了格式对齐,可能会将十进制数写成八进制数。比如:


035
044
123//只是单词的使格式对齐没有多的意思,


5.字符和字符串


C语言中字符与字符串的区别在于单引号与双引号的使用,在某些情况下如果把两者弄混,编译器并不会监测报错,从而运行时产生难以预料的结果。我们先来看看字符与字符串的区别

5.1 字符


用单引号引起的一个字符实际上代表一个整数,整数对应着ASCII码值的字符序列值,比如’a‘的含义与0141(八进制)或者97(十进制)严格一致。


整数(一般为16位或32位)的存储空间可以容纳多个字符(一般位8位)因此有的C编译器允许在一个字符常量(以及字符串常量)中包含多个字符。也就是用‘yes’代替“yes”不会被该编译器监测到。“yes”代表的意思是依次包含‘y’,‘e’,‘s’以及空字符‘\0’的4个练习内存单元的首地址。而‘yes’的含义并没有准确的进行定义,但大多数编译器理解为“一个整数值”


257e9f22b0ef490a9e9924e770688d53.png


5.2 字符串


用双引号引起的字符串,代表的是一个指向无名数组起始字符的指针,该数组被双引号之间的字符以及一个额外的二进制为0的字符’0\‘初始化。


#include <stdio.h>
int main()
{
  printf("xiao tao lai lo\n");
  char ch[100] = "xiao tao lai lo";
  //ch是数组名,数组名是首元素的地址,也就是首字符的地址。
  printf(ch);//ch里存放着首元素的地址
}

a500cd741a284cdea6c57fdbcaac73a7.png


这说明这两个打印效果是一样的,所以一个字符串代表着一个指向无名数组起始字符的指针。可以用一个指针来存放字符串


#include <stdio.h>
int main()
{
char* p = "xiao tao lai lo";
  //指针p里面存放的就是字符串的首字符地址。
  printf(p);
}

eae7bddb89eb4a8ca0363dd142d772dc.png


6.练习题


6.1 练习题①


c3a60356f40b41d68ce2d46ef73ac43e.png


答案:

根据“贪心法”规则,在编译器读入>之前,就已经将- -作为一个符号,而后面的>不能和前面的组成一个符号。


6.2 练习题②


9e11aed8f9d04479aaa531bae445b083.png


答案:

这个代码唯一有意义的解析是这样:


a++ + ++b


但是根据“贪心法”规则,上面代码应该被分解为

a++ ++ +b


这个式子从语法上来说是不对的,它应该写成


((a++)++)+b


但是,a++的结果不能作为左值,因此编译器不会接收a++作为后面的++运算符的操作数,该代码没有意义。


……………………………………………………………………06fee652630344ba9668d8a5463fbc35.gif

………………………………………………………………………………………………………………………加油…………………………………………………………

相关文章
|
自然语言处理 编译器 C语言
C语言编程陷阱:词法陷阱
推荐一个零声学院免费教程,个人觉得老师讲得不错, 服务器课程
42 0
|
编译器 C语言
C语言编程陷阱:语法陷阱
c语言要求在函数调用时即使函数不带参数,也应该包括函数列表。 是挂else问题
57 0
|
存储 自然语言处理 编译器
C陷阱与缺陷
C陷阱与缺陷
62 0
C陷阱与缺陷
|
6月前
|
测试技术
常见测试陷阱
常见测试陷阱
|
6月前
|
自然语言处理 编译器 程序员
C陷阱与缺陷:词法陷阱
C陷阱与缺陷:词法陷阱
51 0
|
6月前
|
存储 程序员 编译器
C陷阱与缺陷:语法陷阱
C陷阱与缺陷:语法陷阱
52 0
|
存储 编译器 C语言
C语言编程陷阱:语义陷阱
C语言中只有一维数组,数组大小必须在编译器就作为一个常数确定下来。 C语言中数组的元素可以是任何类型的对象。
47 1
|
C语言
C语言编程陷阱:预处理器 陷阱
预处理器 不能忽视宏定义中的空格: #include &lt;stdio.h&gt;
39 1
|
编译器 C语言
《C陷阱与缺陷》之“语义”陷阱——数组越界导致的程序死循环问题
《C陷阱与缺陷》之“语义”陷阱——数组越界导致的程序死循环问题
140 0
|
自然语言处理 编译器 程序员
【C陷阱与缺陷】----语法陷阱
由于一个程序错误可以从不同层面采用不同方式进行考察,而根据程序错误与考察程序的方式之间的相关性,可以将程序错误进行划分为各种陷阱与缺陷
118 0