C++ Primer 第五版:第四章「表达式」习题答案

第四章:表达式

练习4.1

表达式 5 + 10 * 20 / 2 的求值结果是多少?

105

练习4.2

根据4.12节中的表,在下述表达式的合理位置添加括号,使得添加括号后运算对象的组合顺序与添加括号前一致。

  • (a) *vec.begin()
  • (b) *vec.begin() + 1
1
2
*(vec.begin())
(*(vec.begin())) + 1

练习4.3

C++语言没有明确规定大多数二元运算符的求值顺序,给编译器优化留下了余地。这种策略实际上是在代码生成效率和程序潜在缺陷之间进行了权衡,你认为这可以接受吗?请说出你的理由。

可以接受。C++的设计思想是尽可能地“相信”程序员,将效率最大化。然而这种思想却有着潜在的危害,就是无法控制程序员自身引发的错误。因此 Java 的诞生也是必然,Java的思想就是尽可能地“不相信”程序员。

练习4.4

在下面的表达式中添加括号,说明其求值过程及最终结果。编写程序编译该(不加括号的)表达式并输出结果验证之前的推断。

1
12 / 3 * 4 + 5 * 15 + 24 % 4 / 2

((12/3)*4) + (5*15) + ((24%4)/2)

实际输出结果:91。

练习4.5

写出下列表达式的求值结果。

1
2
3
4
-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

写出一条表达式用于确定一个整数是奇数还是偶数。

1
if (i % 2 == 0) /* ... */

练习4.7

溢出是何含义?写出三条将导致溢出的表达式。

溢出含义:当计算的结果超出该类型所能表示的范围时就会产生溢出。

1
2
3
short svalue = 32767; ++svalue; // -32768
unsigned uivalue = 0; --uivalue; // 4294967295
unsigned short usvalue = 65535; ++usvalue; // 0

练习4.8

说明在逻辑与、逻辑或及相等性运算符中运算对象的求值顺序。

  • 逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为短路求值
  • 相等性运算符未定义求值顺序。

练习4.9

解释在下面的if语句中条件部分的判断过程。

1
2
const char *cp = "Hello World";
if (cp && *cp)

首先判断 cpcp 不是一个空指针,因此 cp 为真。然后判断 *cp*cp 的值是字符 'H',非0。因此最后的结果为真。

练习4.10

为while 循环写一个条件,使其从标准输入中读取整数,遇到 42 时停止。

1
2
int i;
while(cin >> i && i != 42)

练习4.11

书写一条表达式用于测试4个值a、b、c、d的关系,确保a大于b、b大于c、c大于d。

1
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 的值分别是多少?

1
2
3
int i;   double d;
d = i = 3.5; // i = 3, d = 3.0
i = d = 3.5; // d = 3.5, i = 3
  • 赋值运算符的左侧运算对象必须是一个可修改的左值,赋值运算的结果是左侧运算对象。
  • 若左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。
  • 赋值运算符满足右结合律,因此可多重赋值,例如s1=s2="Hello"
  • 赋值运算符优先级一般较低。
  • 复合赋值运算符等价于:a=a op b,例如a+=1等价于a=a+1

左值和右值

左值和右值的概念是从C语言继承过来的。

在C语言中,左值可以位于赋值语句的左侧,右值则不能。

在C++语言中,区别则复杂一些。当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。

练习4.14

执行下述 if 语句后将发生什么情况?

1
2
if (42 = i)   // 编译错误。赋值运算符左侧必须是一个可修改的左值。而字面值是右值。
if (i = 42) // true.

练习4.15

下面的赋值是非法的,为什么?应该如何修改?

1
2
double dval; int ival; int *pi;
dval = ival = pi = 0;

pi 是指针,不能赋值给 int 类型,应该改为:

1
2
dval = ival = 0;
pi = 0;

练习4.16

尽管下面的语句合法,但它们实际执行的行为可能和预期并不一样,为什么?应该如何修改?

1
2
if (p = getPtr() != 0)
if (i = 1024)
  • p=getPtr() != 0中赋值运算符优先级低于不相等运算符,因此这个条件判断首先执行getPtr() != 0,返回一个bool值,之后将bool值赋值给变量p,因此条件判断可能为true或者false。
  • i = 1024条件判断总是为 true

代码修改为:

1
2
if ((p=getPtr()) != 0)
if (i == 1024)

练习4.17

