C++ Primer 5th 随堂练习
【C++ Primer】第三章 字符串、向量和数组 (练习)
第四章 表达式
练习 4.1
表达式 5 + 10 * 20 / 2 的求值结果是多少?
解答
- 5 + 10 * 20 / 2
- = (5 + ((10 * 20) / 2))
- = (5 + (200 / 2))
- = (5 + 100)
- = 105
练习 4.2
根据 4.12 节中的表,在下述表达式的合理位置添加括号,使得添加括号后运算对象的组合顺序与添加括号前一致。(P147 运算符优先级表)
(a) *vec.begin() (b) *vec.begin() + 1
解答
在本题涉及的运算符中,优先级最高的是 成员选择运算符 和 函数调用运算符,其次是 解引用运算符,最后是 加法运算符。故添加括号后的等价表达式是:
- (a):*(vec.begin())
- (b):(*(vec.begin())) + 1
练习 4.3
C++ 语言没有明确规定大多数二元运算符的求值顺序,给编译器优化留下了余地。这种策略实际上是在代码生成效率和程序潜在缺陷之间进行了权衡,你认为这可以接受吗?请说出你的理由。
解答
我认为这可以接受,因为 C++ 的设计思想是尽可能地 “相信” 程序员,以实现效率最大化。然而,凡事都有两面性,这种思想的潜在危害即:无法避免程序员自身引发的各种错误。因此,Java 应运而生,其思想就是尽可能地 “不相信” 程序员,以实现稳健最大化。
练习 4.4
在下面的表达式中添加括号,说明其求值过程及最终结果。编写程序编译该(不加括号的)表达式并输出结果验证之前的推断。
12 / 3 * 4 + 5 * 15 + 24 % 4 / 2
解答
- 12 / 3 * 4 + 5 * 15 + 24 % 4 / 2
- = (((12 / 3) * 4) + (5 * 15) + ((24 % 4) / 2))
- = ((4 * 4) + 75 + (0 / 2))
- = (16 + 75)
- = 91
练习 4.5
写出下列表达式的求值结果。
(a) -30 * 3 + 21 / 5 (b) -30 + 3 * 21 / 5 (c) 30 / 3 * 21 % 5 (d) -30 / 3 * 21 % 4
解答
- (a):-30 * 3 + 21 / 5 = -90 + 4 = -86
- (b):-30 + 3 * 21 / 5 = -30 + 63 / 5 = -30 + 12 = -18
- (c):30 / 3 * 21 % 5 = 10 * 21 % 5 = 210 % 5 = 0
- (d):-30 / 3 * 21 % 4 = -10 * 21 % 4 = -210 % 4 = -(210 % 4) = -2
练习 4.6
写出一条表达式用于确定一个整数是奇数还是偶数。
解答
设某整数为 x,则通过 if (x % 2 == 0) 可判断 x 为奇数还是偶数。
若 x % 2 == 0,则 x 为偶数;若 x % 2 != 0 即 x % 2 == 1,则 x 为奇数。
此外,通过 if (x & 0x1) 这种位运算也可以判断。
练习 4.7
溢出是何含义?写出三条将导致溢出的表达式。
解答
当计算的结果超出该类型所能表示的范围时,就会产生 溢出 (overflow),例如:
short svalue = 32767; ++svalue; // -32768
unsigned uivalue = 0; --uivalue; // 4294967295
unsigned short usvalue = 65535; ++usvalue; // 0
练习 4.8
说明在逻辑与、逻辑或及相等性运算符中运算对象的求值顺序。
解答
练习 4.9
解释在下面的 if 语句中条件部分的判断过程。
const char *cp = "Hello World"; if (cp && *cp)
解答
cp 是指向字符串常量的指针,故上式的条件部分首先检查指针 cp 是否有效。若 cp 为空指针或无效指针,则 cp 无效/为假,由短路运算 (无需考虑后续表达式) 可知整体表达值为假。否则,cp 有效/为真,表明 cp 指向了内存中的某个有效地址。往后对指针 cp 解引用,检查 cp 所指对象是否为空字符 '\0',若 cp 所指对象并非空字符则条件满足,整体表达值为真;否则为假。
本例中,cp 初始化时指向了字符串字面值的首字符,保存了内存中有效的地址,为真;同时 cp 所指对象具体为字符 'H',非空,为真。故整体表达值有效/为真。
练习 4.10
为 while 循环写一个条件,使其从标准输入中读取整数,遇到 42 时停止。
解答
先检查输入数据流是否正常,再检查输入数字是否为 42。两种等价实现:
int num;
while(cin >> num && num != 42) { /* ... */ }
int num;
while(cin >> num) {
if (num == 42)
break;
/* ... */
}
练习 4.11
书写一条表达式用于测试 4 个值 a、b、c、d 的关系,确保 a 大于 b、b 大于 c、c 大于 d。
解答
注意,Python 才有链式比较,C++ 只能老实地、成双成对地比较。
a > b && b > c && c > d // 不可以写成 a > b > c > d
练习 4.12
假设 i、j 和 k 是三个整数,说明表达式 i != j < k 的含义。
解答
根据 C++ 优先级 (<, <=, >, >= 高于 ==, !=),上述表达式等价于 (i != (j < k),意为先比较 j 和 k 的大小,得到的结果是一个 bool 值 (1 或 0),然后判断 i 的值是否与之相等。
练习 4.13
在下述语句中,当赋值完成后 i 和 d 的值分别是多少?
int i; double d; d = i = 3.5; i = d = 3.5;
解答
- Line 1:i = 3,d = 3.0
- Line 2:d = 3.5,i = 3
练习 4.14
执行下述 if 语句后将发生什么情况?
if (42 = i) if (i = 42)
解答
- Line 1:发生编译错误,因为赋值运算符的左侧运算对象必须是左值,而整数字面值 42 是右值,不能被赋值。
- Line 2:变量 i 被赋值为 42,然后 if 判断 i 值非 0 为真,然而程序原意可能是 if (i == 42)。
练习 4.15
下面的赋值是非法的,为什么?应该如何修改?
double dval; int ival; int *pi; dval = ival = pi = 0;
解答
pi 是指向 int 的指针变量,不能直接赋值给 int 变量 ival (没有对应的隐式类型转换机制),故应分别赋值,改为:
double dval; int ival; int *pi;
pi = 0;
dval = ival = 0;
练习 4.16
尽管下面的语句合法,但它们实际执行的行为可能和预期并不一样,为什么?应该如何修改?
(a) if (p = getPtr() != 0) (b) if (i = 1024)
解答
注意赋值运算符的优先级低于比较运算符,因此 (a) 中表达式会先进行比较,再进行赋值。
注意赋值运算符和等于比较运算符意义完全不同,因此 (b) 中表达式会进行赋值判断,而非比较判断。
应改为:
(a) if ((p = getPtr()) != 0)
(b) if (i == 1024)
练习 4.17
说明 前置递增运算符 和 后置递增运算符 的区别。
解答
前置递增运算符 (++i),首先将运算对象加一,然后将改变后的对象作为求值结果;
后置递增运算符 (i++),也会将运算对象加一,但求值结果是运算对象改变之前的值的副本;
总之,这两种运算符均必须作用于左值运算对象。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
练习 4.18
如果 132 页那个输出 vector 对象元素的 while 循环使用前置递增运算符,将得到什么结果?
解答
练习 4.19
假设 ptr 的类型是指向 int 的指针、vec 的类型是 vector、ival 的类型是 int,说明下面的表达式是何含义?如果有表达式不正确,为什么?应该如何修改?
(a) ptr != 0 && *ptr++ (b) ival++ && ival (c) vec[ival++] <= vec[ival]
解答
- (a):先判断指针 ptr 是否为空,若为空,则表达式为假。否则,ptr 非空,继续通过解引用判断 ptr 所指整数是否为 0。若 ptr 所指整数为 0,则表达式为假。否则,ptr 所指整数不为 0,表达式为真。最后,令指针 ptr 向后移一位。该表达式是合法的,但最后的指针移位操作不一定有意义,特别是当 ptr 指向一个独立的整数变量时,指针后移操作将导致未定义的结果。
- (b):先判断 ival 的值是否为 0,若为 0,则表达式为假。否则,ival 不为 0,继续检查 ival+1 的值是否为 0。当且仅当 ival 和 ival 均不为 0 时,表达式为真。否则,表达式为 0。注意,如果二元运算符的两个运算对象涉及同一个对象并改变了对象的值,则这是一种不好的写法。按照程序的原意,应改写为 ival && (ival+1) 更好。
- (c):比较 vec[ival] 是否不大于 vec[ival+1],若满足则为真,否则为假。与 (b) 出现了类似的错误,二元运算符的两个运算对象涉及同一个对象时,不应改变对象的值。按照程序的原意,应改写为 vec[ival] <= vec[ival+1] 更好。
练习 4.20
假设 iter 的类型是 vector<string>::iterator,说明下面的表达式是否合法。如果合法,表达式的含义是什么?如果不合法,错在何处?
(a) *iter++; (b) (*iter)++; (c) *iter.empty(); (d) iter->empty(); (e) ++*iter; (f) iter++->empty();
解答
(a):合法,后置递增运算符的优先级高于解引用运算符,其含义是 解引用当前迭代器所处位置的对象内容,然后令迭代器位置后移一位,即等价于 *(iter++)。
(b):非法,后置递增运算符的优先级高于解引用运算符,其含义是 先解引用当前迭代器所处位置的对象内容,然后令该对象值 +1。然而,vector 内的对象是 string 类型的,没有后置递增操作。
(c):非法,成员选择运算符的优先级高于解引用运算符,其含义是 先执行 iter.empty(),然后对结果解引用。然而,迭代器没有 empty() 函数,所以无法通过编译。
(d):合法,等价于 (*iter).empty(),其含义是 先解引用迭代器当前所处指元素,然后令所得 string 对象调用 empty() 函数,判断 string 对象是否为空。
(e):非法,前置递增运算符和解引用运算符的优先级相同,其含义是 先解引用迭代器当前所处指元素,然后令所得 string 对象 +1。然而,string 对象没有前置递增操作。
(f):合法,后置递增运算符的优先级低于成员选择运算符,等价于 (*iter++).empty()。其含义是 先执行 (*iter).empty() 操作,判断迭代器当前位置所指对象是否为空,然后令迭代器位置后移一位。
练习 4.21
编写一段程序,使用条件运算符从 vector 中找到哪些元素的值是奇数,然后将这些奇数值翻倍。
解答
源程序:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> vec = {1, 2, 3, 4}; // 操作后结果应为 2, 2, 6, 4
for (int &n : vec) { // 或 for (auto &n : vec), 只要用了 & 就会原地修改
n *= ((n%2 == 1) ? 2 : 1); // 条件运算符
cout << n << " ";
}
cout << endl;
system("pause");
return 0;
}
输出:
2 2 6 4
练习 4.22
本节的示例程序将成绩划分为 high pass、pass 和 fail 三种,扩展该程序使其进一步将 60 分到 75 分之间的成绩设定为 low pass。要求程序包含两个版本:一个版本只使用条件运算符;另一个版本使用1个或多个 if 语句。哪个版本的程序更容易理解呢?为什么?
解答
源程序 - 版本一:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string finalgrade;
int grade;
cin >> grade; // 输入成绩 (0-100)
finalgrade = (grade > 75) ? ((grade > 90) ? "high pass" : "pass")
: ((grade >= 60) ? "low pass" : "fail");
cout << finalgrade << endl; // 输出相应评级
system("pause");
return 0;
}
源程序 - 版本二:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string finalgrade;
int grade;
cin >> grade; // 输入成绩 (0-100)
if (grade > 90)
finalgrade = "high pass";
else if (grade > 75)
finalgrade = "pass";
else if (grade >= 60)
finalgrade = "low pass";
else
finalgrade = "fail";
cout << finalgrade << endl; // 输出相应评级
system("pause");
return 0;
}
事实上,第二个版本容易理解。当条件运算符嵌套层数变多之后,代码的可读性急剧下降。而 if-else 语句的逻辑分层仍然十分清晰。
练习 4.23
因为运算符的优先级问题,下面这条表达式无法通过编译。根据 4.12 节中的表指出它的问题在哪里?应该如何修改?
string s = "word"; string pl = s + s[s.size() - 1] == 's' ? "" : "s" ;
解答
上述表达式存在优先级问题,其中优先级从高到低依次为:加法运算符、相等运算符、条件运算符和赋值运算符。故上式实际发生的行为是:先对 s 和 s[s.size() - 1] 求和/拼接得到新字符串,然后令新字符串与 's' 比较是否相等。此为非法操作,且背离程序原意,故应改为:
string s = "word";
string pl = s + ((s[s.size() - 1] == 's') ? "" : "s");
总之,条件表达式的优先级非常低,将其加入复合表达式时应使用圆括号明确优先级。更一般地,使用大量运算符构成复合表达式时,不使用圆括号明确优先级是十分糟糕的。
练习 4.24
本节的示例程序将成绩划分为 high pass、pass 和 fail 三种,它的依据是条件运算符满足右结合律。假如条件运算符满足的是左结合律,求值的过程将是怎样的?
解答
练习 4.25
如果一台机器上 int 占 32 位、char 占 8 位,用的是 Latin-1 字符集,其中字符 'q' 的二进制形式是 01110001,那么表达式 ~'q' << 6 的值是什么?
解答
位运算中,按位取反运算符的优先级高于左移位运算符,故先对 'q' 取反得。由于 int 占 32 位,首先将 char 类型提升为 int 类型,即 'q' 的 32 位二进制形式是 00000000 00000000 00000000 01110001,按位取反后为 11111111 11111111 11111111 10001110,然后左移 6 位得到 11111111 11111111 11100011 10000000,最高位为 1,可见为负数。
此外,C++ 规定整数按其 补码 形式存储,于是对上式求补 (原码取反得反码,反码加一得补码,注意保持符号位) 得到 10000000 00000000 00011100 10000000 即为最终的二进制形式,转换成十进制为 -7296。
练习 4.26
在本节关于测验成绩的例子中,如果使用 unsigned int 作为 quiz1 的类型会发生什么情况?
解答
常见的 unsigned int 是 16 位的,会因数据位不足 30 位而无法达到预期效果。
练习 4.27
下列表达式的结果是什么?
unsigned long ul1 = 3, ul2 = 7; (a) ul1 & ul2 (b) ul1 | ul2 (c) ul1 && ul2 (d) ul1 || ul2
解答
首先,将变量表示为二进制数,bin(ul1) = 00000000 00000010,bin(ul2) = 00000000 00000111。
- (a):按位与,因为 bin(ul1) & bin(ul2) = 00000000 00000010,所以 ul1 & ul2 = 3
- (b):按位或,因为 bin(ul1) | bin(ul2) = 00000000 00000111,所以 ul1 & ul2 = 7
- (c):逻辑与,所有非 0 整数对应的 bool 值都是 true,故该式等价于 true && true,得 ul1 && ul2 = true
- (d):逻辑或,所有非 0 整数对应的 bool 值都是 true,故该式等价于 true || true,得 ul1 II ul2 = true
练习 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;
system("pause");
return 0;
}
输出:
bool: 1 bytes
char: 1 bytes
wchar_t: 2 bytes
char16_t: 2 bytes
char32_t: 4 bytes
short: 2 bytes
int: 4 bytes
long: 4 bytes
long long: 8 bytes
float: 4 bytes
double: 8 bytes
long double: 16 bytes
练习 4.29
推断下面代码的输出结果并说明理由。实际运行这段程序,结果和你想象的一样吗?如不一样,为什么?
int x[10]; int *p = x; cout << sizeof(x)/sizeof(*x) << endl; cout << sizeof(p)/sizeof(*p) << endl;
解答
推测第一个输出为 40 / 4 = 10,表示数组元素种数;第二个输出为 4 / 4 = 1。实际运行输出:
10
2
经测试,sizeof(p) = 8 (竟然不为 4 ?!?),sizeof(*p) = 4,这是为什么呢?可能与我所用的编译环境有关!
练习 4.30
根据 4.12 节中的表,在下述表达式的适当位置加上括号,使得加上括号之后的表达式的含义与原来的含义相同。
(a) sizeof x + y (b) sizeof p->mem[i] (c) sizeof a < b (d) sizeof f()
解答
(a) sizeof (x + y) // 本应会发生 (sizeof x) + y
(b) sizeof p->mem[i] // 本应会发生 sizeof (p->mem[i])
(c) sizeof (a < b) // 本应会发生 (sizeof a) < b
(d) sizeof f() // 本应会发生 sizeof (f())
练习 4.31
本节的程序使用了前置版本的递增运算符和递减运算符,解释为什么要用前置版本而不用后置版本。要想使用后置版本的递增递减运算符需要做哪些改动?使用后置版本重写本节的程序。
解答
练习 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) { /* ... */ }
解答
迭代指针 ptr 遍历数组 ia,直至 ptr 指向 ia 的最后一个元素。
练习 4.33
根据 4.12 节中的表说明下面这条表达式的含义。
someValue ? ++x, ++y : --x, --y
解答
根据优先级 (条件运算符高于逗号运算符),上式等价于:
(someValue ? ++x, ++y : --x), --y
首先判断 someValue 是否为真,若为真,则依次执行 ++x 和 ++y,最后再执行 --y;否则为假,执行 --x 后再执行 --y。
练习 4.34
根据本节给出的变量定义,说明在下面的表达式中将发生什么样的类型转换:
(a) if (fval) (b) dval = fval + ival; (c) dval + ival * cval;
解答
注意分清各运算符遵循的是 左结合律 还是 右结合律。
- (a):if 语句的条件应为 bool 值,故 float 变量 fval 将转换为 bool 值,即仅当 fval 为 0 时转换为 false,其余非 0 值转换为 true;
- (b):float 变量 fval 与 int 变量 ival 相加,ival 将转换为 float 类型再与 fval 求和,并将结果转换为 double 类型以赋予 double 变量 dval;
- (c):int 变量 ival 与 char 变量 cval 相乘,cval 将执行整型提升 (integral promotion) 转换为 int 类型再与 ival 求积,并将结果转换为 double 类型以赋予 double 变量 dval。
练习 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):char 字面值 'a' 将整型提升为 int 类型 (ASCII 码对应 97),再与 int 字面值 3 求和得到 100,最后使之转换为 char 类型 (ASCII 对应 'd') 赋予 char 变量 cval;
- (b):int 变量 ival 先转换为 double 类型再与 1.0 相乘,得到 double 类型的结果,然后 ui 转换为 double 类型减去该结果,最终的结果将转换为 float 类型以赋予 float 变量 fval;
- (c):unsigned int 变量 ui 先转换为 float 类型再与 float 变量 fval 相乘,相乘的结果将转换为 double 类型以赋予 double 变量 dval;
- (d):从左往右,int 变量 ival 先转换为 float 类型与 fval 求和,再将和转换为 double 类型与 dval 求和,最终的结果将转换为 char 类型赋予 char 变量 cval。
练习 4.36
假设 i 是 int 类型,d 是 double 类型,书写表达式 i *= d 使其执行整数类型的乘法而非浮点类型的乘法。
解答
任何具有明确意义的类型转换,只要不包括底层 const,都可以使用 static_cast。因此,可以使用 static_cast 将 double 变量 d 强制转换为 int 类型,从而参与 int 类型乘法。实现如下:
i *= static_cast<int>(d);
练习 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;
解答
利用 static_cast 执行强制类型转换,对于底层 const 则使用 const_cast。改写如下:
(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);
练习 4.38
说明下面这条表达式的含义。
double slope = static_cast<double>(j/i);
解答
上式将 j / i 的结果强制转换为 double 类型,然后赋予 double 变量 slope。
此外,注意若 i 和 j 都是 int 变量,则 j / i 的结果仍是 int,即便除不尽也会截断商的小数部分,只保留商的整数部分,最后再转换成 double 类型。