C++ Primer 第四章 表达式

基础

左值和右值
  • 左值:可以放到等式左边的值
  • 右值:不可以放到等式左边的值
建议:复合表达式
  • 拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求
  • 如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象
    第 2 条规则有一个重要例外,当改变运算对象的子表达式本身就是另外一个子表达式的运算对象时该规则无效。例如在表达式 *++iter 中,递增运算符改变 iter 的值,iter(已经改变)的值又是解引用运算符的运算对象。此时(或类似的情况下),求值的顺序不会成为问题,因为递增运算(即改变运算对象的子表达式)必须先求值,然后才轮到解引用运算,显然这是一种很常见的用法,不会造成什么问题

算数运算符

算数运算符(左结合律)
运算符 功能 用法
+ 一元正号 + expr
- 一元负号 - expr
--------------------------------------
* 乘法 expr * expr
/ 除法 expr / expr
% 求余 expr % expr
--------------------------------------
+ 加法 expr + expr
- 减法 expr - expr

一元负号运算符对运算对象值取负后,返回其(提升后)的副本

int i = 1024;
int k = -i; // k 是 -1024
bool b = true;
bool b2 = -b; // b2 是 true!; 布尔值不应该参与运算
/*
对大多数运算符来说,布尔类型的运算对象将被提升为 int 类型。
布尔变量 b 的值为真,参与运算时将被提升成整数值 1,对他求负后的结果是 -1.
讲 -1 再转换回布尔值并将其作为 b2 的初始值,显然这个初始值不等于 0,转换成布尔值后应该为 1。
所以, b2 的值是真!
*/
  • C++ 语言的早期版本允许结果为负值的商向上或向下取整,C++11 新标准则规定商一律向 0 取整(即直接切除小数部分)
  • 根据取余运算的定义,如果 m 和 n 是整数且 n 非 0,则表达式 (m / n) * n + m % n 的求值结果与 m 相等。隐含的意思是,如果 m % n 不等于 0,则它的符号和 m 相同。除了 -m 导致溢出的特殊情况,其他时候 (-m) / n 和 m / (-n) 都等于 -(m / n), m % (-n) 等于 m % n, (-m) % n 等于 -(m % n)。
21 % 6 = 3 21 / 6 = 3
21 % 7 = 0 21 / 7 = 3
-21 % -8 = -5 -21 / -8 = 2
21 % -5 = 1 21 / -5 = -4

逻辑和关系运算符

结合律 运算符 功能 用法
右 ! 逻辑非 !expr
---------------------------------------------
左 < 小于 expr < expr
左 <= 小于等于 expr <= expr
左 > 大于 expr > expr
左 >= 大于等于 expr >= expr
---------------------------------------------
左 == 相等 expr == expr
左 != 不相等 expr != expr
---------------------------------------------
左 && 逻辑与 expr && expr
-------------------------------------------------
左 || 逻辑或 expr || expr
  • 短路求值:逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。
    将变量声明成引用可以避免对元素的拷贝
因为关系运算符的求值结果是布尔值,所以将几个关系运算符连写在一起会产生意想不到的结果
if (i < j < k) // 这个相当于用 i < j 的布尔值结果和 k 比较
// 正确写法
if (i < j && j < k)

进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值 true 和 false 作为运算对象

if (val == true) // 只有当 val 等于 1 时条件才为真

赋值运算符

赋值运算符的左侧对象必须是一个可修改的左值

C++11 新标准允许使用花括号括起来的初始化列表作为赋值语句的右侧运算对象

窄化转换
  • 从浮点数转换为整数
  • 从取值范围大的浮点数转换为取值范围小的浮点数(在编译期可以计算并且不会溢出的表达式除外)
  • 从整数转换为浮点数(在编译期可以计算并且转换之后值不变的表达式除外)
  • 从取值范围大的整数转换为取值范围小的整数(在编译期可以计算并且不会溢出的表达式除外)
int k = 0;
k = {3.14} // 错误:窄化转换
vector<int> vi; // 初始为空
vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // vi 现在含有 10 个元素了,值从 0 到 9
赋值运算符满足右结合律
int ival, jval;
ival = jval = 0; // 正确:都被赋值为 0

对于多重赋值语句中的每一个对象,它的类型或者与右边对象的类型相同、或者可由右边对象的类型转换得到

int ival, *pval; // ival 的类型是 int;pval 是指向 int 的指针
ival = pval = 0; // 错误:不能把指针的值赋给 int
string s1, s2;
s1 = s2 = "OK"; // 字符串字面值 "OK" 转换成 string 对象
赋值运算优先级较低
// 形式烦琐,容易出错的写法
int i = get_value();
while (i != 42) {
// xxxxxxx
i = get_value();
}
// 更好的写法
int i;
while ((i = get_value()) != 42) {
// xxxxxxxxx
}

因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号

复合赋值运算符

使用例如(a += b 和 a = a + b)唯一的区别是左侧运算对象的求值次数

递增和递减运算符

  • 前置版本:将改变后的对象作为求值结果
  • 后置版本:求值结果是运算对象改变之前那个值的副本
int i = 0, j;
j = ++ i; // j = 1, i = 1;
j = i ++; // j = 1, i = 2;
  • 除非必须,否则不用递增递减运算符的后置版本
解引用和递增运算符
auto pbeg = v.begin();
while (pbeg != v.end() && *pbeg >= 0)
cout << *pbeg ++ << endl;
/*
*pbeg ++ 等价于 *(pbeg ++),pbeg ++ 把 pbeg 的值加一,然后返回 pbeg 的初始值的副本作为其求值结果,此时解引用运算符的运算对象是 pbeg 未增加之前的值。最终这条语句输出 pbeg 开始时指向的那个元素,并将指针向前移动一个位置。
*/
运算对象可按任意顺序求值
while (beg != s.end() && !isspace(*beg))
*beg = toupper(*beg ++); // 错误:该赋值语句未定义