说明前置递增运算符和后置递增运算符的区别。

这两种运算符必须作用于左值运算对象。前置递增运算符将对象本身作为左值返回,而后置递增运算符将对象原始值的副本作为右值返回。

P.S.如无必要,例如不需要修改前的值,那么不要使用递增递减运算符的后置版本。

练习4.18

如果132页那个输出vector对象元素的while循环使用前置递增运算符,将得到什么结果?

将会从第二个元素开始取值,并且如果序列中没有负值时,最后对 v.end() 进行取值,结果是未定义的,因为end成员返回的迭代器是尾后迭代器,指向一个本不存在的尾后元素(尾后迭代器是标记作用,无实际含义)。

练习4.19

假设 ptr 的类型是指向 int 的指针、vec 的类型是vector、ival 的类型是int,说明下面的表达式是何含义?如果有表达式不正确,为什么?应该如何修改?

1
2
3
(a) ptr != 0 && *ptr++
(b) ival++ && ival
(c) vec[ival++] <= vec[ival]
  • (a) 判断ptr不是一个空指针,并且ptr当前指向的元素的值也为真,然后将ptr指向下一个元素
  • (b) 判断(ival + 1)的值为真,并且ival的值也为真
  • (c) 表达式有误。C++标准中并没有规定 <= 运算符两边的求值顺序,因此编译器既有可能先求左侧的值,也有可能先求右侧的值,而这两种方式导致的结果也不同,这种是未定义行为undefined behavior(详见P133)。应该改为 vec[ival] <= vec[ival+1]

练习4.20

假设 iter 的类型是 vector<string>::iterator, 说明下面的表达式是否合法。如果合法,表达式的含义是什么?如果不合法,错在何处?

1
2
3
4
5
6
(a) *iter++;
(b) (*iter)++;
(c) *iter.empty();
(d) iter->empty();
(e) ++*iter;
(f) iter++->empty();
  • (a)合法。返回迭代器所指向的元素,然后迭代器递增。
  • (b)不合法。因为vector元素类型是 string,没有 ++ 操作。
  • (c)不合法。解引用运算符的优先级低于点运算,iter是一个迭代器,没有名为empty的成员,这里应该加括号。
  • (d)合法。判断迭代器当前的元素是否为空。
  • (e)不合法。string 类型没有 ++ 操作。
  • (f)合法。判断迭代器当前元素是否为空,然后迭代器递增。

成员访问运算符

点运算符和箭头运算符都可用于访问成员,获取类对象的一个成员的的运算符形式如下(设定cls是一个类对象,ptr是指向cls的指针或者迭代器,访问其中men成员):

  • 点运算符:cls.men或者(*ptr).men,点运算符作用于对象类型
  • 箭头运算符:ptr->men,箭头运算符作用于指针类型

练习4.21

编写一段程序,使用条件运算符从 vector中找到哪些元素的值是奇数,然后将这些奇数值翻倍。

条件运算符(?:)允许我们把简单的 if-else 逻辑嵌入到单个表达式当中,条件运算符按照如下形式使用:

1
cond ? exprl : expr2

其中 cond 是判断条件的表达式,而 expr1 和 expr2 是两个类型相同或可能转换为某个公共类型的表达式。条件运算符的执行过程是:首先求 cond 的值,如果条件为真对expr1求值并返回该值,否则对expr2求值并返回该值。

  • 当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值:否则运算的结果是右值。
  • 条件运算符允许嵌套,为了保证代码的可读性,嵌套不要超过两三层。
  • 条件运算符满足右结合律,即运算对象一般按照从右向左的顺序组合。

练习4.22

本节的示例程序将成绩划分为high pass、pass 和 fial 三种,扩展该程序使其进一步将 60 分到 75 分之间的成绩设定为 low pass。要求程序包含两个版本:一个版本只使用条件运算符;另一个版本使用1个或多个if语句。哪个版本的程序更容易理解呢?为什么?

if-else版本更容易理解。当条件运算符嵌套层数变多之后,代码的可读性急剧下降。而if-else的逻辑很清晰。

练习4.23

因为运算符的优先级问题,下面这条表达式无法通过编译。根据4.12节中的表(第147页)指出它的问题在哪里?应该如何修改?

1
2
string s = "word";
string pl = s + s[s.size() - 1] == 's' ? "" : "s" ;

加法运算符的优先级高于条件运算符。因此要改为:

