4.1
【出题思路】
本题旨在考查几种常见算术运算符的优先级和结合律。
【解答】
在算术运算符中,乘法和除法的优先级相同,且均高于加减法的优先级。因此上式的计算结果应该是 105,在编程环境中很容易验证这一点。
4.2
【出题思路】
C++定义了各种各样的运算符,这些运算符根据运算意义的不同被划分成不同的优先级,本题意在让用户先区分出表达式中各个运算符的优先级关系,然后用括号的方式表示哪个运算符先执行。
【解答】
在本题涉及的运算符中,优先级最高的是成员选择运算符和函数调用运算符,其次是解引用运算符,最后是加法运算符。因此添加括号后的等价的式子是:
#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
using namespace std;
int main() {
vector<int> vec;
srand((unsigned) time(NULL));
cout << "系统自动为向量生成一组元素......" << endl;
for (int i = 0; i != 10; i++)
vec.push_back(rand() % 100);
cout << "生成的向量数据是:" << endl;
for (auto c : vec)
cout << c << " ";
cout << endl;
cout << "验证添加的括号是否正确:" << endl;
cout << "*vec.begin() 的值是:" << *vec.begin() << endl;
cout << "*(vec.begin()) 的值是:" << *(vec.begin()) << endl;
cout << "*vec.begin() + 1 的值是:" << *vec.begin() + 1 << endl;
cout << "(*(vec.begin())) + 1 的值是:" << (*(vec.begin())) + 1 << endl;
}
// 运行结果
系统自动为向量生成一组元素......
生成的向量数据是:
52 51 75 33 32 56 43 55 95 60
验证添加的括号是否正确:
*vec.begin() 的值是:52
*(vec.begin()) 的值是:52
*vec.begin() + 1 的值是:53
(*(vec.begin())) + 1 的值是:53
Process finished with exit code 0
4.3
【出题思路】
本题的关键是考查求值顺序对表达式求值过程的影响。
【解答】
正如题目所说,C++只规定了非常少的二元运算符(逻辑与运算符、逻辑或运算符、逗号运算符)的求值顺序,其他绝大多数二元运算符的求值顺序并没有明确规定。这样做提高了代码的生成效率,但是可能引发潜在的缺陷。
关键是缺陷的风险有多大?我们知道,对于没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为;而如果运算对象彼此无关,它们既不会改变同一对象的状态也不执行 IO 任务,则函数的调用顺序不受限制。
就作者的观点而言,这样的做法在一定程度上是可以接受的,前提是在编写程序时注意以下两点:
- 拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求;
- 一旦改变了某个运算对象的值,在表达式的其它地方就不要再使用这个运算对象了。
4.4
【出题思路】
本题旨在考查算术运算符的优先级和结合律,乘法和除法的优先级相同,且均高于加减法的优先级;算术运算符都满足左结合律,意味着当优先级相同时按照从左往右的顺序进行结合。
【解答】
添加括号之后的形式应该是 ((12 / 3) * 4) + (5 * 15) + ((24 % 4) / 2)
,求值的过程是:首先计算 12 / 3 得到 4,接着 4 * 4 得到 16,同时计算 5 * 15 得到 75,计算 24 % 4 得到 0,接着计算 0 / 2 得到 0,最后执行加法运算 16 + 75 + 0 得到 91。最终的计算结果是 91。
4.5
【出题思路】
本题主要考查乘除法、取余的计算规则。
【解答】
本题用到两条典型的算术运算规则。
一是除法:整数相除结果还是整数,如果商含有小数部分,直接弃除。尤其当除法的两个运算对象的符号不同时,商为负,C++11 新标准规定商一律向 0 取整。
二是取余:如果取余的两个运算对象的符号不同,则负号所在的位置不同运算结果也不相同,m % (-n)
等于 m % n
,(-m) % n
等于 - (m % n)
。
因此,本题的求值结果是:
(a)- 86
(b)- 18
(c)0
(d)- 2
4.6
【出题思路】
根据奇数和偶数的定义可知,能被 2 整除的数是偶数,不能被 2 整除的数是奇数。
【解答】
下面的表达式可以用于确定一个整数是奇数还是偶数,假设该整数名为 num,则表达式 num % 2 == 0
为真时 num 是偶数,该表达式为假时 num 是奇数。
4.7
【出题思路】
当计算的结果超出类型所能表示的范围时,产生溢出。
【解答】
溢出是一种常见的算术运算错误。因为在计算机中存储某种类型的内存空间有限,所以该类型的表示能力(范围)也是有限的,当计算的结果值超出这个范围时,就会产生未定义的数值,这种错误称为溢出。
假设编译器规定 int 占 32 位,则下面的 3 条表达式都将产生溢出错误:
int i = 2147483647 + 1;
int j = -100000 * 300000;
int k = 2019 * 2019 * 2019 * 2019;
编程测试如下所示:
#include <iostream>
using std::cout;
using std::endl;
int main() {
int i = 2147483647 + 1;
int j = -100000 * 300000;
int k = 2019 * 2019 * 2019 * 2019;
cout << "i: " << i << endl;
cout << "j: " << j << endl;
cout << "k: " << k << endl;
}
// 运行结果
i: -2147483648
j: 64771072
k: -509465903
Process finished with exit code 0
显然这与我们的预期不相符,是溢出之后产生的错误结果。
4.8
【出题思路】
逻辑与、逻辑或执行短路求值,相等性运算符则正常计算两个运算对象的值。
【解答】
对于逻辑与运算符来说,当且仅当两个运算对象都为真时结果为真;对于逻辑或运算符来说,只要两个运算对象中的一个为真结果就为真。
逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略就是短路求值。其策略是:对于逻辑与运算符来说,当且仅当左侧运算对象为真时才计算右侧运算对象;对于逻辑或运算符来说,当且仅当左侧运算对象为假时才计算右侧运算对象。
值得注意的是,逻辑与运算符和逻辑或运算符是 C++ 中仅有的几个规定了求值顺序的运算符。相等性运算符的两个运算对象都需要求值,C++ 没有规定其求值顺序。
4.9
【出题思路】
本题旨在考查指针和解引用运算符作为 if 条件的用法。
【解答】
cp 是指向字符串的指针,因此上式的条件部分含义是首先检查指针 cp 是否有效。如果 cp 为空指针或无效指针,则条件不满足。如果 cp 有效,即 cp 指向了内存中的某个有效地址,继续解引用指针 cp 并检查 cp 所指对象是否为空字符 \0
,如果 cp 所指的对象不是空字符则条件满足;否则不满足。
在本例中,显然初始化状态下 cp 指向了字符串的首字符,是有效的;同时当前 cp 所指的对象是字符 H
,不是空字符,所以 if 的条件部分为真。
4.10
【出题思路】
综合运用逻辑与运算符及相等性运算符,注意在本题中利用了逻辑与运算符的短路求值特性,其左侧运算对象是为了确保右侧运算对象求值过程的正确性和安全性。
【解答】
最简洁的形式是:
while (cin >> num && num != 42)
该语句首先检查从输入流读取数据是否正常,然后判断当前输入的数字是否是 42,遇到 42 则条件不满足,推出循环。还有一种形式也可以实现同样的目的:
int num;
while (cin >> num) {
if (num == 42)
break;
// 其他操作
}
4.11
【出题思路】
本题旨在考查关系运算符的用法。
【解答】
要想用一条表达式测试 a、b、c、d 的关系,并确保 a 大于 b、b 大于 c、c 大于 d,应该写成:
a > b && b > c && c > d
切勿写成:
a > b > c > d
因为关系运算符满足左结合律且运算的结果是布尔值,所以把几个关系运算符连写在一起必然会产生意想不到的结果。a > b > c > d 的实际求值过程是先判断 a > b 是否成立,成立则为 1,不成立则为 0;接着用这个布尔值(1 或 0)与 c 比较,所得的结果仍然是一个布尔值;最后再用刚刚得到的布尔值与 d 进行比较。显然这一过程与用户的书写原意背道而驰。
4.12
【出题思路】
需要明确两种关系运算符 != 和 < 的优先级关系,这是解答本题的关键。
【解答】
C++ 规定 <、<=、>、>= 的优先级高于 == 和 !=,因此上式的求值过程等同于 i != (j < k)
,意即先比较 j 和 k 的大小,得到的结果是一个布尔值(1 或 0);然后判断 i 的值与之是否相等。
4.13
【出题思路】
本题涉及的知识点有两个:第一,如果赋值运算符左右两个运算对象的类型不同,则右侧运算对象转换成左侧运算对象的类型;第二,赋值运算符满足右结合律。
【解答】
由题意可知,(a)式的含义是先把 3.5 赋值给整数 i,此时发生了自动类型转换,小数部分被舍弃,i 的值为 3;接着 i 的值再赋给双精度浮点数 d,所以 d 的值也是 3。
(b)式的含义是先把 3.5 赋值给双精度浮点数 d,因此 d 的值是 3.5;接着 d 的值再赋给整数 i,此时发生了自动类型转换,小数部分被舍弃,i 的值为 3。
编程测试如下所示:
#include <iostream>
using std::cout;
using std::endl;
int main() {
int i;
double d;
d = i = 3.5;
cout << "d = " << d << " " << "i = " << i << endl;
i = d = 3.5;
cout << "i = " << i << " " << "d = " << d << endl;
return 0;
}
// 运行结果
d = 3 i = 3
i = 3 d = 3.5
Process finished with exit code 0
4.14
【出题思路】
本题涉及的知识点有两个:第一,赋值运算符的左侧运算对象必须是左值,右侧运算对象可以是左值,也可以是右值;第二,赋值运算符与相等性运算符在作为 if 语句的条件时含义不同。
【解答】
第一条语句发生编译错误,因为赋值运算符的左侧运算对象必须是左值,字面值常量 42 显然不是左值,不能作为左侧运算对象。
第二条语句从语法上来说是正确的,但是与程序的原意不符。程序的原意是判断 i 的值是否是 42,应该写成 i == 42;而 i = 42 的意思是把 42 赋值给 i,然后判断 i 的值是否为真。因为所有非 0 整数转换成布尔值时都对应 true,所以该条件是恒为真的。
4.15
【出题思路】
赋值运算符满足右结合律,自右向左分析赋值操作的含义。
【解答】
该赋值语句是非法的,虽然连续赋值的形式本身并没有错,但是参与赋值的几个变量类型不同。其中,dval 是双精度浮点数,ival 是整数,pi 是整型指针。
自右向左分析赋值操作的含义,pi = 0 表示 pi 是一个空指针,接下来 ival = pi 试图把整型指针的值赋给整数,这是不符合语法规范的操作,无法编译通过。稍作调整,就可以把上述程序改为合法。
double dval;
int ival, *pi;
dval = ival = 0;
pi = 0;
4.16
【出题思路】
本题考查运算符优先级及赋值表达式作为 if 条件的含义。
【解答】
(a)的原意是把 getPtr() 得到的指针赋值给 p,然后判断 p 是否是一个空指针,但上述表达式的实际执行结果与之相距甚远。因为赋值运算符的优先级低于不相等运算符,所以真正的表达式求值过程是先判断 getPtr() 的返回值是否为空指针,若为空指针,则 getPtr() != 0
得到的布尔值为 0,然后将 0 赋值给 p,即 p = 0;否则,若为非空指针,则 getPtr() != 0
得到的布尔值为 1,然后将 1 赋值给 p,即 p = 1。最后以 p 的值作为 if 语句的条件。要想符合原意,应该修改为:
if ((p = getPtr()) != 0)
(b)的原意是判断 i 的值是否是 1024,但上述表达式实际上是把 1024 赋值给 i,然后以 i 作为 if 语句的条件。因为所有非 0 整数转换成布尔值时都对应 true,所以该条件是恒为真的。
4.17
【出题思路】
C++ 实现了两种递增(递减)运算符:即前置版本和后置版本,二者的工作机理有所区别,一般来说前置版本是更好的选择。
【解答】
递增和递减运算符有两种形式:前置版本和后置版本。前置版本首先将运算对象加 1(或减 1),然后把改变后的对象作为求值结果。后置版本也将运算对象加 1(或减 1),但是求值结果是运算对象改变之前那个值的副本。这两种运算符必须作用于左值运算对象。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
我们的建议是,除非必须,否则不用递增(递减)运算符的后置版本。前置版本的递增运算符避免了不必要的工作,它把值加 1 后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便返回这个未修改的内容。如果我们不需要修改之前的值,那么后置版本的操作就是一种浪费。
对于整数和指针类型来说,编译器可能对这种额外的工作进行了一定的优化,但是对于相对复杂的迭代器类型来说,这种额外的工作就消耗巨大了。建议养成使用前置版本的习惯,这样不仅不需要担心性能问题,而且更重要的是写出的代码会更符合编程人员的初衷。
4.18
【解题思路】
前置递增运算符先将运算对象加 1,然后把改变后的对象本身作为求值结果;后置递增运算符也将运算对象加 1,但是求值结果是运算对象改变之前那个值的副本。
简而言之,如果一条表达式中出现了递增运算符,则其计算规律是:++ 在前,先加 1,后参加运算;++ 在后,先参与运算,后加 1。
【解答】
基于上述分析,本题不应该把 while 循环的后置递增运算符改为前置递增运算符。如果这样做了,会产生两个错误结果:一是无法输出 vector 对象的第一个元素;二是当所有元素都不为负时,移动到最后一个元素的地方,程序试图继续向前移动迭代器并解引用一个根本不存在的元素。
4.19
【出题思路】
本题旨在考查递增运算符与关系运算符、逻辑运算符、解引用运算符的优先级关系,读者也需要理解后置递增运算符的计算规则。
【解答】
(a)的含义是先判断指针 ptr 是否为空,如果不为空,继续判断指针 ptr 所指的整数是否为非 0 数。如果非 0,则该表达式的最终求值结果为真;否则为假。最后把指针 ptr 向后移动一位。该表达式从语法上分析是合法的,但是最后的指针移位操作不一定有意义。如果 ptr 所指的是整型数组中的某个元素,则 ptr 可以按照预期移动到下一个元素。如果 ptr 所指的只是一个独立的整数变量,则移动指针操作将产生未定义的结果。
(b)的含义是先检查 ival 的值是否是非 0,如果非 0 继续检查 (ival + 1) 的值是否是非 0。只有当两个值都是非 0 值时,表达式的求值结果为真;否则为假。在 4.1.3 节中我们学习到,如果二元运算符的两个运算对象涉及同一个对象并改变对象的值,则这是一种不好的程序写法,应该改写。所以,按照程序的原意,本式应该改写成 ival && (ival + 1)
。
(c)的含义是比较 vec[ival]
和 vec[ival + 1]
的大小,如果前者较小则求值结果为真,否则为假。与(b)式一样,本式也出现了二元运算符的两个运算对象涉及同一个对象并改变对象值的情况,应该改写为 vec[ival] <= vec[ival + 1]
。
4.20
【出题思路】
本题旨在考查成员访问运算符与递增运算符和解引用运算符的优先级关系。
【解答】
(a)是合法的,后置递增运算符的优先级高于解引用运算符,其含义是解引用当前迭代器所处位置的对象内容,然后把迭代器的位置向后移动一位。
(b)是非法的,解引用 iter 得到 vector 对象当前的元素,结果是一个 string,显然 string 没有后置递增操作。
(c)是非法的,解引用运算符的优先级低于点运算符,所以该式先计算 iter.empty()
,而迭代器并没有定义 empty 函数,所以无法通过编译。
(d)是合法的,iter->empty();
等价于 (*iter).empty();
。解引用迭代器得到迭代器当前所指的元素,结果是一个 string,显然字符串可以判断是否为空,empty 函数在此有效。
(e)是非法的,前置递增运算符的优先级和解引用运算符的优先级相同,但表达式的结合性是自右向左(右结合律)。该式先解引用 iter,得到迭代器当前所指的元素,结果是一个 string,显然 string 没有前置递增操作。
(f)是合法的,后置递增运算符的优先级和成员访问运算符的优先级相同,但表达式的结合性是从左往右(左结合律)。iter++->empty();
等价于 (*iter++).empty();
。含义是解引用迭代器当前位置的对象内容,得到一个字符串,判断该字符串是否为空,然后把迭代器向后移动一位。
4.21
【出题思路】
条件运算符使得我们可以把简单的 if-else 结构嵌入到表达式中,条件表达式可以作为赋值运算符的右侧运算对象。
【解答】
满足题意的程序如下所示:
#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
using namespace std;
int main() {
vector<int> vInt;
const int sz = 10; // 使用 sz 作为数组的维度
srand((unsigned) time(NULL)); // 生成随机数种子
// 使用普通 for 循环为数组赋初值
cout << "数组的初始值是:" << endl;
for (int i = 0; i != sz; ++i) {
vInt.push_back(rand() % 100); // 生成 100 以内的随机数
cout << vInt[i] << " "; // 使用下标运算符输出数组内容
}
cout << endl;
// 使用范围 for 循环把数组中的奇数翻倍
for (auto &val : vInt) {
val = (val % 2 != 0) ? val * 2 : val; // 条件表达式
}
// 使用普通 for 循环和迭代器输出数组的当前值
cout << "调整后的数组是:" << endl;
for (auto it = vInt.cbegin(); it != vInt.cend(); ++it) {
cout << *it << " ";
}
cout << endl;
return 0;
}
// 运行结果
数组的初始值是:
89 75 55 1 45 73 90 21 38 82
调整后的数组是:
178 150 110 2 90 146 90 42 38 82
Process finished with exit code 0
4.22
【出题思路】
条件表达式的作用与 if-else 语句类似。条件表达式可以嵌套,但是嵌套的层数越少越好,如果嵌套层数过多,则代码的可读性会变得非常差。
【解答】
使用条件运算符实现的程序如下所示:
#include <iostream>
#include <string>
using namespace std;
int main() {
string finalgrade;
int grade;
cout << "请输入您要检查的成绩:" << endl;
// 确保输入的成绩合法
while (cin >> grade && grade >= 0 && grade <= 100) {
// 使用三层嵌套的条件表达式
finalgrade = (grade > 90) ? "high pass"
: (grade > 75) ? "pass"
: (grade > 60) ? "low pass" : "fail";
cout << "该成绩所处的档次是:" << finalgrade << endl;
cout << "请输入您要检查的成绩:" << endl;
}
cout << finalgrade << endl;
}
// 运行结果
请输入您要检查的成绩:
77
该成绩所处的档次是:pass
请输入您要检查的成绩:
66
该成绩所处的档次是:low pass
请输入您要检查的成绩:
55
该成绩所处的档次是:fail
请输入您要检查的成绩:
使用 if 语句实现的程序如下所示:
#include <iostream>
#include <string>
using namespace std;
int main() {
string finalgrade;
int grade;
cout << "请输入您要检查的成绩:" << endl;
// 确保输入的成绩合法
while (cin >> grade && grade >= 0 && grade <= 100) {
// 使用 if 语句实现
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;
cout << "请输入您要检查的成绩:" << endl;
}
cout << finalgrade << endl;
}
// 运行结果
请输入您要检查的成绩:
77
该成绩所处的档次是:pass
请输入您要检查的成绩:
66
该成绩所处的档次是:low pass
请输入您要检查的成绩:
55
该成绩所处的档次是:fail
请输入您要检查的成绩:
4.23
【出题思路】
条件运算符的优先级非常低,因此在使用条件运算符构成复合表达式时必须在适当的地方添加括号。
【解答】
题目中的几个运算符的优先级次序从高到低是加法运算、相等运算符、条件运算符和赋值运算符,因此式子的求值过程是先把 s 和 s[s.size() - 1] 相加得到一个新字符串,然后该字符串与字符 s
比较是否相等,这是一个非法操作,并且与程序的原意不符。
要想实现程序的原意,即先判断字符串 s 的最后一个字符是否是 s
,如果是,什么也不做;如果不是,在 s 的末尾添加一个字符 s
,我们应该添加括号强制限定运算符的执行顺序。
string p1 = s + (s[s.size() - 1] == 's' ? "" : "s");
4.24
【出题思路】
假设条件运算符满足的是左结合律,即嵌套的条件运算符从左向右求值。
【解答】
原文的程序是:
finalgrade = (grade > 90) ? "high pass"
: (grade < 60) ? "fail" : "pass";
根据左结合律的含义,该式等价于:
finalgrade = ((grade > 90) ? "high pass" : (grade < 60)) ? "fail" : "pass";
先考查 grade > 90 是否成立,如果成立,第一个条件表达式的值为 “high pass”;如果不成立,第一个条件表达式的值为 grade < 60。这条语句是无法编译通过的,因为条件运算符要求两个结果表达式的类型相同或者可以相互转化。即使假设语法上通过了,也就是说,第一个条件表达式的求值结果分为 3 种,分别是 “high pass”、1 和 0。接下来根据第一个条件表达式的求值结果解第二个条件表达式,求值结果是 “fail” 或 “pass”。上述求值过程显然与我们的期望是不符的。
4.25
【出题思路】
本题旨在考查位运算符的优先级和计算规则。
【解答】
在位运算符中,运算符 ~
高于 <<
,因此先对 q 按位求反,因为位运算符的运算对象应该是整数类型,所以字符 q
首先转换为整数类型。如题所示,char 占 8 位而 int 占 32 位,所以字符 q
转换后得到 00000000 00000000 00000000 01110001
,按位求反得到 11111111 11111111 11111111 10001110
;接着执行移位操作,得到 11111111 11111111 11100011 10000000
。
C++规定整数按照其补码形式存储,对上式求补,得到 10000000 00000000 00011100 10000000
,即最终结果的二进制形式,转换成十进制形式是 -7296。
4.26
【出题思路】
结合本题的题意,选择适当的数据类型。
【解答】
根据题目的要求,班级中有 30 个学生,我们用一个二进制位代表某个学生在一次测验中是否通过,则原书中使用 unsigned long
作为 quiz1 的数据类型是恰当的,因为 C++ 规定 unsigned long
在内存中至少占 32 位,这样就足够存放 30 个学生的信息。
如果使用 unsigned int
作为 quiz1 的类型,则由于 C++ 规定 unsigned int
所占空间的最小值是 16,所以在很多机器环境中,该数据类型不足以存放全部学生的信息,从而造成了信息丢失,无法完成题目要求的任务。
4.27
【出题思路】
读者需要掌握按位与和按位或的计算方法,同时注意区分按位与和逻辑与、按位或和逻辑或。
【解答】
ul1 转换为二进制形式是:00000000 00000000 00000000 00000011
,ul2 转换为二进制形式是:00000000 00000000 00000000 00000111
。各个式子的求值结果分别是:
(a)按位与,结果是:00000000 00000000 00000000 00000011
,即 3。
(b)按位或,结果是:00000000 00000000 00000000 00000111
,即 7。
(c)逻辑与,所有非 0 整数对应的布尔值都是 true,所以该式等价于 true && true,结果是 true。
(d)逻辑或,所有非 0 整数对应的布尔值都是 true,所以该式等价于 true || true,结果是 true。
4.28
【出题思路】
运用 sizeof 运算符输出内置类型的大小,注意 C++ 只规定了每种类型所占的最小空间,类型实际占多少空间依赖于具体实现。
【解答】
满足题意的程序如下所示:
#include <iostream>
using namespace std;
int main() {
cout << "类型名称\t" << "所占空间" << endl;
cout << "bool\t\t" << sizeof(bool) << endl;
cout << "char\t\t" << sizeof(char) << endl;
cout << "wchar_t\t\t" << sizeof(wchar_t) << endl;
cout << "char16_t\t" << sizeof(char16_t) << endl;
cout << "char32_t\t" << sizeof(char32_t) << endl;
cout << "short\t\t" << sizeof(short) << endl;
cout << "int\t\t\t" << sizeof(int) << endl;
cout << "long\t\t" << sizeof(long) << endl;
cout << "long long\t" << sizeof(long long) << endl;
cout << "float\t\t" << sizeof(float) << endl;
cout << "double\t\t" << sizeof(double) << endl;
cout << "long double\t" << sizeof(long double) << endl;
return 0;
}
在我的编译环境中,输出结果是:
// 运行结果
类型名称 所占空间
bool 1
char 1
wchar_t 4
char16_t 2
char32_t 4
short 2
int 4
long 8
long long 8
float 4
double 8
long double 16
Process finished with exit code 0
sizeof 运算符的求值结果是类型在内存中所占的字节数。正常情况下,求值结果应该至少等于 C++ 规定的最小值,依赖于机器的不同,部分类型实际占用的空间会大于这个最小值。例如,在作者的编译环境中,int 占 4 字节,超过了 C++ 规定的 2 字节。
4.29
【出题思路】
当 sizeof 的运算对象是数组名、数组内容、指针时,应该了解其区别。
【解答】
sizeof(x)
的运算对象 x 是数组的名字,求值结果是整个数组所占空间的大小,等价于对数组中所有的元素各执行一次 sizeof 运算并对所得结果求和。尤其需要注意,sizeof 运算符不会把数组转换成指针来处理。在本例中,x 是一个 int 数组且包含 10 个元素,所以 sizeof(x) 的求值结果是 10 个 int 值所占的内存空间总和。sizeof(*x)
的运算对象 *x 是一条解引用表达式,此处的 x 既是数组的名称,也表示指向数组首元素的指针,解引用该指针得到指针所指的内容,在本例中是一个 int。所以,sizeof(*x) 在这里等价于 sizeof(int),即 int 所占的内存空间。sizeof(x) / sizeof(*x)
可以理解为数组 x 所占的全部空间除以其中一个元素所占的空间,得到的结果应该是数组 x 的元素总数。实际上,因为 C++ 的内置数组并没有定义成员函数 size(),所以通常无法直接得到数组的容量。本题所示的方法是计算得到数组容量的一种常规方法。sizeof(p)
的运算对象 p 是一个指针,求值结果是指针所占的空间大小。sizeof(*p)
的运算对象 *p 是指针 p 所指的对象,即 int 变量 x,所以求值结果是 int 值所占的空间大小。
在我的编译环境中,int 占 4 字节,指针占 8 字节,所以本题程序输出结果是:
10
2
编程测试如下所示:
#include <iostream>
using namespace std;
int main() {
int x[10];
int *p = x;
cout << sizeof(x) << "\t" << sizeof(*x) << "\n";
cout << sizeof(x) / sizeof(*x) << endl;
cout << sizeof(p) << '\t' << sizeof(*p) << '\n';
cout << sizeof(p) / sizeof(*p) << endl;
return 0;
}
// 运行结果
40 4
10
8 4
2
Process finished with exit code 0
4.30
【出题思路】
本题考查 sizeof 运算符与其他运算符的优先级关系。
【解答】
(a)的含义是先求变量 x 所占空间的大小,然后与变量 y 的值相加;因为 sizeof 运算符的优先级高于加法运算符的优先级,所以如果想求表达式 x+y 所占的内存空间,应该改为 sizeof(x + y)。
(b)的含义是先定位到指针 p 所指的对象,然后求该对象中名为 mem 的数组成员第 i 个元素的尺寸。因为成员选择运算符的优先级高于 sizeof 的优先级,所以本例无须添加括号。
(c)的含义是先求变量 a 在内存中所占空间的大小,再把求得的值与变量 b 的值比较。因为 sizeof 运算符的优先级高于关系运算符的优先级,所以如果想求表达式 a < b 所占的内存空间,应该改为 sizeof(a < b)。
(d)的含义是求函数 f() 返回值所占内存空间的大小,因为函数调用运算符的优先级高于 sizeof 的优先级,所以本例无须添加括号。
4.31
【出题思路】
需要掌握前置版本和后置版本的联系和区别。
【解答】
本题从程序运行结果来说,使用前置版本或后置版本是一样的,这是因为递增递减运算符与真正使用这两个变量的语句位于不同的表达式中,所以不会有什么影响。
使用后置版本重写的程序是:
vector<int>::size_type cnt = ivec.size();
// 将从 size 到 1 的值赋给 ivec 的元素
for (vector<int>::size_type ix = 0; ix != ivec.size(); ix++, cnt--)
ivec[ix] = cnt;
根据 4.5 节的介绍我们知道,除非必须,否则不用递增(递减)运算符的后置版本。前置版本的递增运算符避免了不必要的工作,它把值加 1 后直接返回改变了的运算对象本身。与之相比,后置版本需要将原始值存储下来以便返回这个未修改的内容。如果我们不需要修改之前的值,那么后置版本的操作就是一种浪费。
就本题而言,使用前置版本是更好的选择。
4.32
【出题思路】
考查逗号运算符的计算规则。
【解答】
首先定义了一个常量表达式 size,它的值是 5;接着以 size 作为维度创建一个整型数组 ia,5 个元素分别是 1~5。
for 语句头包括三部分:第一部分定义整型指针指向数组 ia 的首元素,并且定义了一个整数 ix,赋给它初始值 0;第二部分判断循环终止的条件,当 ix 没有达到 size 同时指针 ptr 没有指向数组最后一个元素的下一位置时,执行循环体;第三部分令变量 ix 和指针 ptr 分别执行递增操作。
4.33
【出题思路】
本题的关键是理解条件运算符和逗号运算符的优先级关系。
【解答】
C++ 规定条件运算符的优先级高于逗号运算符,所以 someValue ? ++x, ++y : --x, --y;
实际上等价于 (someValue ? ++x, ++y : --x), --y;
。它的求值过程是,首先判断 someValue 是否为真,如果为真,依次执行 ++x 和 ++y,最后执行 --y ;如果为假,执行 --x 和 --y。
#include <iostream>
using namespace std;
int main() {
int x = 10, y = 20;
// 检验条件为真的情况
bool someValue = true;
someValue ? ++x, ++y : --x, --y;
cout << x << endl;
cout << y << endl;
cout << someValue << endl;
x = 10, y = 20;
// 检验条件为假的情况
someValue = false;
someValue ? ++x, ++y : --x, --y;
cout << x << endl;
cout << y << endl;
cout << someValue << endl;
return 0;
}
// 运行结果
11
20
1
9
19
0
Process finished with exit code 0
当 someValue 取值为 true 时,依次执行 ++x、++y、–y ,也就是说,x 的值加 1 变为 11,y 的值先加 1 后减 1 保持不变,还是 20。
当 someValue 取值为 false 时,依次执行 --x、—y,x 和 y 的值各减少 1 变为 9 和 19。
4.34
【解题思路】
读者需要掌握算术转换的规则。
【解答】
(a)if 语句的条件应该是布尔值,因此 float 型变量 fval 自动转换成布尔值,转换规则是所有非 0 值转换为 true,0 转换为 false。
(b)ival 转换成 float,与 fval 求和后所得的结果进一步转换为 double 类型。
(c)cval 执行整型提升转换为 int,与 ival 相乘后所得的结果转换为 double 类型,最后再与 dval 相加。
4.35
【解题思路】
读者需要掌握算术转换的规则。
【解答】
(a)字符 a
提升为 int,与 3 相加所得的结果再转换为 char 并赋给 cval。
(b)ival 转换为 double,与 1.0 相乘的结果也是 double 类型,ui 转换为 double 类型后与乘法得到的结果相减,最终的结果转换为 float 并赋给 fval。
(c)ui 转换为 float,与 fval 相乘的结果转换为 double 类型并赋给 dval。
(d)ival 转换为 float,与 fval 相加所得的结果转换为 double 类型,再与 dval 相加后结果转换为 char 类型。
4.36
【出题思路】
任何具有明确定义的类型转换,只要不包括底层 const,都可以使用 static_cast。
【解答】
使用 static_cast 把 double 类型的变量 d 强制转换成 int 类型,就可以令表达式 i *= d
执行整数类型的乘法。语句的形式应该是:i *= static_cast<int>(d);
4.37
【解题思路】
利用 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
【出题思路】
理解命名的强制类型转换。
【解答】
把 j / i 的值强制类型转换成 double,然后赋值给 slope。请注意,如果 i 和 j 的类型都是 int,则 j / i 的求值结果仍然是 int,即使除不尽也只保留商的整数部分(向零取整),最后再转换成 double 类型。
评论区