运算符(下)
求值顺序
求值顺序是独立于运算符优先级和结合性的存在,与这两者都没有关系。例如
f1() * f2() + f3();
函数f3()可能在f1()和f2()之前、之后甚至之间调用——因为这就这个表达式的计算过程而言是不影响的。例如我先计算f3()也不影响我之后再算f1()和f2()之后再先乘除后加减。但另一方面,如果这三个函数确确实实是有副作用的,例如修改了某个全局变量,进行了输入输出操作等,产生的结果却是是有可能不同的。例如
getchar() - getchar();
输入ab,你很难保证上面的表达式结果会是1还是-1。更有意思的是,下面这个表达式:
-(getchar() - getchar())
很有可能与前者有一模一样的结果。既然先求值谁是未定义的,也就是无所谓的,那么这个负号实际上就被编译器直接去掉了。
我们来看下面这个例子。
int i = 0, x[10], y[10];
while (i < 10)
x[i] = y[i++];
实际上,你不能保证是赋值符号左侧的i先求值还是右侧的i先求值。所以你面临着两种可能性,即x[i]=y[i]或x[i + 1] = y[i]。同样的如果你把它对调变成x[i++] = y[i]也是不行的。解决这个问题最直接的办法就是老老实实把自增单独提出来。
for (int i = 0; i < 10; ++i)
x[i] = y[i];
或者,如果你实在是一个忠实的自增爱好者,你可以试试
int* p = x, q = y;
while (p != x + 10)
*p++ = *q++;
虽然这里两侧都进行了自增,但是是互不影响的,因此没有什么问题。
同理,下面的代码都是应该避免的,它们都会引发 UB。
i++ + ++i;
f(i++, i++);
f(i = 1, i = 2);
a[i] = i++;
解决这些求值顺序问题的方法呼之欲出:
- 把有副作用的语句单独拆开来,明确执行的顺序
- 借助有求值顺序的运算符来实现
此外,标准还是明确规定了四个运算符的求值顺序,它们是&&、||、?:和,。
逗号运算符
逗号运算符规定先计算左表达式,再计算右表达式,最后返回左表达式的值。而因为逗号运算符是左结合的,所以在一连串的逗号运算符连接的表达式,总是从第一个开始依次往后执行,并返回最后一个表达式的值。
逗号运算符的本意是在不方便插入多个语句的地方(例如for的最后一个子句里)插入多个表达式。因此逗号拥有最低的优先级。所以下面的代码
a = 1, 2
并不会把a赋值为2,因为它实际的含义是
(a = 1), 2
所以a是1,2被直接丢掉了。
所以a, b == b为真,虽然确实是因为逗号运算符返回最后一个操作数,但不是因为(a, b) == b,而是因为a, (b == b)。
需要注意的是,逗号运算符的解析,只在没有逗号要求的情况下发生:
f(1, 2, 3); // 调用 f(int, int, int),不是逗号运算符
g((1, 2, 3)); // 调用 g(int),是逗号运算符
类似的情况还在复合初始化、宏等地方出现。
这就需要我们注意到求值顺序的问题,不能想当然的把逗号运算符的求值顺序往一切逗号上面套:
x = 1, x = 2, x = 3; // OK, x = 3
f(x = 1, x = 2, x = 3); // UB!
a[x = 1, x = 2, x = 3]; // OK, a[x = 3]
注意最后一行不是多维数组的访问(这对有多维数组的语言使用者来说可能比较熟悉,但请别弄混了,这不是你想的那样),逗号在这里只是一个普通的运算符而已。
短路求值
短路求值,说的是,只有&&第一个操作数为真才回去求值第二个操作数,||只有第一个操作数为假才回去求值第二个操作数。这很容易帮助我们写出下面这样的代码:
if (y != 0 && x / y > t)
...
这里我们不用担心会发生除以零的问题——因为只有y != 0为真,x / y > t才会被求值。同样的,
if (ch || (ch = getchar()))
...
只有在ch为'\0'的时候,才会调用getchar()读入一个字符。
你可能注意到一个细节——这样的形式完全可以单独拿出来用。(这里我们使用, 0的方法让结果一定可以参与逻辑运算)
cond() && (clause(), 0); // if (cond()) clause();
cond() || (clause(), 0); // if (!cond()) clause();
如果我们再大胆一点,也许我们可以进行进一步的组合。(想一想,为什么then_clause, 后面跟的是1?)
cond() && (then_clause(), 1) || (else_clause(), 0);
// if (cond()) then_clause(); else else_clause();
等等,我们是不是有一种更直接的方法:(想一想,下面哪个括号是可以省去的?)
cond() ? (then_clause(), 1) : (else_clause(), 0);
没错,你可能才意识到,其实条件运算符也完全具备短路求值的特性——如果条件的真假值没有确定,后面两个子句也不会被执行。即时确定了,也只会选择其中一个执行,另外一个不会执行。由此来看,条件运算符和if-else并没有本质上的区别。
条件运算符
不知道你注意到没有,条件运算符的结合性是从右向左的。也就是说,下面的代码
a > b && a > c ? a : b > c ? b : c
的含义是
a > b && a > c ? a : (b > c ? b : c)
而不是左结合的
(a > b && a > c ? a : b > c) ? b : c
这可能有些出会意料,可能没人会预料到出现左结合的形式,但是极个别语言(点名批评 PHP)还真就有这样的例外。
可以看到如果不加括号其实上面的代码是 很难读的,不过条件运算符的结合性给了我们一种格式化的思路:
a > b && a > c
? a
: b > c
? b
: c
或者把问号冒号都对齐,也是可以的。
你可能注意到赋值运算符和条件运算符微妙的关系了。在 C 语言里面条件运算符比赋值运算符优先级要高,因此下面的代码的含义显而易见
c = a > b ? a : b; // 选取 a 和 b 中的最大值
在 C++ 里,条件运算符和赋值运算符是同一个等级,但是上面句子的含义却没有变。因为它们都是右结合的运算符,所以即使优先级相同,结合性仍然保证了他们的先后顺序。例外出现在下面这个例子:
a > b ? c = a : c = b;
在 C++ 中它是合法的,被解读为
(a > b) ? (c = a) : (c = b);
但是在 C 语言里,因为条件运算符优先级较高,它的意思是
(a > b ? c = a : c) = b;
而 C 语言的三目运算符返回的是右值,因此编译失败。(而在 C++ 里,如果三目 运算符两个分支都是左值,那么返回的也是左值,所以(a > b ? a : b) = c是可以的。)
你可能已经注意到了,问号和冒号之间天然的有一个括号的语义在里面,里面的优先级不会受到外部干扰。
位运算与逻辑运算符
位移运算
当你想要把整数的高位和低位拼在一起时,如果你这么写:
r = hi << 4 + lo; // 等价于 r = hi << (4 + lo)
在lo == 0时,你还能得到正确的结果,但多数时候都不会那么侥幸。