1
string pl = s + (s[s.size() - 1] == 's' ? "" : "s") ;

练习4.24

本节的示例程序将成绩划分为 high pass、pass、和fail三种,它的依据是条件运算符满足右结合律。假如条件运算符满足的是左结合律,求值的过程将是怎样的?

如果条件运算符满足的是左结合律。那么

1
finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass";

等同于

1
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,结果是 00000000 00000000 00011100 01000000,转换十进制为7232。

位运算符的总结

  • 位运算符操作可能会改变符号为的值,关于符号位如何处理没有明确的规定,具体依赖于机器,因此强烈建议仅将位运算符用于处理无符号类型。
  • 移位运算符满足左结合律,移位运算符又名IO运算符(重载版本,用以IO操作)

练习4.26

在本节关于测验成绩的例子中,如果使用unsigned int 作为quiz1 的类型会发生什么情况?

C++标准中并未以字节的单位指定int的大小,但指定了它们必须能够容纳的最小范围。unsigned int的最小取值范围是0到65535。但在有的机器上,unsigned int类型可能只有16位,而这结果是未定义行为。

练习4.27

下列表达式的结果是什么?

1
2
3
4
5
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) true

练习4.28

编写一段程序,输出每一种内置类型所占空间的大小。

练习4.29

推断下面代码的输出结果并说明理由。实际运行这段程序,结果和你想象的一样吗?如不一样,为什么?

1
2
3
int x[10];   int *p = x;
cout << sizeof(x)/sizeof(*x) << endl;
cout << sizeof(p)/sizeof(*p) << endl;

第一个输出结果是 10。第二个结果是未定义。

参考:Why the size of a pointer is 4bytes in C++

sizeof运算符的总结:

  • 对char或者类型为char的表达式执行sizeof运算,结果得1。
  • 对引用类型执行sizeof运算得到被引用对象所占空间的大小。
  • 对指针执行sizeof运算得到指针本身所占空间的大小。
  • 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需有效。在sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。sizeof不需要真的解引用指针也能知道它所指对象的类型。
  • 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和(因为执行sizeof运算能得到整个数组的大小,所以可以用数组的大小除以单个元素的大小得到数组中元素的个数:sizeof(ia)/sizeof(*ia)返回ia的元素数量)。注意,sizeof运算不会把数组转换成指针来处理。
  • 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。

练习4.30

根据4.12节中的表,在下述表达式的适当位置加上括号,使得加上括号之后的表达式的含义与原来的含义相同。

1
2
3
4
(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())

练习4.31

本节的程序使用了前置版本的递增运算符和递减运算符,解释为什么要用前置版本而不用后置版本。要想使用后置版本的递增递减运算符需要做哪些改动?使用后置版本重写本节的程序。

练习4.17(4.5节,132页)已经说过了,除非必须,否则不用递增递减运算符的后置版本。在这里要使用后置版本的递增递减运算符不影响程序结果,不需要任何改动。

练习4.32

解释下面这个循环的含义。

1
2
3
4
5
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 都是起到一个计数的功能,任意选择其中一个都可以完成遍历。

练习4.33

根据4.12节中的表说明下面这条表达式的含义。

1
someValue ? ++x, ++y : --x, --y

逗号表达式的优先级是最低的。因此这条表达式也等于:

1
(someValue ? ++x, ++y : --x), --y

如果someValue的值为真,x和y的值都自增并返回y值,然后丢弃y值,y递减并返回y值。如果someValue的值为假,x递减并返回x值,然后丢弃x值,y递减并返回y值。

练习4.34

根据本节给出的变量定义,说明在下面的表达式中奖发生什么样的类型转换:

1
2
3
(a) if (fval)
(b) dval = fval + ival;
(c) dval + ival * cval;

需要注意每种运算符遵循的是左结合律还是右结合律。

  • (a) fval 转换为 bool 类型
  • (b) ival 转换为 float ,相加的结果转换为 double
  • (c) cval 转换为 int,然后相乘的结果转换为 double

练习4.35

假设有如下的定义:

1
2
3
4
5
char cval;
int ival;
unsigned int ui;
float fval;
double dval;

请回答在下面的表达式中发生了隐式类型转换吗?如果有,指出来。

