表达式基础
-
重载运算符:当运算符作用在类类型的运算对象时,用户可以自行定义其含义。
使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的;
但是运算对象的个数、运算符的优先级和结合律都是无法改变的。 -
左值和右值
- C中原意:左值可以在表达式左边,右值不能。
C++
- 当一个对象被用作右值的时候,用的是对象的值(内容);
- 被用做左值时,用的是对象的身份(在内存中的位置)。
- 建议:
- 不确定时,最好用括号来强制让表达式的组合关系符合程序逻辑的要求。
- 如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。
算术运算符
一元运算符的优先级最高,接下来是乘法和除法,优先级最低的是加法和减法。
算术表达式有可能产生未定义的结果:
-
一部分原因是数学性质本身:例如除数是0的情况;
-
另外一部分则源于计算机的特点:例如溢出,当计算的结果超出该类型所能表示的范围时就会产生溢出。
short svalue = 32767; ++svalue; // -32768 unsigned uivalue = 0; --uivalue; // 4294967295 unsigned short usvalue = 65535; ++usvalue; // 0
逻辑和关系运算符
短路求值:逻辑与运算符、逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。
- 逻辑与运算符,当且仅当左侧运算对象为真时才对右侧运算对象求值
- 逻辑或运算符,当且仅当左侧运算对象为假时才对右侧运算对象求值
int main() {
vector<string> vs{"zero.", "two", "three", "", "four"};
for (const auto &s:vs) {
cout << s;
if (s.empty() || s[s.size() - 1] == '.') {
cout << endl;
} else {
cout << " ";
}
}
return 0;
}
表达式i != j < k
的含义:
等于i != (j < k)
。首先得到j < k
的结果为true
或false
,转换为整数值是1
或0
,然后判断i
不等于1
或0
,最终的结果为bool
值。
赋值运算符
- 如果赋值运算的左右侧运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。
- 赋值运算符满足右结合律,这点和其他二元运算符不一样。
ival = jval = 0;
等价于ival = (jval = 0);
多重赋值语句中的每一个对象,它的类型与右边对象的类型相同(或者通过类型转换得到)。 - 赋值运算优先级比较低。
递增和递减运算符
递增和递减运算符有两种形式:前置版本和后置版本。
- 前置版本,这种形式的运算符首先将运算对象加1(或减1),然后将改变后的对象作为求值结果;
- 后置版本,也会将运算对象加1(或减1),但是求值结果是运算对象改变之前那个值的副本;
建议:用递增递减运算符的前置版本。
后置版本也有用武之地,举例:使用后置的递增运算符来控制循环输出一个 vector 对象内容直至遇到(但不包括)第一个负值为止
int main() {
vector<int> v{1, 2, 3, 0, 4, -1, 5};
auto pbeg = v.begin();
while (pbeg != v.end() && *pbeg >= 0) {
cout << *pbeg++ << endl;
}
return 0;
}
后置递增运算符的优先级高于解引用运算符,因此*pbeg++
等价于*(pbeg++)
。pbeg++
把 pbeg 的值加1,然后返回 pbeg 的初始值的副本作为其求值结果,此时解引用运算符的运算对象是 pbeg 未增加之前的值。最终,这条语句输出 pbeg 开始时指向的那个元素,并将指针向前移动一个位置。
大多数运算符都没有规定运算对象的求值顺序,这在一般情况下不会有什么影响。然而,如果一条子表达式改变了某个运算对象的值,另一条子表达式又要使用该值的话,运算对象的求值顺序就很关键了。举例:
int main() {
string str{"ab"};
for (auto s = str.begin(); s != str.end() && !isspace(*s); ++s) {
*s = toupper(*s);
}
// 改用while循环实现
auto s = str.begin();
while (s != str.end() && !isspace(*s)) {
// *s = tolower(*s++); 赋值语句未定义,赋值运算符左右两侧运算对象都用到了s,并且右侧的运算对象还改变了s的值
*s = tolower(*s);
s++;
}
for (auto s_ :str) {
cout << s_;
}
return 0;
}
对于使用 while 循环中注释的代码,编译器可能按照下面的任意一种思路处理该表达式:
*s = tolower(*s);
*(s + 1) = tolower(*s);
测试题:
假设ptr
的类型是指向int
的指针、vec
的类型是vector
、ival
的类型是int
,说明下面的表达式是何含义?如果有表达式不正确,为什么?应该如何修改?
(a) ptr != 0 && *ptr++
(b) ival++ && ival
(c) vec[ival++] <= vec[ival]
- (a) 判断
ptr
不是一个空指针,并且ptr
当前指向的元素的值也为真,然后将ptr
指向下一个元素 - (b) 判断
ival
的值为真,并且(ival + 1)
的值也为真 - © 表达式有误。C++并没有规定
<=
运算符两边的求值顺序,应该改为vec[ival] <= vec[ival+1]
成员访问运算符
点运算符 和 箭头运算符 都可用于访问成员
-
点运算符获取类对象的一个成员;
-
箭头运算符与点运算符有关,表达式
ptr->mem
等价于(*ptr).mem
:int main() { string str = "a string"; string *pstr = &str; auto n = str.size(); // 运行string对象str的size成员 n = (*pstr).size(); // 运行pstr所指对象的size成员 n = pstr->size(); // 等价于上一句代码 return 0; }
因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。
假设iter
的类型是vector::iterator
, 说明下面的表达式是否合法:
*iter++; // 合法。返回迭代器所指向的元素,然后迭代器递增。等价于 *(iter++);
(*iter)++; // 不合法。因为vector元素类型是string,没有++操作。
*iter.empty(); // 不合法。这里应该加括号: (*iter).empty()
iter->empty(); // 合法。判断迭代器当前的元素是否为空。
++*iter; // 不合法。string类型没有++操作。
iter++->empty(); // 合法。判断迭代器当前元素是否为空,然后迭代器递增。
条件运算符
条件运算符(?:
)允许我们把简单的if-else
逻辑嵌入到单个表达式中去,按照如下形式:cond? expr1: expr2
条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。
string s = "word";
string str = s + (s[s.size() - 1] == 's' ? "" : "s");
cout << str << endl;
位运算符
- 位运算符是作用于整数类型的运算对象。
- 二进制位向左移(
<<
)或者向右移(>>
),移出边界外的位就被舍弃掉了。 - 位取反(
~
)、与(&
)、或(|
)、异或(^
)
移位运算符的优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。
sizeof 运算符
- 返回一条表达式或一个类型名字所占的字节数。返回的类型是
size_t
。 - 两种形式:
sizeof (type)
和sizeof expr
s
因为 sizeof 的返回值是一个常量表达式,所以可用用来声明数组的维度。
sizeof 不会实际求运算对象的值,所以对于 sizeof p;
即使p是一个无效(即未初始化)的指针也不会有什么影响。在 sizeof 的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。sizeof 不需要真的解引用指针也能知道它所指对象的类型。
sizeof 运算符的结果部分地依赖于其作用的类型:
-
对 char 或者类型为 char 的表达式执行 sizeof 运算,结果得1
-
对引用类型执行 sizeof 运算得到被引用对象所占空间的大小
-
对指针执行 sizeof 运算得到指针本身所占空间的大小
-
对解引用指针执行 sizeof 运算得到指针指向的对象所占空间的大小,指针不需有效
-
对数组执行 sizeof 运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次 sizeof 运算并将所得结果求和(可以用来获取数组中元素的个数)。注意,sizeof 运算不会把数组转换成指针来处理
int x[10]; int *p = x; cout << sizeof(x) / sizeof(*x) << endl; // 10
-
对 string 对象或 vector 对象执行 sizeof 运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间
逗号运算符
逗号运算符(comma operator)含有两个运算对象,按照从左向右的顺序依次求值。
逗号表达式的优先级是最低的。
测试:someValue ? ++x, ++y : --x, --y
等价于 (someValue ? ++x, ++y : --x), --y
,
- 如果
someValue
的值为真,x
和y
的值都自增并返回y
值,然后丢弃y
值,y
递减并返回y
值 - 如果
someValue
的值为假,x
递减并返回x
值,然后丢弃x
值,y
递减并返回y
值
类型转换
隐式类型转换
- 比
int
类型小的整数值先提升为较大的整数类型。 - 条件中,非布尔转换成布尔。
- 初始化中,初始值转换成变量的类型。
- 算术运算或者关系运算的运算对象有多种类型,要转换成同一种类型。
- 函数调用时。
显式类型转换(尽量避免)
- static_cast:任何明确定义的类型转换,只要不包含底层const,都可以使用。
double slope = static_cast<double>(j);
- dynamic_cast:支持运行时类型识别。
- const_cast:只能改变运算对象的底层const,一般可用于去除const性质。
const char *pc; char *p = const_cast<char*>(pc)
- reinterpret_cast:通常为运算对象的位模式提供低层次上的重新解释。