操作数求值顺序不定
C只规定小部分运算符以已知、特定顺序对其操作数求值,其他运算符的求值顺序没有定义,有偶然性。如a < b && c < d,C标准规定:先求a<b,如a<b成立,再求c<d并计算整个表达式的值。任何编译器都不会先算c<d再算a<b,即&&运算符的求值顺序在C标准中有明确规定,先左后右。但下到a<b粒度时,C编译器对a和b的分别求值就没有先后规定,各种可能性都有。
C里只规定了四个运算符(&& || ,和?:号)的操作数求值顺序。&&和||先求左边操作数值,如果单靠左边值无法决定整体值,才计算右边。而a?b:c先求a值,再根据a值选择b或c之一求值。运算符,先求左边操作数的值,然后抛弃它的值,求右边操作数值。
除此以外所有其它运算符对操作数求值顺序都未定义,即不能对求值顺序做任何保证。因此下面将数组x的前n元素复制到数组y中的方法不可行:
i = 0;
while(i < n)
y[i] = x[i++];
因为y[i]和x[i++]的求值顺序不确定,如果先算x[i++],就变成y[1]=x[0],y[2]=x[1]……,反过来y[i++] = x[i];问题也是一样。只能避开C标准的空白,用下面方法确保正确:for(i=0; i<n; i++) { y[i]=x[i]; } 这样无论=号左边还是右边先求值,结果都一样。
警惕&&和||中的短路运算
前面提到C里有四个运算符的操作数求值顺序确定,其中&& ||操作符先左后右,且某些情况下左边运算的结果就能决定整体表达式的值,比如a||b,如果a为true,整个表达式肯定是true,不需要再算b。同样对a&&b,如果a是false,整个表达式肯定是false,也不需要再算b。C标准里这种情况右侧不再求值,以节省运算量,这就是短路运算。
短路运算,即对于求值顺序确定的运算符组成的复合表达式,如果计算第一个子式能确定整个表达式的值,就不再去计算剩余子式,以此类推。这是C编译器的特性。
之前潜规则篇从代码可读性角度不提倡用短路运算组合出复杂的长表达式,比如用ptr &&(i=ptr->value)代替if(ptr){ i=ptr->value;}就是自寻烦恼,前者不过形式上紧凑,不能提高效率,反而降低可读性。
另外隐藏的短路运算还会导致bug,例如: if (a && myfunc(b)) { do_something(); }
如果a值为0,短路计算会使myfunc(b)不被调用。有人忽视这点,以为myfunc(b)总会被调用,结果导致逻辑错误。因此:不要把必须执行的有效运算放在&&和||的右边,以防止短路运算导致的功能遗漏,有效运算是指能改变程序执行结果的操作,比如ch&&i++及a && myfunc(b)等等。