1
2
3
4
(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

隐式转换:

  • 算术转换
    • 在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型。
    • 在条件中,非布尔值转换成布尔类型。
    • 初始化过程中,初始值转换成变量的类型:在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
    • 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
    • 如第 6 章将要介绍的,函数调用时也会发生类型转换。
  • 其他隐式类型转换(详见P143-P144)
    • 数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针。
    • 指针的转换
    • 转换成布尔类型
    • 转换成常量
    • 类类型定义的转换

练习4.36

假设 i 是int类型,d 是double类型,书写表达式 i*=d 使其执行整数类型的乘法而非浮点类型的乘法。

1
i *= static_cast<int>(d);

练习4.37

用命名的强制类型转换改写下列旧式的转换语句。

1
2
3
4
5
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(*pc);
  • (c) pv = static_cast<void*>(&d);
  • (d) pc = static_cast<char*>(pv);

练习4.38

说明下面这条表达式的含义。

1
double slope = static_cast<double>(j/i);

j/i 的结果值转换为 double,然后赋值给slope。

显式转换(如无必要,尽可能避免强制类型转换,因为会干扰正常的类型检查)

  • 命名的强制类型转换:cast-name<type>(expression);其中,type是转换的目标类型,而expression是要转换的值。如果type是引用类型,则结果是左值。 cast-name有以下四种类型
    • static_cast
      • 任何具有明确定义的类型转换,只要不包含底层const ,都可以使用 static_cast。例如,通过将一个运算对象强制转换成 double 类型就能使表达式执行浮点数除法:double slope=static_cast<double>(j)/i
      • 当需要把一个较大的算术类型赋值给较小的类型时, static_cast非常有用。此时,强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息;但是当我们执行了显式的类型转换后,警告信息就会被关闭了。
      • static_cast对于编译器无法自动执行的类型转换也非常有用。
    • dynamic_cast:支持运行时类型识别。
    • const_cast:只能改变运算对象的底层const(详见P145)。const_cast常常用于有函数重载的上下文中。
    • reinterpret_cast:通常为运算对象的位模式提供较低层次上的重新解释。使用这个类型是非常危险的。reinterpret_cast本质上依赖于机器。要想安全地使用reinterpret_cast,必须对涉及的类型和编译器实现转换的过程都非常了解。
  • 旧式的强制类型转换
    • type(expr);:函数形式
    • (type)expr;:C语言风格

运算符优先级表

运算符功能用法结合律参考页码
::全局作用域::nameP256
::类作用域class::nameP79
::命名空间作用域namespace::nameP74
.成员选择object::memberP20
->成员选择pointer->memberP98
[]下标expr[expr]P104
()函数调用name(expr_list)P20
()类型构造type(expr_list)P145
++后置递增运算lvalue++P131
--后置递减运算lvalue--P131
typeid类型IDtypeid(type)P731
typeid运行时类型IDtypeid(type)P731
explicit cast类型转换cast_nam<type>(expr)P144
++前置递增运算++lvalueP131
--前置递减运算--lvalueP131
~位求反~exprP136
!逻辑非!exprP126
-一元负号-exprP124
+一元正号+exprP124
*解引用*exprP48
&取地址&lvalueP47
()类型转换(type)exprP145
sizeof对象的大小sizeof exprP139
sizeof类型的大小sizeof(type)P139
sizeof...参数包的大小sizeof...(name)P619
new创建对象new typeP407
new[]创建数组new typep[size]P407
delete释放对象delete exprP409
delete[]释放数组delete[] exprP409
noexcept能否抛出异常noexpect(expr)P690
->*指向成员选择的指针ptr->*ptr_to_memberP740
.*指向成员选择的指针obj.*ptr_to_memberP740
*乘法expr*exprP124
/除法expr/exprP124
%取模(取余)expr%exprP124
+加法expr+exprP124
-减法expr-exprP124
<<向左移位expr<<exprP136
>>向右移位exp>>exprP136
<小于exp<exprP126
<=小于等于exp<=exprP126
>大于expr>exprP126
<=大于等于exp>=exprP126
==相等exp==exprP126
!=不相等exp!=exprP126
&位与exp&exprP136
^位异或exp^exprP136
|位或exp|exprP136
&&逻辑与exp&&exprP126
||逻辑或exp||exprP126
?:条件exp?expr:exprP134
=赋值lvalue=exprP129
*=, /=, %=, +=, -=, <<=, >>=, &=, |=, ^=复合赋值lvalue+=expr等P129
throw抛出异常throw exprP173
,逗号expr, exprP140