【C++ Primer】第四章 表达式 (练习)

C++ Primer 5th 随堂练习

【C++ Primer】第一章 开始 (练习)

【C++ Primer】第二章 变量和基本类型 (练习)

【C++ Primer】第三章 字符串、向量和数组 (练习)

【C++ Primer】第四章 表达式 (练习)

【C++ Primer】第五章 语句 (练习)

【C++ Primer】第六章 函数 (练习)

【C++ Primer】第七章 类 (练习)

【C++ Primer】第八章 IO 库 (练习)

【C++ Primer】第九章 顺序容器 (练习)

【C++ Primer】第十章 泛型算法 (练习)

【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 类型。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值