问题在于:赋值运算符左右两端的运算对象都用到了 beg,并且右侧的运算对象还改变了 beg 的值,所以该赋值语句是未定义的。
编译器可能按照下面的任意一种思路处理该表达式:

*beg = toupper(*beg);
*(beg + 1) = toupper(*beg);

成员访问运算符

  • 点运算符:获取类对象的一个成员
  • 箭头运算符:与点运算符有关, ptr -> mem 等价于 (*ptr).mem

条件运算符

  • 形式 cond ? expr1 : expr2
  • 执行过程:首先求 cond 的值,如果条件为真对 expr1 求值并返回该值,否则对 expr2 求值并返回该值。
嵌套条件运算符
finalgrade = (grade > 90) ? "high pass" : (grade > 60) ? "fail" : "pass";
// 检查成绩为:优秀、合格、不合格

随着条件运算符嵌套层数的增加,代码可读性急剧下降。因此,条件运算的嵌套最好别超过两到三层

  • 条件运算符的优先级非常低,通常要在两端加上括号
cout << ((grade < 60) ? "fail" : "pass"); // 输出 pass 或 fail
cout << (grade < 60) ? "fail" : "pass"; // 输出 1 或者 0 !
cout << grade < 60 ? "fail" : "pass"; // 错误:试图比较 cout 和 60

位运算符

  • 左结合律
运算符 功能 用法
~ 位求反 ~expr
--------------------------------------------------
<< 左移 expr1 << expr2
>> 右移 expr1 >> expr2
--------------------------------------------------
& 位与 expr & expr
--------------------------------------------------
^ 位异或 expr ^ expr
--------------------------------------------------
| 位或 expr | expr

以为运算符的优先级不高不低,介于中间;比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。

sizeof 运算符

  • 右结合律,所得的值是一个 size_t 类型
sizeof(type)
sizeof expr
  • 在第二种形式中,sizeof 返回的是表达式结果类型的大小。与众不同的一点是,sizeof 并不实际计算其运算对象的值
Sales_data data, *p;
sizeof(Salse_data); // 存储 Sales_data 类型的对象所占的空间大小
sizeof data; // data 的类型的大小,即sizeof(Sales_data)
sizeof p; // 指针所占的空间大小
sizeof *p; // p 所指类型的空间大小,即 sizeof(Sales_data)
sizeof data.revenue; // Sales_data 的 revenue 成员对应类型的大小
sizeof Salse_data::revenue; // 另一种获取 revenue 大小的方式
  • 对 char 或者类型为 char 的表达式执行 sizeof 运算,结果得 1
  • 对引用类型执行 sizeof 运算得到被引用对象所占空间的大小
  • 对指针执行 sizeof 运算得到指针本身所占空间的大小
  • 对解引用指针执行 sizeof 运算得到指针指向的对象所占空间的大小,指针不需有效
  • 对数组执行 sizeof 运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次 sizeof 运算并将所得结果求和。注意,sizeof 运算不会把数组转换成指针来处理
  • 对 string 对象或 vector 对象执行 sizeof 运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间
利用 sizeof 求出数组中元素的个数
constexpr size_t sz = sizeof(ia) / sizeof(*ia);
int arr2[sz]; // 正确:sizeof 返回一个常量表达式
// 因为 sizeof 返回值是一个常量表达式,所以我们可以用 sizeof 的结果声明数组的维度

逗号运算符

  • 对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉,逗号运算符真正的结果是右侧表达式的值。如果右侧运算符对象是左值,那么最终的求值结果也是左值。

类型转换

  • 关联:两种类型可以互相转换
int a = 3.5 + 3.6;
cout << a << endl; // 输出 7
int b = 3.5 + 3;
cout << b << endl; // 输出 6

现根据类型转换规则设法将运算对象的类型统一再求值,上述的类型转换是自动执行的,无需程序员的介入,被称作隐式转换
在上面的例子中,隐式转换被设计得尽可能避免丢失精度, 3 会变为 double,再进行加法,初始化得时候 double 转为 int

何时发生隐式类型转换
  • 在大多数表达式中,比 int 类型小的整数值首先提升为较大的整数类型
  • 在条件中,非布尔值转换成布尔类型
  • 初始化过程中,初始值转换成变量的类型,在赋值语句中,右侧运算对象转换成左侧运算对象的类型
  • 如果算数运算或关系运算的运算对象有多种类型,需要转换成同一种类型
  • 函数调用时也会发生类型转换
无符号类型的运算对象
  • 类型匹配:无需进行进一步转换
  • 无符号不小于带符号,那么带符号的运算对象转换成无符号
  • 带符号大于无符号,如果无符号类型的所有值都能存在该带符号类型中,则无符号转带符号,否则带符号转无符号
其他隐式类型转换(P143)
  • 数组转换成指针
  • 转换成布尔类型
  • 转换成常量
  • 类类型定义的转换
显式转换
  • 命名的强制类型转换具有如下形式 cast-name<type>(expression) type 时转换的目标类型,expression 是要转换的值,如果 type 是引用类型,则结果是左值。 cast-name 是 static_cast dynamic_cast const_cast reinterpret_cast
static_cast
  • 任何具有明确定义的类型转换,只要不包含底层 const,都可以使用 static_cast
const_cast
  • const_cast 只能改变运算对象的底层 const
reinterpret_cast
  • reinterpret_cast 通常为运算对象的位模式提供较低层次上的重新解释

运算符优先级(P147)

posted @   HuiPuKui  阅读(41)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示