在计算布尔表达式时,C++和C语言一样,使用布尔表达式短路求值法(参考实用经验38)计算布尔表达式值。这一原则表明:一旦确定了布尔表达式的真假值,即使还有部分表达式没有被测试求值,整个布尔表达式也停止运算。如&&的例子:
// 声明pszName指针。
char *pszName = NULL;
// 对pszName指针进行操作和赋值
...
if ((NULL != pszName) && (strlen(pszName) > 0))
{
...
}
此时,你可能会担心:如果pszName = NULL,strlen无法运行。但实际上这些担心是多余的,因为一旦(pszName != NULL)测试失败,strlen不会被调用执行。
同样,对于||也是如此,来看一个||的例子:
// 功能测试函数。验证index是否合法。
int Check(int index)
{
// Index 取值检查
if((index < LOWER_BOUND) || (index > UPPER_BOUND))
{
...
}
}
此时,如果index < LOWER_BOUND测试成立,index > UPPER_BOUND检查就不会被测试。
关于||和&&的简短求值特性,在很早以前就被反复灌输给每个C/C++程序员。所以大多程序员也基于简短求值法来写程序。例如上面第一段代码,当pszName = NULL时,确保strlen永远不会被调用是非常重要的。因为C++标准:strlen传入空指针,结果不确定。
C++提供了比C语言更加强大的操作符功能。例如:C++允许用户根据自己定义的数据类型定制&&和||操作,这就是我们经常说的操作符重载。而针对上述所说定制&&和||,就是重载函数operator &&和operator ||。读者可以根据自己的需要在全局重载或特定的类中重载&&和||,然后如果你这想采用这种方法,那我必须告诉你,你正在极大的改变游戏规则。这是因为你以函数调用替代简短计算法。
如果你重载了&&和||,到底发生了什么?请参考这段代码:
if(exp1 && exp2)
{
….
}
我们看这段代码的等价代码,编译后上述代码会等价于下面其中的一个实现:
// 当operator &&是类成员函数时。
if(exp1.operator&& (exp2))
{
…
}
// 当operator &&是全局函数时。
if(operator&& (exp1, exp2))
{
…
}
乍一看,这两者没有多大区别。但函数调用法和简短求值法是绝对不同的。首先当函数被执行时,需要计算所有的参数,所以执行函数operator && 和operator ||时,两个参数都需要计算。所以这儿没有采用简短求值规则。接着,就是C++语言规范中没有定义函数参数的计算顺序,所以我们没有办法知道表达式exp1 和表达式exp2到底哪个先计算。这完全与具有从左到右的参数计算顺序的简短计算法向违背。
因此,如果你重载&&和||,会导致&&和||没有办法提供给程序员提供他们期望的行为特征。所以不要重载||和&&。
小心陷阱
- 不要重装&&和||运算符。因为重载||和&&,会导致||和&&失去简短求值功能。这是你所不愿意看到的。同时也存在风险。
- 同样的规则也适用于,和(),请不要重载,和()操作符。
我们接着讨论,运算符,你可能不是很熟悉,运算符,请参考下述代码:
// 将字符串转换为它的逆序。输入参数为一个字符串。
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循环的最后一部分只能使用一个表达式,而分开表达式改变i和j的值都不合法。
内建,表达式同&&和||一样。C++也定义了其求值计算方法。C++规定:一个包含逗号的表达式首先计算逗号左边的表达式,然后计算逗号右边的表达式;整个表达式的结果是逗号右边表达式的值。所以在上述循环的最后部分里,编译器首先计算++i,然后是—j,逗号表达式的结果是–j。
,重载存在的问题,如代码所示:
#include <iostream>
#include <string>
using namespace std;
// 返回字符串函数
string retString()
{
cout << "string" << endl;
return "hello";
}
// 返回整数函数
int retInt()
{
cout << "int" << endl;
return 47;
}
int main()
{
retInt(), retString();
}
不管用什么编译器,只要其支持C++标准,程序的输出都应该为:
int
string
但如果重载了逗号操作符,像下面这样:
void operator , (int a, const string &b)
{
}
用VS2010编译后,程序就会先输出string,再输出int,可见操作数是从右向左求值的。然而如果对重载定义做少许的改动,代码如下:
void operator , (const int &a, const string &b)
{
}
也就是把左操作数a的定义变成常量引用。那么程序将会先输出int再输出string了。你可能会怀疑此时程序调用的是内建逗号操作符,那么可以在重载定义中加一条cout输出语句,就知道调用的其实还是重载操作符。只不过把参数的定义改了改,参数的求值顺序就变了,是不是很“神奇”?这就是,操作符存在的隐患。所以不要随便的重载,操作符。
那读者可能有这样的疑问,C++中到底哪些操作符可以重载,哪些不能重载呢?幸运的是C++确实作出了规定。
不能重载的操作符包括:
. .* :: ?:
new delete sizeof typeid
static_cast dynamic_cast const_cast reinterpret_cast
可重载的操作符包括:
operator new operator delete
operator new[] operator delete[]
+ - * / % ^ & | ~
! = < > += -= *= /= %=
^= &= |= << >> >>= <<= == !=
<= >= && || ++ -- , ->* ->
() []
虽然这些操作符支持重载,但也不能成为你重载的理由。重载操作符的目的是简化程序,而不引人新的问题,像||、&&和,还是不要重载的好。因为重载后的行为可能和你期望的行为不一样。
讨论了||、&&和,重载的陷阱,你可能会问重载操作符有什么规则吗?答案是有的。
操作符重载规则
- 类的设计者不能声明一个没有预定义的重载操作符。
- 不能为内置数据类型定义其他的操作符。
- 预定义的操作符优先级不能被改变。
- 一个类最终需要提供哪些操作符,是由该类预期的用途来决定的。
- 当一个重载操作符是一个名字空间的函数时,对于操作符的第一个和第二个参数,即等于操作符的左和右两个操作数,都会考虑转换。
操作符声明为类成员还是名字空间成员?
如果一个重载操作符是类成员,那么只有当跟它一起被使用的左操作数是该类的对象时,它才会被调用。如果该操作符的左操作数必须是其他的类型,那么重载操作符必须是名字空间成员。
C++要求赋值= 下标[] 调用() 和成员访问箭头-> 操作符必须被定义为类成员操作符。任何把这些操作符定义为名字空间成员的定义都会被标记为编译时刻错误。
由类设计者选择把操作符声明为一个类成员还是一个名字空间成员。如果有一个操作数是类类型,如String 类的情形,那么对于对称操作符,比如等于操作符最好定义为名字空间成员。
请谨记
- 在操作符重载时,支持重载并不是你重载的理由。重载操作符要以简化程序为目的,而不引入新的问题。
- 在重载||、&&和,时,要慎重。不要随意的重载他们。以防产生不可预知的副作用。