“牙里长嘴”和“a+=a-=a*a”
题目典故出自相声《反正话》:甲说一句话,乙则把词颠倒过来说。例如甲说“桌子”,乙说“子桌”。当甲说出“我嘴里长牙”后,乙按约定的规则说了一句“我牙里长嘴”。这时“包袱”立刻抖响,满堂捧腹。
捧腹的原因是因为“牙里长嘴”荒诞得出乎人们的预料,人们无法想象“牙里”如何能够“长嘴”。但是从另一方面讲,必须承认“我牙里长嘴”在语法上是中规中矩的——它的荒诞不经并非是由于冲破了语言规范的藩篱,而是由于其内涵荒诞——它描述的是一种根本无法想象的、无意义的情形。
相声是语言的艺术,编程也是语言的艺术。巧合的是,用C语言也同样会讲出这种“牙里长嘴”的话,这就是所谓的“未定义行为”(Undefined behavior,后面简称UB)。最著名的UB恐怕就是谭浩强所写的自称“主流教材”的《C程序设计》中二十多年来一直津津乐道的“a+=a-=a*a”这个表达式了,这个表达式就是典型的“牙里长嘴”。为什么这么说呢?
我们都知道,程序设计语言是一种人为定义的形式语言,因此这种语言所表达的含义也是人为定义的。譬如 1 + 1 这个表达式,它的含义就是要求计算机求出 1 + 1 的值。但是 1 + INT_MAX (注:INT_MAX是在limits.h中定义的一个宏,表示该编译器上int数据类型所能表示的最大的值)的含义是什么呢?对不起,C语言压根就没有规定这样写的含义究竟是什么。这就是所谓的UB。 1 + INT_MAX 这种写法的意义C语言没有任何规定,换句话说,谁也不知道它的含义是什么。所以这就是“牙里长嘴”。
有依据吗?有。C标准中描述“+”运算时有这样一句话:If an exceptional condition occurs during the evaluation of an expression (that is, if the result is not mathematically defined or not in the range of representable values for its type), the behavior is undefined.
标准总是这样骈五骈六得让人难以理解,原因很简单,标准追求的是严谨,可读性从来不是它所追求的首要目标。所以在这里得容我用俗话解释一下。它的意思就是,在表达式求值时,如果发生了什么意外情况,比如1/0,这在数学上就没有解释,或者求值结果不在对应类型所能表示范围内( 1 + INT_MAX就是这种情况,两个int类型数据相加应该得到一个int类型的值,但现在这个值却超出了int类型的表示范围),那么这个表达式究竟是什么意思,C语言说它不知道。
所以问题就是,你写了1 + INT_MAX 这样的表达式,连C语言都不知道是什么意思,你自己知道你到底说的是什么意思吗?你可能以为你知道,就如同你以为你知道“牙里长嘴”是什么意思一样,但那只能是一个错觉,因为谁都不知道“牙里长嘴”应该是什么样子。
“主流教材”中二十多年来一直津津乐道的“a+=a-=a*a”同样也是“牙里长嘴”,因为C语言规定:Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression.
这句话的意思是说,在相邻两个序点(sequence point)之间,同一个数据对象的值最多可以通过表达式求值改变一次。更通俗一些解释就是,在一个没有&&、||、,?:运算的表达式中,同一个数据对象的值最多可以改变一次。这是程序员写表达式必须遵守的一个基本规则。违反这条规则的代码,就是“牙里长嘴”。因为其行为是C语言未定义的,因而这样的代码是错误的。
但有些人不这样看。有一种常见的看法是,UB不算是错误,无法通过编译才算错误。这无异于说“牙嘴里长”才是错误,而“牙里长嘴”则不算错误。这种看法无疑是肤浅和荒唐的。一个程序员写出了C语言并没有规定其含义而他自己则居然以为他自己知道含义的代码,可能正确吗?
坚持UB不是错误的人往往有一条似是而非的理由,那就是编译器没有报错。但问题是C语言仅仅要求编译器看到“牙嘴里长”这样的话才必须报错,而并没有要求编译器看到“牙里长嘴”这样的话报错。这就如同听相声听到“牙里长嘴”时,大家只是哈哈一笑,绝对不会有人大声纠正一样。所以编译器不报错并不能证明代码没有错,正如
int i;
scanf("%d",i);
这样荒唐的写法在很多编译器上都不会报错一样。
scanf("%d",i);这样的代码很多编译器会给出警告,“a+=a-=a*a”在某些“聪明”的编译器上也会得到警告,这就如同“牙里长嘴”不会得到纠正只会得到哄堂大笑一样。
所以绕来绕去,问题又回到了最初的起点,“a+=a-=a*a”这种“牙里长嘴”的代码究竟有没有意义?如果你认为“牙里长嘴”荒谬绝伦,那么“a+=a-=a*a”无疑就是错误的。如果你一本正经地认为“牙里长嘴”不荒谬,那么能否麻烦您在牙里长个嘴给大家瞧瞧?