&&和||
和C一样,C++对于“真假值表达式”采用所谓的“骤死式”评估方式。意思是一旦该表达式的真假值确定,即使表达式中还有部分尚未检验,整个评估工作仍告结束。
举个例子,在下面情况中:
char *p;
...
if ((p!=0)&&(strlen(p)>10))
你无须担心调用strlen时p是否为null指针,因为如果“p是否为0”的测试结果是否定的,strlen 就绝不会被调用。
同样道理,以下代码:
int rangeCheck(int index)
{
if ((index < lowerBound) || (index >upperBound))
//...
}
如果 index小于 lowerBound,它就绝不会被拿来和 upperBound比较。
这是C/C++社区中人尽皆知的一个行为,其年代已经古老得不复记忆。这是他们预期而毫不犹豫的行为。甚至他们所写的程序必须依赖这种“骤死式”评估方式才能表现出正确行为。例如,上一段代码所依持的一个重要事实是,如果是个null 指针,strlen 就不会被调用,因为对C++standard(以及Cstandard)来说,对一个 null 指针调用strlen,结果不可预期。
C++允许你为“用户定制类型”量身定做&&和||操作符。做法是对operator&& 和 operatorl| 两函数进行重载工作。
你可以在global scope 或是在每个class内做这件事儿。
然而如果你决定运用这个机会,你必须知道,你正从根本层面改变整个游戏规则,因为从此“函数调用 语义”会取代“骤死式语义”,也就是说,如果你将operator&&重载,下面这个式子:
if (expressionl && expression2)
会被编译器视为以下两者之一:
if (expression1.operator&&(expression2))
// 假设 operator&& 是个 member function。
if (operator&&(expressionl, expression2))
// 假设operator&&是个全局函数。
这看起来没什么大不了,但是“函数调用”语义和所谓的“骤死式”语义有两个重大的区别。
- 第一,当函数调用动作被执行,所有参数值都必须评估完成,所以当我们调用operator&&和operator||时,两个参数都已评估完成。换句话说没有什么骤死式语义。
- 第二,C++语言规范并未明确定义函数调用动作中各参数的评估顺序,所以没办法知道expression1和expression2 哪个会先被评估。这与骤死式评估法形成一个明确的对比,后者总是由左向右评估其自变量。
所以,如果你将&&或||重载,就没有办法提供程序员预期(甚至依赖)的某种行为模式。所以请不要重载&&或||。
,
逗号(,)操作符的情况类似,但是在探究它之前,我要先暂停一下,让你调匀你那乱掉了的呼吸:“逗号操作符?哦?有所谓的逗号操作符吗?”是的,有!
逗号操作符用来构成表达式,你应该已经在 for 循环的更新区 (update part)见过此物。
举个例子,以下函数以 Kemighan 和 Ritchie 合著的经典作品 The C Programming Language 第二版(Prentice-Hall,1988)为本:
//将字符串 s的字符顺序颠倒。
void reverse (char s[])
{
for(int i=0,j=strlen(s)-1;i<j;++i,--j)//啊哈,用到了逗号操作符!
{
int c=s[i];
s[i]=s[j];
s[j]=c;
}
在这里,for 循环的最后一个成分中,i被累加而j被递减。
这里很适合使用逗号操作符,因为for 循环的最后一个成分必须是个表达式(expression);如果用个别语句(statements)来改变i和j的值,是不合法的。
C++有一些规则用来定义&&和||面对内建类型的行为,C++同样也有一些规则用来定义逗号操作符面对内建类型的行为。
表达式如果内含逗号,那么逗号左侧会先被评估,然后逗号的右侧再被评估;最后,整个逗号表达式的结果以逗号右侧的值为代表。
所以面对上述循环的最后一个成分,编译器首先评估++i,然后是--j,而整个逗号表达式的结果是--j的返回值。
或许你会奇怪为什么你需要知道这些。是的,你需要知道,因为如果你打算撰写自己的逗号操作符,就必须模仿这样的行为。不幸的是,你无法执行这些必要的模仿。
如果你把操作符写成一个non-member function,你绝对无法保证左侧表达式一定比右侧表达式更早被评估,因为两个表达式都被当做函数调用时的自变量,传递给该操作符函数,而你无法控制一个函数的自变量评估顺序。所以non-member 做法不可行。
唯一剩下的可能是将操作符写成一个member function。但即便如此你仍然不能保证逗号操作符的左操作数会先被评估,因为编译器并不强迫做这样的事情。因此你无法“不但将逗号操作符重载,并保证其行为像它应该有的那样”。所以不要轻率地将它重载。
你或许会疑惑,重载的疯狂行为到底有没有底线?毕竟,如果可以将逗号操作符
重载,还有什么是你不能重载的呢?事实证明有底线存在。
你不能够重载以下操作符:
(关于 new 和 delete operators,以及 operator new,operator delete,operator new[]和operator delete[],请参考条款8)
当然啦,只因为可以重载这些操作符,就毫无理由地去进行,是没有道理的。
操作符重载的目的是要让程序更容易被阅读、被撰写、被理解,不是为了向别人夸耀你知道“逗号其实是个操作符”。
如果你没有什么好理由将某个操作符重载,就不要去做。面对&&,||和,,实在难有什么好理由,因为不管你多么努力,就是无法令其行为像它们应有的行为一样。