实用经验 34 运算符引发的混乱

C++和它的前辈C确实紧密相连,C++从C那儿继承了很多的东西。其中也包括一套含义模糊不清的运算符。也正是由于这套运算符,加之C++那套灵活的语法,才导致了很多程序员C++运算符的使用混乱。按照产生混乱的原因,C++运算符的混乱可分为:粗心导致的混乱,优先级带来的换乱,结合性带来的混乱。

1.粗心导致的混乱

由于C++运算符的灵活性,看某些粗心的程序员,编辑出了出人意料的代码。下面代码就给我们展示了一个典型的例子。

if (nValue = 0)
{
 	// 如果nValue等于0,进行某些操作。
}

显然,程序员的本意是要写出if(nValue == 0)。但不幸的是,由于他的笔误导致上述语句未能达到程序员期望的效果,但它却是合法的。编译器在这种情况下,不会给出任何错误提示。这段代码的执行步骤是,首先将0赋值与nValue。然后if判断nValue是否为0。但结果是,if条件判断始终为false导致大括号的程序语句永远无法被执行。这就是我们通常所说的“=”和“==”混乱错误。

明白了“=”和“==”的混乱错误,接着介绍如何避免这种错误。避免这种错误其实很简单,将0和nValue的位置交换即可实现。代码如下:

if (0 == nValue )
{
	// 如果nValue等于0,进行某些操作。
}

此时如果你在写成if(0=nValue),编译器会直接报出编译错误,编译失败。所以可以看出这种错误其实可以通过良好的代码风格进行避免。

除了“=”和“==”运算符混淆以外,还有其他几对容易混淆的运算符号,他们是“&(按位与)”和“&&(与)”,“|(按位或)”和“||(或)”。对于这两类运算符,能够避免错误的只有细心了。

2.优先级导致的混乱

在C++中引入不同层级的运算符优先级通常是一桩好事,因为这样可以不必使用多余的、分散注意力的括号,继而把复杂的表达式简化。如iostream的设计初衷是允许工程师使用尽可能少的括号。

cout << "a+b=" << a+b <<endl;

由于加法运算的优先级比左移运算符高,所以我们的解析过程是符合期望的:a+b计算求值,然后结果发送给cout。

但并非所有的运算结果都如你期望。下面的代码就给我们展示了因优先级导致的混乱。

cout << a ? f() : g();

这是C++中唯一的一个三元运算符给我们的引来的麻烦,由于三元运算符的优先级低于左移运算符。所以按照编译器的理解,编译器首先让cout左移a位,然后把这个结果用作三元运算符所需的一个判断表达式。可悲的是这段代码居然是完全合法的!(具体为何合法,这涉及到cout的隐式转换符operator void *,此隐式转换符首先将cout<<a转换为void *,然后判断这个指针是否为空将其转化为true或false)。

针对这种情况,你可通过括号进行强制优先级切换实现。

cout << (a ? f() : g());

但我还是建议你采用if形式实现。因为这种形式有着清晰,有易于维护的优点。

if (a)
{
	f();
}
else
{
	g();
}

除了三元运算符之外,我们在实际程序编写时还应该注意“,”运算符与“->*”运算符。由于他们特殊的优先级往往会导致程序偏离最初的设想。

3.结合性导致的混乱

优先级决定表达式中各种不同的运算符起作用的优先次序,而结合性则在相邻的运算符的具有同等优先级时,决定表达式的结合方向。

结合性分为left-to-rigth和right-to-left两种结合方式。在C++中,大部分的运算符采用left-to-right这种方式,而采用rigth-to-left结合方式的仅有“=”、“+=”、“-=”、“*=”、“/=”、“%=”、“&=”、“^=”、“|=”、“<<=”、“>>=”11种赋值操作符。需要特殊说明的是C++中没有非结合的运算符。

关于结合性的经典示例当属“连续赋值”表达式。

a = b = c;

b的两边都是赋值运算,优先级自然相同。而赋值表达式具有“向右结合”的特性,这就决定了这个表达式的语义结构是“a = (b = c)”,而非“(a = b) = c”。即首先完成c向b的赋值(类型不同时可能发生提升、截断或强制转换之类的事情),然后将表达式“b = c”的值再赋向a。我们知道,赋值表达式的值就是赋值完成之后左侧操作数拥有的值,在最简单的情况下,即a、b、c的类型完全相同时,它跟“b = c; a = b;”这样分开来写效果完全相同。

规律总结
一般来讲,对于二元运算符▽来说,如果它是“向左结合”的,那么“x▽y▽z”将被解读为“(x▽y)▽z”,反之则被解读为“x▽(y▽z)”。注意,相邻的两个运算符可以不同,但只要有同等优先级,上面的结论就适用。再比如“a * b / c”将被解读为“(a * b) / c”,而不是“a * (b / c)”

一元运算符的结合性问题一般简单一些,比如“*++p”只可能被解读为“*(++p)”。

最后我们讨论一下“++”的结合性。“++”分为前置和后置。为了解释“++”的特殊结合性,我们看一下strcpy的代码实现。

char* strcpy( char* dest, const char* src )
{ 
	char*p = dest;  
	while(*p++ = *src++); 
	    
	return dest; 
} 

首先,解引用运算符“*”的优先级低于后自增运算符“++”,所以,这个表达式在语义上等价于“*(p++)”,而不是“(*p)++”。

依据ISO/IEC 9899:1999:后自增表达式的结果值就是被自增之前的那个值,然后这个结果值被确定之后,操作数的值会被自增。而这种“自增”的副作用会在上一个“序列点”跟下一个“序列点”之间完成。

按照ISO标准“while(*p++ = *src++) ;”可这么解释:首先,while当中的条件变量是个赋值表达式,左侧操作数是“*p++”,右侧操作数是“*src++”,整个表达式的值将是赋值完成之后左侧项的值。而左右两侧是对两个后自增表达式解引用。既然解引用作用于整个后自增表达式而不是仅作用于p或src,那么根据标准,它们“取用”的分别是指针p和src的当前值。而自增的副作用只需在下一个序列点之前完成。

然后,除此之外还有另外一种说法,这种说法是:后自增“x++”相当于一个逗号表达式:“tmp = x, ++x, tmp”。相对来讲,还是标准中的说法为编译器的实现(特别是优化)留下了更多空间,但上面的这种“说法”却更便于人的理解,而且跟正确的用法在最终效果上是一致的。

4.结合性和优先级的总结

(1)优先级决定表达式中各种不同的运算符起作用的优先次序,而结合性则在相邻的两个运算符的具有同等优先级时,决定表达式的结合方向;

(2)后自增(后自减)从语义效果上可以理解为在做完自增(自减)之后,返回自增(自减)之前的值作为整个表达式的结果值;

(3)准确来讲,优先级和结合性确定了表达式的语义结构,不能跟求值次序混为一谈。

请谨记

  • 不要混淆“=”和“==”、“&(按位与)”和“&&(与)”,“|(按位或)”和“||(或)”这三对运算符之间的差异,使用良好的代码规范避免由此而带来的麻烦。
  • 除非你肯定运算符的优先级和结合性是你期望的。否则最好用括号设置你期望的优先级。以防由此而引入的不确定麻烦。
上一篇:如何使用StandardScaler在Spark中标准化ONE列?


下一篇:将Spark DataFrame写入Hive表中的内存分配问题