# Chapter 4 Expressions
# Fundamentals
# 表达式基础
- 运算对象转换:小整数类型会被提升为较大的整数类型
- 重载运算符:当运算符作用在类类型的运算对象时,用户可以自行定义其含义。
- 左值和右值:
- C 中原意:左值可以在表达式左边,右值不能。
C++
:当一个对象被用作右值的时候,用的是对象的值(内容);- 被用做左值时,用的是对象的身份(在内存中的位置)。
- 求值顺序:
int i = f1() + f2()
- 先计算
f1() + f2()
, 再计算int i = f1() + f2()
。但是 f1 和 f2 的计算先后不确定 - 但是,如果 f1、f2 都对同一对象进行了修改,因为顺序不确定,所以会编译出错,显示未定义
- 先计算
# Exercise 4.1
表达式 5 + 10 * 20 / 2
的求值结果是多少?
解:
等价于 5 + ((10 * 20) / 2) = 105
# Exercise 4.2
根据 4.12 节中的表,在下述表达式的合理位置添加括号,使得添加括号后运算对象的组合顺序与添加括号前一致。
(a) *vec.begin()
(b) *vec.begin() + 1
解:
*(vec.begin()) | |
(*(vec.begin())) + 1 |
# Exercise 4.3
C++ 语言没有明确规定大多数二元运算符的求值顺序,给编译器优化留下了余地。这种策略实际上是在代码生成效率和程序潜在缺陷之间进行了权衡,你认为这可以接受吗?请说出你的理由。
解:
可以接受。C++ 的设计思想是尽可能地 “相信” 程序员,将效率最大化。然而这种思想却有着潜在的危害,就是无法控制程序员自身引发的错误。因此 Java 的诞生也是必然,Java 的思想就是尽可能地 “不相信” 程序员。
# Arithmetic Operators
# 算术运算符
溢出:当计算的结果超出该类型所能表示的范围时就会产生溢出。
bool 类型不应该参与计算
bool b=true;
bool b2=-b; // 仍然为 true
//b 为 true,提升为对应 int=1,-b=-1
//b2=-1≠0,所以 b2 仍未 true
取余运算 m% n,结果符号与 m 相同
# Exercise 4.4
在下面的表达式中添加括号,说明其求值过程及最终结果。编写程序编译该(不加括号的)表达式并输出结果验证之前的推断。
12 / 3 * 4 + 5 * 15 + 24 % 4 / 2
解:
((12 / 3) * 4) + (5 * 15) + ((24 % 4) / 2) = 16 + 75 + 0 = 91
# Exercise 4.5
写出下列表达式的求值结果。
-30 * 3 + 21 / 5 // -90+4 = -86 | |
-30 + 3 * 21 / 5 // -30+63/5 = -30+12 = -18 | |
30 / 3 * 21 % 5 // 10*21%5 = 210%5 = 0 | |
-30 / 3 * 21 % 4 // -10*21%4 = -210%4 = -2 |
# Exercise 4.6
写出一条表达式用于确定一个整数是奇数还是偶数。
解:
if (i % 2 == 0) /* ... */ |
或者
if (i & 0x1) /* ... */ |
# Exercise 4.7
溢出是何含义?写出三条将导致溢出的表达式。
解:
当计算的结果超出该类型所能表示的范围时就会产生溢出。
short svalue = 32767; ++svalue; // -32768 | |
unsigned uivalue = 0; --uivalue; // 4294967295 | |
unsigned short usvalue = 65535; ++usvalue; // 0 |
# Logical and Relational Operators
# 逻辑运算符
- 短路求值:逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。先左再右
- 小技巧,声明为引用类型可以避免对元素的拷贝,如下,如 string 特别大时可以节省大量时间。
vector<string> text; | |
for(const auto &s: text){ | |
cout<<s; | |
} |
# Exercise 4.8
说明在逻辑与、逻辑或及相等性运算符中运算对象的求值顺序。
解:
- 逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为 短路求值。
- 相等性运算符未定义求值顺序。
# Exercise 4.9
解释在下面的 if
语句中条件部分的判断过程。
const char *cp = "Hello World"; | |
if (cp && *cp) |
解:
首先判断 cp
, cp
不是一个空指针,因此 cp
为真。然后判断 *cp
, *cp
的值是字符 'H'
,非 0。因此最后的结果为真。
# Exercise 4.10
为 while
循环写一个条件,使其从标准输入中读取整数,遇到 42
时停止。
解:
int i; | |
while(cin >> i && i != 42) |
# Exercise 4.11
书写一条表达式用于测试 4 个值 a、b、c、d 的关系,确保 a 大于 b、b 大于 c、c 大于 d。
解:
a>b && b>c && c>d |
# Exercise 4.12
假设 i
、 j
和 k
是三个整数,说明表达式 i != j < k
的含义。
解:
这个表达式等于 i != (j < k)
。首先得到 j < k
的结果为 true
或 false
,转换为整数值是 1
或 0
,然后判断 i
不等于 1
或 0
,最终的结果为 bool
值。
# Assignment Operators
# 赋值运算符
- 赋值运算的返回结果时它的左侧运算对象,且是一个左值。类型也就是左侧对象的类型。
- 如果赋值运算的左右侧运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。
- 赋值运算符满足右结合律,这点和其他二元运算符不一样。
ival = jval = 0;
等价于ival = (jval = 0);
- 赋值运算优先级比较低,使用其当条件时应该加括号。
- 复合赋值运算符,复合运算符只求值一次,普通运算符求值两次。(对性能有一点点点点影响)
任意复合运算符 op 等价于a = a op b;
# Exercise 4.13
在下述语句中,当赋值完成后 i 和 d 的值分别是多少?
int i; double d; | |
d = i = 3.5; // i = 3, d = 3.0 | |
i = d = 3.5; // d = 3.5, i = 3 |
# Exercise 4.14
执行下述 if 语句后将发生什么情况?
if (42 = i) // 编译错误。赋值运算符左侧必须是一个可修改的左值。而字面值是右值。 | |
if (i = 42) // true. |
# Exercise 4.15
下面的赋值是非法的,为什么?应该如何修改?
double dval; int ival; int *pi; | |
dval = ival = pi = 0; |
解:p
是指针,不能赋值给 int
,应该改为:
dval = ival = 0; | |
pi = 0; |
# Exercise 4.16
尽管下面的语句合法,但它们实际执行的行为可能和预期并不一样,为什么?应该如何修改?
if (p = getPtr() != 0) | |
if (i = 1024) |
解:
if ((p=getPtr()) != 0) | |
if (i == 1024) |
# Increment and Decrement Operators
# 递增递减运算符
- 前置版本
j = ++i
,先加一后赋值 - 后置版本
j = i++
,先赋值后加一
优先使用前置版本,后置多一步储存原始值。(除非需要变化前的值)
# 混用解引用和递增运算符
*iter++
等价于 *(iter++)
,递增优先级较高
auto iter = vi.begin();
while (iter!=vi.end()&&*iter>=0)
cout<<*iter++<<endl; // 输出当前值,指针向前移1
简介是一种美德,追求简洁能降低程序出错可能性
# Exercise 4.17
说明前置递增运算符和后置递增运算符的区别。
解:
前置递增运算符将对象本身作为左值返回,而后置递增运算符将对象原始值的副本作为右值返回。
# Exercise 4.18
如果 132 页那个输出 vector
对象元素的 while
循环使用前置递增运算符,将得到什么结果?
解:
将会从第二个元素开始取值,并且最后对 v.end()
进行取值,结果是未定义的。
# Exercise 4.19
假设 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) 表达式有误。C++ 并没有规定
<=
运算符两边的求值顺序,应该改为vec[ival] <= vec[ival+1]
# The Member Access Operators
# 成员访问运算符
ptr->mem
等价于 (*ptr).mem
注意 .
运算符优先级大于 *
,所以记得加括号
# Exercise 4.20
假设 iter
的类型是 vector::iterator
, 说明下面的表达式是否合法。如果合法,表达式的含义是什么?如果不合法,错在何处?
(a) *iter++; | |
(b) (*iter)++; | |
(c) *iter.empty(); | |
(d) iter->empty(); | |
(e) ++*iter; | |
(f) iter++->empty(); |
解:
- (a) 合法。返回迭代器所指向的元素,然后迭代器递增。
- (b) 不合法。因为
vector
元素类型是string
,没有++
操作。 - (c) 不合法。这里应该加括号。
- (d) 合法。判断迭代器当前的元素是否为空。
- (e) 不合法。
string
类型没有++
操作。 - (f) 合法。判断迭代器当前元素是否为空,然后迭代器递增。
# The Conditional Operator
# 条件运算符
条件运算符(
?:
)允许我们把简单的if-else
逻辑嵌入到单个表达式中去,按照如下形式:cond? expr1: expr2
可以嵌套使用,右结合律,从右向左顺序组合
finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass"; //等价于 finalgrade = (grade > 90) ? "high pass" : ((grade < 60) ? "fail" : "pass");
输出表达式使用条件运算符记得加括号,条件运算符优先级太低。
# Exercise 4.21
编写一段程序,使用条件运算符从 vector
中找到哪些元素的值是奇数,然后将这些奇数值翻倍。
解:
#include <iostream> | |
#include <vector> | |
using std::cout; | |
using std::endl; | |
using std::vector; | |
int main() | |
{ | |
vector<int> ivec{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }; | |
for (auto &i : ivec) | |
{ | |
cout << ((i & 0x1) ? i * 2 : i) << " "; | |
} | |
cout << endl; | |
return 0; | |
} |
# Exercise 4.22
本节的示例程序将成绩划分为 high pass
、 pass
和 fail
三种,扩展该程序使其进一步将 60 分到 75 分之间的成绩设定为 low pass
。要求程序包含两个版本:一个版本只使用条件运算符;另一个版本使用 1 个或多个 if
语句。哪个版本的程序更容易理解呢?为什么?
解:
#include <iostream> | |
using std::cout; using std::cin; using std::endl; | |
int main() | |
{ | |
for (unsigned g; cin >> g;) | |
{ | |
auto result = g > 90 ? "high pass" : g < 60 ? "fail" : g < 75 ? "low pass" : "pass"; | |
cout << result << endl; | |
// ------------------------- | |
if (g > 90) cout << "high pass"; | |
else if (g < 60) cout << "fail"; | |
else if (g < 75) cout << "low pass"; | |
else cout << "pass"; | |
cout << endl; | |
} | |
return 0; | |
} |
第二个版本容易理解。当条件运算符嵌套层数变多之后,代码的可读性急剧下降。而 if else
的逻辑很清晰。
# Exercise 4.23
因为运算符的优先级问题,下面这条表达式无法通过编译。根据 4.12 节中的表指出它的问题在哪里?应该如何修改?
string s = "word"; | |
string pl = s + s[s.size() - 1] == 's' ? "" : "s" ; |
解:
加法运算符的优先级高于条件运算符。因此要改为:
string pl = s + (s[s.size() - 1] == 's' ? "" : "s") ; |
# Exercise 4.24
本节的示例程序将成绩划分为 high pass
、 pass
、和 fail
三种,它的依据是条件运算符满足右结合律。假如条件运算符满足的是左结合律,求值的过程将是怎样的?
解:
如果条件运算符满足的是左结合律。那么
finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass";
等同于finalgrade = ((grade > 90) ? "high pass" : (grade < 60)) ? "fail" : "pass";
假如此时 grade > 90
,第一个条件表达式的结果是 "high pass"
,而字符串字面值的类型是 const char *
,非空所以为真。因此第二个条件表达式的结果是 "fail"
。这样就出现了自相矛盾的逻辑。
# The Bitwise Operators
# 位运算符
用于检查和设置二进制位的功能。
- 位运算符是作用于整数类型的运算对象。
- 二进制位向左移(
<<
)或者向右移(>>
),移出边界外的位就被舍弃掉了。 - 位取反(
~
)(逐位求反)、与(&
)、或(|
)、异或(^
)
有符号数负值可能移位后变号,所以强烈建议位运算符仅用于无符号数。
应用:
unsigned long quiz1 = 0; // 每一位代表一个学生是否通过考试
1UL << 12; // 代表第12个学生通过
quiz1 |= (1UL << 12); // 将第12个学生置为已通过
quiz1 &= ~(1UL << 12); // 将第12个学生修改为未通过
bool stu12 = quiz1 & (1UL << 12); // 判断第12个学生是否通过
位运算符使用较少,但是重载 cout、cin 大家都用过
位运算符满足左结合律,优先级介于中间,使用时尽量加括号。
# Exercise 4.25
如果一台机器上 int
占 32 位、 char
占 8 位,用的是 Latin-1
字符集,其中字符 'q'
的二进制形式是 01110001
,那么表达式 ~'q' << 6
的值是什么?
解:
首先将 char
类型提升为 int
类型,即 00000000 00000000 00000000 01110001
,然后取反,再左移 6 位,结果是 - 7296。
# Exercise 4.26
在本节关于测验成绩的例子中,如果使用 unsigned int
作为 quiz1
的类型会发生什么情况?
解:
在有的机器上, unsigned int
类型可能只有 16 位,因此结果是未定义的。
# Exercise 4.27
下列表达式的结果是什么?
unsigned long ul1 = 3, ul2 = 7; | |
(a) ul1 & ul2 | |
(b) ul1 | ul2 | |
(c) ul1 && ul2 | |
(d) ul1 || ul2 |
解:
- (a) 3
- (b) 7
- (c) true
- (d) ture
# The sizeof Operator
# sizeof 运算符
- 返回一条表达式或一个类型名字所占的字节数。
- 返回的类型是
size_t
的常量表达式。 sizeof
并不实际计算其运算对象的值。- 两种形式:
sizeof (type)
,给出类型名sizeof expr
,给出表达式
- 可用 sizeof 返回数组的大小
int ia[10];
// sizeof(ia)返回整个数组所占空间的大小
// sizeof(ia)/sizeof(*ia)返回数组的大小
constexpr size_t sz = sizeof(ia)/sizeof(*ia);
int arr[sz];
# Exercise 4.28
编写一段程序,输出每一种内置类型所占空间的大小。
解:
#include <iostream> | |
using namespace std; | |
int main() | |
{ | |
cout << "bool:\t\t" << sizeof(bool) << " bytes" << endl << endl; | |
cout << "char:\t\t" << sizeof(char) << " bytes" << endl; | |
cout << "wchar_t:\t" << sizeof(wchar_t) << " bytes" << endl; | |
cout << "char16_t:\t" << sizeof(char16_t) << " bytes" << endl; | |
cout << "char32_t:\t" << sizeof(char32_t) << " bytes" << endl << endl; | |
cout << "short:\t\t" << sizeof(short) << " bytes" << endl; | |
cout << "int:\t\t" << sizeof(int) << " bytes" << endl; | |
cout << "long:\t\t" << sizeof(long) << " bytes" << endl; | |
cout << "long long:\t" << sizeof(long long) << " bytes" << endl << endl; | |
cout << "float:\t\t" << sizeof(float) << " bytes" << endl; | |
cout << "double:\t\t" << sizeof(double) << " bytes" << endl; | |
cout << "long double:\t" << sizeof(long double) << " bytes" << endl << endl; | |
return 0; | |
} |
输出:
bool: 1 bytes
char: 1 bytes
wchar_t: 4 bytes
char16_t: 2 bytes
char32_t: 4 bytes
short: 2 bytes
int: 4 bytes
long: 8 bytes
long long: 8 bytes
float: 4 bytes
double: 8 bytes
long double: 16 bytes
# Exercise 4.29
推断下面代码的输出结果并说明理由。实际运行这段程序,结果和你想象的一样吗?如不一样,为什么?
int x[10]; int *p = x; | |
cout << sizeof(x)/sizeof(*x) << endl; | |
cout << sizeof(p)/sizeof(*p) << endl; |
解:
第一个输出结果是 10。第二个结果 1, 此处用法不合理不是未定义,参考 https://www.geeksforgeeks.org/using-sizof-operator-with-array-paratmeters/。
# Exercise 4.30
根据 4.12 节中的表,在下述表达式的适当位置加上括号,使得加上括号之后的表达式的含义与原来的含义相同。
(a) sizeof x + y | |
(b) sizeof p->mem[i] | |
(c) sizeof a < b | |
(d) sizeof f() |
解:
(a) (sizeof x) + y | |
(b) sizeof(p->mem[i]) | |
(c) sizeof(a) < b | |
(d) sizeof(f()) |
# Comma Operator
# 逗号运算符
从左向右依次求值。
左侧求值结果丢弃,逗号运算符结果是右侧表达式的值。
# Exercise 4.31
本节的程序使用了前置版本的递增运算符和递减运算符,解释为什么要用前置版本而不用后置版本。要想使用后置版本的递增递减运算符需要做哪些改动?使用后置版本重写本节的程序。
解:
在 4.5 节(132 页)已经说过了,除非必须,否则不用递增递减运算符的后置版本。在这里要使用后者版本的递增递减运算符不需要任何改动。
# Exercise 4.32
解释下面这个循环的含义。
constexpr int size = 5; | |
int ia[size] = { 1, 2, 3, 4, 5 }; | |
for (int *ptr = ia, ix = 0; | |
ix != size && ptr != ia+size; | |
++ix, ++ptr) { /* ... */ } |
解:
这个循环在遍历数组 ia
,指针 ptr
和整型 ix
都是起到一个循环计数的功能。
# Exercise 4.33
根据 4.12 节中的表说明下面这条表达式的含义。
someValue ? ++x, ++y : --x, --y |
解:
逗号表达式的优先级是最低的。因此这条表达式也等于:
(someValue ? ++x, ++y : --x), --y |
如果 someValue
的值为真, x
和 y
的值都自增并返回 y
值,然后丢弃 y
值, y
递减并返回 y
值。如果 someValue
的值为假, x
递减并返回 x
值,然后丢弃 x
值, y
递减并返回 y
值。
# Type Conversions
# 类型转换
# 隐式类型转换
设计为尽可能避免损失精度,即转换为更精细类型。
- 比
int
类型小的整数值先提升为较大的整数类型。 - 条件中,非布尔转换成布尔。
- 初始化中,初始值转换成变量的类型。
- 算术运算或者关系运算的运算对象有多种类型,要转换成同一种类型。
- 函数调用时也会有转换。
# 算术转换
# 整型提升
- 常见的 char、bool、short 能存在 int 就会转换成 int,否则提升为
unsigned int
wchar_t,char16_t,char32_t
提升为整型中int,long,long long ……
最小的,且能容纳原类型所有可能值的类型。
# 其他转换
p143
# 显式类型转换(尽量避免)
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:通常为运算对象的位模式提供低层次上的重新解释。
# 旧式强制类型转换
type expr
# Exercise 4.34
根据本节给出的变量定义,说明在下面的表达式中将发生什么样的类型转换:
(a) if (fval) | |
(b) dval = fval + ival; | |
(c) dval + ival * cval; |
需要注意每种运算符遵循的是左结合律还是右结合律。
解:
(a) fval 转换为 bool 类型 | |
(b) ival 转换为 float ,相加的结果转换为 double | |
(c) cval 转换为 int,然后相乘的结果转换为 double |
# Exercise 4.35
假设有如下的定义:
char cval; | |
int ival; | |
unsigned int ui; | |
float fval; | |
double dval; |
请回答在下面的表达式中发生了隐式类型转换吗?如果有,指出来。
(a) cval = 'a' + 3; | |
(b) fval = ui - ival * 1.0; | |
(c) dval = ui * fval; | |
(d) cval = ival + fval + dval; |
解:
- (a)
'a'
转换为int
,然后与3
相加的结果转换为char
- (b)
ival
转换为double
,ui
转换为double
,结果转换为float
- (c)
ui
转换为float
,结果转换为double
- (d)
ival
转换为float
,与fval
相加后的结果转换为double
,最后的结果转换为char
# Exercise 4.36
假设 i
是 int
类型, d
是 double
类型,书写表达式 i*=d
使其执行整数类型的乘法而非浮点类型的乘法。
解:
i *= static_cast<int>(d); |
# Exercise 4.37
练习 4.37
用命名的强制类型转换改写下列旧式的转换语句。
int i; double d; const string *ps; char *pc; void *pv; | |
(a) pv = (void*)ps; | |
(b) i = int(*pc); | |
(c) pv = &d; | |
(d) pc = (char*)pv; |
解:
(a) pv = static_cast<void*>(const_cast<string*>(ps)); | |
(b) i = static_cast<int>(*pc); | |
(c) pv = static_cast<void*>(&d); | |
(d) pc = static_cast<char*>(pv); |
# Exercise 4.38
说明下面这条表达式的含义。
double slope = static_cast<double>(j/i); |
解:
将 j/i
的结果值转换为 double
,然后赋值给 slope
。
# Operator Precedence Table
# 运算符优先级表
p147
# Chapter Summary
🍓:)