参考资料:
- 《C++ Primer》第5版
- 《C++ Primer 习题集》第5版
4.1 基础(P120)
表达式由一个或多个运算对象组成,对表达式求值将得到一个结果。字面量和变量是最简单的表达式。
4.1.1 基本概念(P120)
C++ 定义了若干一元运算符、二元运算符,还有一个三元运算符,函数调用也是一种特殊的运算符。
组合运算符和运算对象
对以一个复杂的表达式,我们首先要理解其运算符的优先级、结合律,以及运算对象的求值顺序。
运算对象的转换
小整型会被提升为大整型。
重载运算符
重载运算符时,运算对象的类型和返回值的类型是可以重新定义的,但运算对象的个数、运算符的优先级和结合律是不能改变的。
左值和右值
C++ 表达式要么是右值,要么是左值。一个左值表达式的求值结果为一个对象或者一个函数,一般而言,左值可以位于赋值语句的左侧,但以常量对象为代表的某些左值不能位于赋值语句的左侧。右值表达式的结果不能位于赋值语句的左侧,且某些右值表达式的求职结果也是对象。
当一个对象被用作右值的时候,用的是对象的值;当其被用作左值的时候,用的是对象的身份(内存中的地址)。左值可以当作右值使用,而右值不能当左值使用。常见的需要使用左值的运算符:
- 赋值运算符需要左值作为其左侧运算对象,结果同样为左值。
- 取地址符作用于左值对象,结果为右值。
- 内置的解引用、下标运算符、迭代器的解引用、
string
和vector
的下标运算符的结果都是左值,
4.1.2 优先级与结合律(P121)
高优先级运算符与运算对象的结合更加紧密,如果优先级相同,则组合规律由结合律确定。算术运算符满足左结合律,即优先级相同时,将按照从左到右的顺序计算
括号无视优先级与结合律
被括号括起来的部分会被当成一个单元来求值,然后再与其他部分组合。
4.1.3 求值顺序
优先级规定了运算对象的组合方式,但没有说明运算对象按照什么顺序求值:
int i = f1() + f2();
f1
和 f2
一定会在执行乘法前调用,但两个函数执行的先后顺序是没有规定的。
对于那些未指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,则将引发未定义行为。
下面 4 种运算符明确指定了运算对象的求值顺序:&&
、||
、?:
、,
求值顺序、优先级、结合律
对于下面的表达式:
f() + g()*h() + j();
优先级规定了 g()
和 h()
的返回值相乘;结合律规定了 g()
和 h()
的乘积先和 f()
相加;这些函数的调用顺序没有规定。
书写复合表达式的两条经验:
- 在不确定运算符优先级和结合律的情况下最好使用括号
- 如果改变了某个运算对象的值,则在表达式的其他地方不要再使用这个对象。例外情况:
*++iter
。
4.2 算术运算符(P124)
算术运算符的结果为右值。
一元正号和一元负号,如 +a
可以理解为 0 + a
,其中 0
为 int
类型:
bool b = true;
bool b2 = -b; // b2是true
当计算的结果超过该类型的表示范围时就会产生溢出,溢出的结果是不可预知的,不同的系统可能有不同的结果。
整数相除的结果为整数,小数部分直接舍弃(向 0 取整)。
对取余运算来说,在不溢出的前提下恒有 m % (-n)
等价于 m % n
、(-m) % n
等价于 -(m % n)
:
21 % -6; // 结果为3
-21 % 6; // 结果为-3
4.3 逻辑和关系运算符(P126)
关系运算符作用域算术类型或指针类型,逻辑运算符作用于任何能转换成布尔值的类型。
逻辑与和逻辑和运算符
短路求值:&&
和 ||
都是先求左侧运算对象再求右侧对象。
vector<string> text{"hello", "world"};
for(const auto &s : text){
cout << s << endl;
}
上面的代码中,由于 string
类型对象可能较大,将 s
声明成引用可以避免对元素的拷贝。同时,由于不需要对 string
对象进行写操作,所以将 s
声明成 const
。
逻辑非运算符
关系运算符
关系运算符的结果为布尔值,所以其在连用时需要特别注意:
if(1 < 9 < 2) // 结果为真
相等性测试与布尔字面值
进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值。
练习
字符串字面量等价于以 \0
结尾的字符数组,所以上面的声明等价于让 cp
指向一个字符数组的首元素。
4.4 赋值元素符(P129)
赋值运算符的左侧对象必须为可修改的左值,运算结果为其左侧运算对象。如果赋值运算符两侧对象的类型不同,则右侧对象将转换为左侧对象。
C++11 允许将花括号括起来的初始值列表作为赋值语句的右侧对象:
vector<int> vi{1, 2, 3};
vi = {4, 5, 6, 7};
如果赋值运算符左侧为内置类型,则列表最多只能包含一个值,且其所占空间不应大于目标类型空间。
无论左侧对象的类型是什么,初始值列表均可以为空,此时编译器将创建一个值初始化的临时量并将其赋值给左侧对象。
赋值运算满足右结合律
i = j = 0;
赋值运算优先级较低
切勿混淆相等运算符和赋值运算符
复合赋值运算符
如:+=
、-=
等等。
任何一种复合运算符都等价于 a = a op b
,但复合运算符只求值一次,而使用普通的运算符还要进行一次额外的赋值。
4.5 递增和递减运算符(P131)
++
和 --
为对象的加 1 和减 1 提供了一种简洁的写法。此外,这两个运算符可以应用于迭代器,而很多迭代器不支持算术运算,所以递增递减运算是必要的。
递增递减运算符作用于左值对象,有前置版本和后置版本。前置版本返回将对象本身作为左值返回,后置版本将对象原始值的副本作为右值返回。
除非必须,否则不用递增递减运算符的后置版本,因为相比前置版本,后置版本需要将原始值保存下来,造成浪费。
在一条语句中混用解引用和递增运算符
auto p = v.begin();
while(p != v.end && *p >= 0){
cout << *p++ << endl; // 输出当前值,并将p向前移动一个元素
}