符号
我一把把把把住了
符号:词法分析的单位
在编译我们的代码的时候,编译器中的词法分析器(lexer)会把代码分割为一个一个的符号(token)再进行解读。空白字符是分割符号最常见的方法。在 C 语言中对空白字符没有严格的要求,可以在符号之间任意的添加或删除。例如下面这段代码
if (a > b) {
a *= b;
}
完全可以写作
if
(
a
>
b
)
{
a
*=
b
;
}
但是符号之内是不能加入空格的。例如这里的if
、*=
作为一个整体,就不能够写作i f
和* =
,否则会被解读为两个符号。
不过比起上面这种情况,我们可能更熟悉下面的这种情况:
if(a>b){c*=a+b;}
即使没有空白字符间隔在符号之间,编译器仍然可以进行符号的分割。在这种分割中有一个著名的规则,便是贪心法。
贪心法
所谓贪心法,又被形象地称为 最大一口策略(maximal munch strategy),说的是:在符号分割时,即尽可能多的读取为同一个符号。例如++i
不会被解读为+(+i)
,而是会把两个加号做一个运算符,而+ +a
则毫无疑问的是+(+a)
,因为两个加号之间有空格间隔,这已经是两个符号了。
再比如
a---b
与
a-- -b
是等价的,而与
a- --b
是不同的。这是因为前两个减号是可以组成合法的符号的,于是就单出来了最后一个减号。
再比如经典的“趋近于”运算符-->
int x = 10;
do {
printf("countdown: %d\n", x);
} while (x --> 0);
会给出一个十到零的倒计时,x-->0
实际上是(x--)>0
的意思,后面的减号与大于在一起并没有构成箭头运算符->
。
所以有人写下这样的经典的邪恶代码,来询问你的时候:
i+++++i
你应该立即驳斥它,这甚至是不能通过编译的,因为它等价于
((i++)++)+i
所以,如果真的要展示邪恶代码,那么应该是
i++ + ++i
空格很微妙,也很重要。至于为什么很邪恶,参见这里。
还有一个边缘但是很经典的错误,现代的程序员在 IDE 的语法高亮加 持下已经不太会犯了:
pa = &a;
pb = &b;
pc = &c;
*pc=*pa/*pb;
这里我们尝试对pb
解引用,然后作为除数参与运算。但是很遗憾,星号与除号不小心连在了一起,构成了一个注释开始的符号/*
,也就是说,接下来的内容意外的变成了注释!
如果要极端一点,我们可以写出这样的代码:
a //*
//*/ b
不同版本的 C 语言编译器得到的结果不同:在 C99 支持//
注释之前,它是a / b
;在那之后则是a
。
既然提到注释,你是否想过注释可不可以嵌套?像这样:
/* /* nested comment */ */
答案是不可以,所以前面的代码只有/* /* nested comment */
在注释里面,最后一个 */
是不在其中的。
辨别符号的含义
同一个符号,往往在不同的地方肩负着不同的含义。比如-
可以作单目运算符,求相反数,也可以作减号。就连比较少见的符号,也可能会有多种含义:
#define STR(x) #x
通常情况下,符号的不同含义需要根据上下文判断。但切勿将一切诉诸程序员的判断力,因为除了前面提到的邪恶的+++++
和符合直觉的趋近于-->
之外,你可能还会遇到更多莫名其妙的运算符使用和组合。比如,没想到 C 语言也有 Python 那样的乘方运算符:
printf("50**2 = %d", 50 ** "2"); // 输出:50**2 = 2500
你被骗了吗?首先这里有两个*
符号,而不是只有一个**
(贪心法读到两个星号发现并不是合法的符号,所以只能读一个),其次里的两个*
符号并不是同一个含义:第一个星号就是乘号的含义,而第二个星号实际上是解引用:*"2"
实际上就是"2"[0]
也就是'2'
,而它的 ASCII 码恰好是 50!
你可能还会遇到“远远大于”
if (x >> 4) // x 远远大于 4?
...
或者是阶乘?
if (5!=120) // 真!
...
相信你能一眼看出端倪,这里就不作解释了。
总的来说,解决这一类问题的方法主要有三个,一是打空格,二是打括号,三是拆成多个表达式。