sairate 4108f6c42d docs(book): 添加现代 C++教程及相关代码
- 新增现代 C++ 教程的 Preface 章节,包括英文和中文版本
- 添加 C++ Primer 练习代码
- 新增 Learn C++ 教程的 C++ 开发简介章节
- 添加头文件解析文档
- 更新 mkdocs.yml,包含新教程的目录结构
- 修改项目设置,使用 Python 3.10环境
2025-07-06 16:14:43 +08:00

638 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
sort: 4
---
# 表达式
## 练习4.1
表达式`5 + 10 * 20 / 2`的求值结果是多少?
解:
等价于`5 + ((10 * 20) / 2) = 105`
## 练习4.2
根据4.12节中的表,在下述表达式的合理位置添加括号,使得添加括号后运算对象的组合顺序与添加括号前一致。
(a) `*vec.begin()`
(b) `*vec.begin() + 1`
解:
```cpp
*(vec.begin())
(*(vec.begin())) + 1
```
## 练习4.3
C++语言没有明确规定大多数二元运算符的求值顺序,给编译器优化留下了余地。这种策略实际上是在代码生成效率和程序潜在缺陷之间进行了权衡,你认为这可以接受吗?请说出你的理由。
解:
正如题目所说C++只规定了非常少的二元运算符(逻辑与运算符、逻辑或运算符、逗号运算符)的求值顺序,其他绝大多数二元运算符的求值顺序并没有明确规定。这样做提高了代码生成的效率,但是可能引发潜在的缺陷。
关键是缺陷的风险有多大?我们知道,对于没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为;而如果运算对象彼此无关它们既不会改变同一对象的状态也不执行IO任务则函数的调用顺序不受限制。
就作者的观点而言,这样的做法在一定程度上是可以接受的,前提是在编写程序时注意以下两点:一是拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求;二是一旦改变了某个运算对象的值,在表达式的其地方就不要再使用这个运算对象了。
## 练习4.4
在下面的表达式中添加括号,说明其求值过程及最终结果。编写程序编译该(不加括号的)表达式并输出结果验证之前的推断。
`12 / 3 * 4 + 5 * 15 + 24 % 4 / 2`
解:
`((12 / 3) * 4) + (5 * 15) + ((24 % 4) / 2) = 16 + 75 + 0 = 91`
## 练习4.5
写出下列表达式的求值结果。
```cpp
-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
```
## 练习4.6
写出一条表达式用于确定一个整数是奇数还是偶数。
解:
```cpp
if (i % 2 == 0) /* ... */
```
或者
```cpp
if (i & 0x1) /* ... */
```
## 练习4.7
溢出是何含义?写出三条将导致溢出的表达式。
解:
当计算的结果超出该类型所能表示的范围时就会产生溢出。
```cpp
short svalue = 32767; ++svalue; // -32768
unsigned uivalue = 0; --uivalue; // 4294967295
unsigned short usvalue = 65535; ++usvalue; // 0
```
## 练习4.8
说明在逻辑与、逻辑或及相等性运算符中运算对象的求值顺序。
解:
对于逻辑与运算符来说,当且仅当两两个运算对象都为真时结果为真;对于逻辑或运算符来说只要两个运算对象中的1个为真结果就为真。
逻辑与运算符和逻辑或运算符都是先先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表过达式的结果时才会计算右侧运算对象的值。这种策略就是短路求值。其策略是:对于干逻辑与运算符来说,当且仅当左侧运算对象为真时才计算右侧运算对象;对于逻转辑或运算符来说,当且仅当左侧运算对象为假时才计算右侧运算对象。
## 练习4.9
解释在下面的`if`语句中条件部分的判断过程。
```cpp
const char *cp = "Hello World";
if (cp && *cp)
```
解:
cp是指向字符串的指针因此上式的条件部分含义是首先检查指针cp是否有效。如果cp为空指针或无效指针则条件不满足。如果cp有效即cp指向了内存中的某个有效地址继续解引用指针cp并检查cp所指的对象是否为空字符'\0'如果cp所指的对象不是空字符则条件满足; 否则不满足。
在本例中显然初始状态下cp指向了字符串的首字符是有效的;同时当前cp所指的对象是字符'H不是空字符所以 if的条件部分为真。
## 练习4.10
`while`循环写一个条件,使其从标准输入中读取整数,遇到`42`时停止。
解:
```cpp
int i;
while(cin >> i && i != 42)
```
## 练习4.11
书写一条表达式用于测试4个值a、b、c、d的关系确保a大于b、b大于c、c大于d。
解:
```cpp
a>b && b>c && c>d
```
## 练习4.12
假设`i``j``k`是三个整数,说明表达式`i != j < k`的含义。
解:
这个表达式等于`i != (j < k)`。首先得到`j < k`的结果为`true``false`,转换为整数值是`1``0`,然后判断`i`不等于`1``0` ,最终的结果为`bool`值。
## 练习4.13
在下述语句中,当赋值完成后 i 和 d 的值分别是多少?
```cpp
int i; double d;
d = i = 3.5; // i = 3, d = 3.0
i = d = 3.5; // d = 3.5, i = 3
```
## 练习4.14
执行下述 if 语句后将发生什么情况?
```cpp
if (42 = i) // 编译错误。赋值运算符左侧必须是一个可修改的左值。而字面值是右值。
if (i = 42) // 所有非0整数转换成布尔值时都对应true该条件恒为真。
```
## 练习4.15
下面的赋值是非法的,为什么?应该如何修改?
```cpp
double dval; int ival; int *pi;
dval = ival = pi = 0;
```
解:
该赋值语句是非法的虽然连续赋值的的形式本身并没有错但是参与赋值的几个变量类型不同。其中dval是双精度浮点数ival是整数pi是整型指针。
自右向左分析赋值操作的含义pi=0天表示pi是一个空指针接下来ival=pi试图把整型指针的值赋给整数这是不符合合语法规范的操作无法编译通过。稍作调整就可以把上述程序改为合法。
```cpp
dval = ival = 0;
pi = 0;
```
## 练习4.16
尽管下面的语句合法,但它们实际执行的行为可能和预期并不一样,为什么?应该如何修改?
```cpp
if (p = getPtr() != 0)
if (i = 1024)
```
解:
(a)的原意是把getPtr()得到的指针赋值给p然后判断p是否是一个空指针但上述表达式的实际执行结果与之相距甚这元。因为赋值运算符的优先级低于不相等运算符所以真正的表达式求值过程是先判到断getPtr()的返回值是否为空指针如果是则p=0否则p=1最后以p的值作为if语句的条件。要想符合原意应该修改为:
```cpp
if ((p=getPtr()) != 0)
if (i == 1024)
```
## 练习4.17
说明前置递增运算符和后置递增运算符的区别。
解:
递增和递减运算符有两种形式:前置版本和后置版本。前置版本首先将运算对象加1(或减 1),然后把改变后的对象作为求值结果。后置版本也将运算对象加(或减1),但是求值结果是运算对象改变之前那个值的副本。这两种运算符必须作用于左值运算对象。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
我们的建议是,除非必须,否则不用递增(递减)运算符的后置版本。前置版本的递增运算符避免了不必要的工作,它把值加 1 后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改之前的值,那么后置版本的操作就是一种浪费。
对于整数和指针类型来说,编译器可能对这种额外的工作进行了一定的优化;
但是对于相对复杂的迭代器类型来说,这种额外的工作就消耗巨大了。建议养成使用前置版本的习惯,这样不仅不需要担心性能问题,而且更重要的是写出的代码会更符合编程人员的初衷。
## 练习4.18
如果132页那个输出`vector`对象元素的`while`循环使用前置递增运算符,将得到什么结果?
解:
【出题思路】
前置递增运算符先将运算对象加1 然后把改变后的对象作为求值结果;后置递增运算符也将运算对象加1但是求值结果是运算对象改变之前那个值的副本。
简言之,如果在一条表达式中出现了 递增运算符,则其计算规律是:++在前, 先加1后参与运算;++在后,先参与运 算后加1。
【解答】
基于上述分析本题不应该把whil e循环的后置递增运算符改为前置递增运算符。如果这样做了会产生两个错误结果:一是无法输出vector对象的第一个元素;二是当所有元素都不为负时,移动至到最后一个元素的地方,程序试图继续向前移动迭代器并解引用一个根本不存在的元素。
## 练习4.19
假设`ptr`的类型是指向`int`的指针、`vec`的类型是`vector``ival`的类型是`int`,说明下面的表达式是何含义?如果有表达式不正确,为什么?应该如何修改?
```cpp
(a) ptr != 0 && *ptr++
(b) ival++ && ival
(c) vec[ival++] <= vec[ival]
```
解:
- (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[ivall和vec[ival+1]`的大小,如果前者较小则求值结果为真,否则为假。与(b)式一样,本式也出现了二元运算符的两个运算对象涉及同一个对象并改变对象值的情况,应该改写为`vec[ivall <=vec[ival +11`
## 练习4.20
假设`iter`的类型是`vector::iterator`, 说明下面的表达式是否合法。如果合法,表达式的含义是什么?如果不合法,错在何处?
```cpp
(a) *iter++;
(b) (*iter)++;
(c) *iter.empty();
(d) iter->empty();
(e) ++*iter;
(f) iter++->empty();
```
解:
- (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
编写一段程序,使用条件运算符从`vector`中找到哪些元素的值是奇数,然后将这些奇数值翻倍。
解:
```cpp
#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
using namespace std;
int main() {
vector<int> vInt;
const int sz = 10;
srand((unsigned ) time(NULL));
cout <<"数组的初始值为" << endl;
for(int i = 0; i != sz; ++i) {
vInt.push_back(rand() % 100);
cout << vInt[i] << " ";
}
cout <<endl;
for(auto &val : vInt) {
val = (val % 2 == 0) ? val : val * 2;
}
cout <<"最终数组为" << endl;
for(auto it = vInt.cbegin(); it != vInt.cend(); ++it) {
cout << *it << " ";
}
cout <<endl;
return 0;
}
```
## 练习4.22
本节的示例程序将成绩划分为`high pass``pass``fail` 三种,扩展该程序使其进一步将 60 分到 75 分之间的成绩设定为`low pass`。要求程序包含两个版本一个版本只使用条件运算符另一个版本使用1个或多个`if`语句。哪个版本的程序更容易理解呢?为什么?
解:
```cpp
#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
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;
// -------------------------
if (grade > 90) cout << "high pass";
else if (grade < 60) cout << "fail";
else if (grade < 75) cout << "low pass";
else cout << "pass";
cout << endl;
return 0;
}
```
第二个版本容易理解。当条件运算符嵌套层数变多之后,代码的可读性急剧下降。而`if else`的逻辑很清晰。
## 练习4.23
因为运算符的优先级问题下面这条表达式无法通过编译。根据4.12节中的表指出它的问题在哪里?应该如何修改?
```cpp
string s = "word";
string pl = s + s[s.size() - 1] == 's' ? "" : "s" ;
```
解:
加法运算符的优先级高于条件运算符。因此要改为:
```cpp
string pl = s + (s[s.size() - 1] == 's' ? "" : "s") ;
```
## 练习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"`。这样就出现了自相矛盾的逻辑。
## 练习4.25
如果一台机器上`int`占32位、`char`占8位用的是`Latin-1`字符集,其中字符`'q'` 的二进制形式是`01110001`,那么表达式`~'q' << 6`的值是什么?
解:
首先将`char`类型提升为`int`类型,即`00000000 00000000 00000000 01110001`然后取反再左移6位结果是-7296。
## 练习4.26
在本节关于测验成绩的例子中,如果使用`unsigned int` 作为`quiz1` 的类型会发生什么情况?
解:
在有的机器上,`unsigned int` 类型可能只有 16 位,因此结果是未定义的。
## 练习4.27
下列表达式的结果是什么?
```cpp
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
## 练习4.28
编写一段程序,输出每一种内置类型所占空间的大小。
解:
```cpp
#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
```
## 练习4.29
推断下面代码的输出结果并说明理由。实际运行这段程序,结果和你想象的一样吗?如不一样,为什么?
```cpp
int x[10]; int *p = x;
cout << sizeof(x)/sizeof(*x) << endl;
cout << sizeof(p)/sizeof(*p) << endl;
```
解:
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++的内置数组并没有定义成员函数size0),所以通常无法直接得到数组的容量。本题所示的方法是计算得到数组容量的一种常规方法。
sizeof(p)的运算对象p是一个指针求值结果是指针所占的空间大小。
sizeof(*p)的运算对象*p是指针p所指的对象即int变量x所以求值结果是int值所占的空间大小。
在作者的编译环境中int占4字节指针也占4字节所以本题程序的输出结果是:
10
1
## 练习4.30
根据4.12节中的表,在下述表达式的适当位置加上括号,使得加上括号之后的表达式的含义与原来的含义相同。
```cpp
(a) sizeof x + y
(b) sizeof p->mem[i]
(c) sizeof a < b
(d) sizeof f()
```
解:
(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
本节的程序使用了前置版本的递增运算符和递减运算符,解释为什么要用前置版本而不用后置版本。要想使用后置版本的递增递减运算符需要做哪些改动?使用后置版本重写本节的程序。
解:
在4.5节132页已经说过了除非必须否则不用递增递减运算符的后置版本。在这里要使用后者版本的递增递减运算符不需要任何改动。
## 练习4.32
解释下面这个循环的含义。
```cpp
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) { /* ... */ }
```
解:
首先定义一个常量表达式size它的的值是5;接着以size作为维度创建一个整型数组ia5个元素分别是1~5。
for语句头包括三部分:第一部分定义整型指针指向数组ia的首元素并且定义了一个整数ix赋给它初值0;第二部了分判断循环终止的条件当ix没有达到 size同时指针ptr没有指向数组最后一个元素的下一位置时执行循环体;第三部分令变量ix和指针ptr分别执行递增操作。
## 练习4.33
根据4.12节中的表说明下面这条表达式的含义。
```cpp
someValue ? ++x, ++y : --x, --y
```
解:
逗号表达式的优先级是最低的。因此这条表达式也等于:
```cpp
(someValue ? ++x, ++y : --x), --y
```
如果`someValue`的值为真,`x``y` 的值都自增并返回 `y` 值,然后丢弃`y`值,`y`递减并返回`y`值。如果`someValue`的值为假,`x` 递减并返回`x` 值,然后丢弃`x`值,`y`递减并返回`y`值。
## 练习4.34
根据本节给出的变量定义,说明在下面的表达式中将发生什么样的类型转换:
```cpp
(a) if (fval)
(b) dval = fval + ival;
(c) dval + ival * cval;
```
需要注意每种运算符遵循的是左结合律还是右结合律。
解:
```cpp
(a) fval 转换为 `bool` 类型,0值转换为 `true`0转换成 `false`
(b) ival 转换为 `float` ,相加的结果转换为 `double`
(c) cval 转换为 `int` ,然后相乘的结果转换为 `double`
```
## 练习4.35
假设有如下的定义:
```cpp
char cval;
int ival;
unsigned int ui;
float fval;
double dval;
```
请回答在下面的表达式中发生了隐式类型转换吗?如果有,指出来。
```cpp
(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`
## 练习4.36
假设 `i``int`类型,`d``double`类型,书写表达式 `i*=d` 使其执行整数类型的乘法而非浮点类型的乘法。
解:
```cpp
i *= static_cast<int>(d);
```
## 练习4.37
练习4.37
用命名的强制类型转换改写下列旧式的转换语句。
```cpp
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;
```
解:
```cpp
(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
说明下面这条表达式的含义。
```cpp
double slope = static_cast<double>(j/i);
```
解:
`j/i`的结果值转换为`double`,然后赋值给`slope`