本节书摘来自异步社区出版社《C++覆辙录》一书中的第2章,第2.3节,作者: 【美】Stephen C. Dewhurst(史蒂芬 C. 杜赫斯特),更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.3:(运算符)优先级问题
本条款不讨论到底是伯爵夫人还是男爵夫人该在晚宴时坐在大使的旁座(此问题无解)。我们要讨论的是在C++语言中的多层级化的运算符优先级如何带来一些令人困扰的问题。
优先级和结合性
在一种程序设计语言中引入不同层级的运算符优先级通常来说是好事一桩,因为这样就可以不必使用多余的、分散注意力的括号而能把复杂表达式简化。(但是请注意,在复杂的或是比较晦涩的、亦即并非所有代码读者都能很好理解的表达式中显式地加上括号以表明意义,这是正确的想法。当然了,在那些平凡的、众人皆知的情况下一般来说还是不加不必要的括号反而最让人觉得清楚。)
a = a + b * c;
在上面的表达式中,我们知道乘法运算符具有最高的优先级,或者说最高的绑定强度,所以我们先执行那个乘法操作。赋值运算符的优先级是最低的,所以我们最后做赋值操作。
b = a = a + b + c;
这种情况下,我们知道加法操作会比赋值操作先执行,因为加法运算符的优先级比赋值运算符的优先级要高。但是哪个加法会先执行,又是哪个赋值会先执行呢?这就迫使我们去考察运算符的结合性了。在C++语言中,一个运算符要么是左结合的,要么是右结合的。一个左结合的运算符,比如加法运算符,会首先绑定它左边的那个实参。是故,我们先算出a、b之和,然后才把它加到c上去。
赋值运算符是右结合的,所以我们首先把a+b+c
的结果赋给a,然后才把a的值赋给b。有些语言里有非结合的运算符:如果@是一个非结合的运算符,那么形如a@b@c
的表达式就是不合法的。合情起见,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
。)10这是一个我们非加括号不可的情况:
cout << (a? f() :g());
如果你想被别人觉得精神方面无懈可击,你还可以再进一步:
if (a)
cout << f();
else
cout << g();```
这种写法也许不如前一种写法那么令人浮想联翩,但是它的确有着又清楚、又容易维护的优点。
很少有采用C++语言的软件工程师会在处理指涉到classes的指针时遭遇运算符优先级带来的问题,因为大家都知道`operator ->`和运算符.具有非常高的优先级。是故,像“`a =++ptr->mem;`”的意思就是要一个将ptr指涉的对象含有的成员mem自增后的结果。如果我们是想让这个ptr指针先自增,我们原本会写“a = (++ptr)->mem`;”,或也许`“++ptr; a = ptr->mem;`”,或哪天心情特别糟的话,一怒之下写成“``a = (++ptr, ptr->mem);”`。
指涉到成员的指针则完全是另一回事了。它们必须在一个class对象的语境里做提领操作(参见常见错误46)。为了这个,两个专用提领运算符被引入了语言:operator ->*用来从指涉到一个class对象的指针提领一个指涉到该对象的class成员的指针,运算符.*用来从一个class对象提领一个该对象的class成员的指针。
指涉到成员函数的指针通常用起来会比较头疼,但是它们一般不会造成特别严重的语法问题:
class C {
// ...
void f( int );
int mem;
};
void (C::*pfmem)(int) = &C::f
int C::*pdmem = &C::mem; ⑤
C *cp = new C;
// ...
cp->*pfmem(12); // 错误! `
⑤译者注:这些是C++语言里不常用的声明语法,牢记。
我们的代码通不过编译,因为函数调用运算符operator()的优先级高于
operator ->*。问题在于,将函数提领之前(此时其地
址尚未决议),我们无法调用它。这里,加括号是必须的:
(cp->*pfmem)(12); ⑥
⑥译者注:指涉到成员的指针除了包括一个运算符,还有一个名字。
指涉到数据成员的指针相对来说更容易出问题,考虑以下的表达式:
a = ++cp->*pdmem```
变量cp和上面那个是同一个指涉到class对象的指针,`pdmem`不是一个`class`成员的名字,而是一个指涉到成员的指针的名字。在这种情况下,由于`operator ->*`的优先级不如运算符++高,cp会在指涉到成员的指针被提领前实施自增。除非cp指涉到的是一个`class`对象的数组,否则这个提领动作肯定不知道会得到什么结果11。
指涉到class成员的指针是一个好多C++软件工程师都没理解透的概念。为了让你代码的维护工程师未来的日子好过些,我们还是本着平淡是真的精神使用它吧:
++cp;
a = cp->*pdmem;`
结合性带来的问题
大多数C++运算符是左结合的,而且C++语言里没有非结合的运算符。但这并不能阻止有些聪明过头的软件工程师以下面的方式来使用这些运算符:
int a = 3, b =2, c = 1;
// ...
if (a > b > c) // 合法,但很有可能是错的…… ```
这段代码完全合法,但极有可能辞不达意。表达式`“3 > 2 > 1`”的结果是false。就像大多数C++运算符一样,operator >是左结合的,所以我们先计算子表达式“3>2”,结果是`true`。然后余下的就是计算`“true>1`”。为了计算这个,我们首先对true实施目标为整数型别的型别转换,结果实际就是在对“1>1”评估求值,其结果显然是`false`。