Skip to main content

符号

我一把把把把住了

符号:词法分析的单位

在编译我们的代码的时候,编译器中的词法分析器(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) // 真!
...

相信你能一眼看出端倪,这里就不作解释了。


总的来说,解决这一类问题的方法主要有三个,一是打空格,二是打括号,三是拆成多个表达式。