c++ primer 笔记:表达式及练习题解

表达式及练习题解

  • 基础
    • 基本概念
    • 优先级与结合律
    • 求值顺序
  • 算术运算符
  • 逻辑和关系运算符
  • 赋值运算符
  • 递增和递减运算符
  • 成员访问运算符
  • 条件运算符
  • 位运算符
  • sizeof 运算符
  • 逗号运算符
  • 类型转换
    • 算术转换
    • 其他隐式类型转换
    • 显式转换
  • 运算符优先级表

表达式是由一个或多个 运算对象(operand) 组成,对 表达式(expression)求值得到一个结果(result)

基础

基本概念

C++ 定义了一元运算符(unary operator)二元运算符(binary operator)。作用于一个运算对象的是一元运算符,作用于两个运算对象的是二元运算符。

组合运算符和运算对象

有多个运算对象时要先理解运算符的优先级(precedence)、结合律(associativity)以及运算对象的求值顺序(order of evaluation)。

运算对象转换

表达式求值时,运算对象经常从一种类型转换为另一种类型:整数与浮点数相互转换、小整数类型提升(promoted) 成较大的整数类型、但指针不能转换成浮点数。

重载运算符

当运算符作用于类类型的运算对象时,用户可以自行定义其含义,称之为重载运算符(overloaded operator)

左值和右值

C++的表达式分**左值(lvalue)右值(rvalue)**两种。
左值表达式的求值结果是一个对象或者一个函数。当一个对象被用作右值的时候,用的是对象的值;当对象被用作左值的时候,用的是对象的内存位置。

需要右值的地方能用左值代替,但不能把右值当成左值使用。下面是几种需要用到左值的运算符:

  • 赋值运算符需要一个非常量左值作为其左侧运算对象,得到的结果也仍然是一个左值;
  • 取地址符作用于一个左值运算对象,返回指向该运算对象的指针,该指针是一个右值;
  • 内置解引用运算符、下标运算符、迭代器解引用运算符、string 和 vector 的下标运算符都返回左值;
  • 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本返回左值,后置版本返回右值。

使用关键字 decltype时,如果表达式的求值结果是左值,decltype 作用于该表达式得到一个引用类型。假定 p 的类型是 int *,解引用运算符生成左值,所以 decltype(*p)生成的类型是 int &;取地址生成右值,所以 decltype(&p)生成的类型是 int **。

优先级与结合律

复合表达式(compound expression) 含有两个或多个运算符。

括号无视优先级和结合律。

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

答:在算术运算符中,乘法和除法的优先级相同,且均高于加减法的优先级。因此上式的计算结果等价于 5 + ((10 * 20) / 2),= 105。

练习 4.2:
根据4.12节中的表,在下述表达式的合理位置添加括号,使得添加括号后运算对象的组合顺序与添加括号前一致。
(a) *vec.begin()
(b) *vec.begin() + 1

答:优先级最高的是成员选择运算符和函数调用运算符,其次是解引用运算符,最后是加法运算符。因此等价后的式子是:

*(vec.begin())
(*(vec.begin())) + 1

求值顺序

对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。

比如输出运算符(<<):

int i = 0;
cout << i << " " << ++i << endl;//未定义的

上面语句可能会输出 0 1,也可能输出 1 1。

有4 种运算符明确规定了运算对象的求值顺序:

  • 逻辑与(&&)
  • 逻辑或(||)
  • 条件(?:)
  • 逗号(,)

求值顺序、优先级、结合律

运算对象的求值顺序与优先级和结合律无关。 f() + g() * h() + j() 中如果 f、g、h 和 j 是无关函数,他们既不会改变同一对象的状态也不执行 IO 任务,那么函数的调用顺序不受限制,但是其中某几个影响到同一对象,那就是一条错误的表达式。

建议:处理符合表达式

  • 不确定求值顺序时最好使用括号来强制让表达式的组合关系符合程序逻辑的要求;
  • 如果表达式改变了某个运算对象的值,则在表达式的其他位置不要再使用这个运算对象。

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

答:C++语言只规定了非常少的二元运算符(逻辑与运算符、逻辑或运算符、逗号运算符)的求值顺序,其他绝大多数二元运算符的求值顺序没有明确规定。这样提高了代码生成的效率,但是可能引发潜在的缺陷。

对于没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为;而如果运算对象彼此无关,它们既不会改变同一对象的状态,也不执行IO任务,则函数的调用顺序不受限制。

故在一定程度上是可以接受的,前提是在编写程序时注意以下两点:

  • 一是当拿不准的时候,最好用括号来让表达式的组合关系复合程序逻辑的要求;
  • 二是一旦改变了某个运算对象的值,在表达式的其他地方就不要再使用这个运算对象了。

算术运算符

按照运算符的优先级将其分组:
c++ primer 笔记:表达式及练习题解_第1张图片
上面的所有运算符都满足左结合律, 意味着当优先级相同时按照从左向右的顺序进行组合。

在除法运算中,C++语言的早期版本允许结果为负数的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接去除小数部分)。

21 % 6; 	/* 结果是3  */ 	21 / 6; 	/* 结果是3  */
21 % 7; 	/* 结果是0  */ 	21 / 7; 	/* 给果是3  */
-21 % -8; 	/* 结果是-5 */ 	-21 / -8; 	/* 结果是2  */
21 % -5; 	/* 结果是1  */ 	21 / -5; 	/* 结果是-4 */

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

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

答:添加括号之后的形式是 ((12 / 3) * 4) + (5 * 15) + ((24 % 4) / 2) = 16 + 75 + 0,= 91。

练习 4.5:写出下列表达式的求值结果。

-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:写出一条表达式用于确定一个整数是奇数还是偶数。

答:

if(i % 2 == 0) //真为偶数,假为奇数。

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

答:溢出是一种常见的算术运算错误。因为在计算机中存储某种类型的内存空间有限,所以该类型的表示能力(范围)也是有限的,当计算的结果值超出这个范围时,就会产生未定义的数值,这种错误称为溢出。

逻辑和关系运算符

关系运算符作用于算术类型或指针类型,逻辑运算符作用于能转成布尔值的类型,两者的返回值都是布尔类型。值为 0 的运算对象表示 false,否则为真,且两者的运算对象和求值结果都是右值。c++ primer 笔记:表达式及练习题解_第2张图片
逻辑与和逻辑或运算符

逻辑与(&&)当两个运算对象都为真时结果为真;逻辑或(||)只要有一个运算对象为真时结果为真。

逻辑与和或都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会去计算右侧运算对象的值,这为 短路求值(short-circuit evaluation)

  • 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。
  • 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。

举例:

// s 是对常量的引用;元素既没有被拷贝也不会被改变
for (const auto &s : text){	// text 是存储字符串的vector,对于text 的每个元素
	cout << s; 				// 输出当前元素
	// 遇到空字符串或者以句号结束的字符串进行换行
	if (s.empty() || s[s.size() - 1] == '.')
		cout << endl;
	else
		cout << " "; 		// 否则用空格隔开
}

if 语句的条件部分首先检查 s 是否是一个空 string,如果是,则不论右侧如何都换行;只有当 string 对象非空时才求第二个运算对象的值,即是否是以句号结束。

逻辑非运算符

运算符 ! 将运算对象的值取反后返回。

// 输出vec的首元素(如果有的话)
if (!vec.empty())
	cout << vec[O];

关系运算符

关系运算符比较运算对象的大小关系并返回布尔值,关系运算符都满足左结合律。

// 哎哟!这个条件居然拿i
if ( i < j < k) 	// 若k大于1则为真
// 正确:当i小于j并且j小于k时条件为真
if (i < j && j < k)
{
	/* ... */
}

第一个判断以 i < j 的布尔结果(0或1)与 k 做比较,与数学表达式的意思相差十万八千里;第二个判断的意思与数学表达式 i

测试相等性与布尔字面值

想测试一个算数对象或指针对象的真值,最直接的方式就是将其作为 if 语句的条件:

if (val)
{
	/* ... */
} // 如果val是任意的非0值,条件为真

if (!val)
{
	/* ... */
} // 如果val是0,条件为真

if (val == true)
{
	/* ... */
} // 只有当val等于true时条件才为真!

if (val == 1)
{
	/* ... */
} // 只有当val等于1时条件才为真!

warning:进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值 true 和 false 作为运算对象。

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

答:逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略就是 短路求值。
值得注意的是,逻辑与运算符和逻辑或运算符是 C++ 中仅有的几个规定了求值顺序的运算符。相等性运算符的两个运算对象都需要求值,C++没有规定其求值顺序。

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

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

答:cp 是指向字符串的指针,因此上式的条件部分含义是首先检查指针 cp 是否有效。

  • 如果 cp 为空指针或无效指针,则条件不满足。
  • 如果 cp 有效,即 cp 指向了内存中的某个有效地址,继续解引用指针 cp 并检查所指的对象是否为空字符 \0。
  • 如果 cp 所指的对象不是空字符则条件满足;
  • 否则不满足。

在本例中,显然初始状态下 cp 指向了字符串的首字符,是有效地;同时当前 cp 所指的对象是字符 H,不是空字符,所以 if 的条件部分为真。

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

答:

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

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

答:

a>b && b>c && c>d

赋值运算符

赋值运算符 = 的左侧运算对象必须是一个 可修改 的 左值。

int i = 0, j = 0, k = 0;	// 初始化而非赋值
const int ci = i;			// 初始化而非赋值

1024 = k ;					// 错误:字面值是右值
i + j = k ;					// 错误:算术表达式是右值
ci = k;						// 错误:ci是常量(不可修改的)左值

C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象:

k = (3.14);						// 错误:窄化转换
vector<int> vi;     			// 初始为空
vi = {0,1,2,3,4,5,6,7,8,9}; 	// vi现在含有10个元素了,值从0到9

赋值运算符满足右结合律

赋值运算符满足右结合律:

int ival, jval;
ival = jval = 0;    // 正确:都被赋值为0

先给 jval 赋值 0,再将 jval 的值赋给 ival。

多重赋值语句中所有对象必须相同:

int ival, *pval;	// ival的类型是int;pval是指向int的指针
ival = pval = 0;	// 错误: 不能把指针的值赋给int

string s1, s2;
s1 = s2 = "OK";		// 字符串字面值"OK"转换成string对象

赋值运算优先级较低

由于赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号:

// 这是一种形式烦琐、容易出错的写法
int i = get_value(); // 得到第一个值
while (i != 42)
{
	// 其他处理......
	i = get_value(); // 得到剩下的值
}

// 更好的写法:条件部分表达得更加清晰
int i;
while ((i = get_value()) != 42)
{
	// 其他处理......
}

复合赋值运算符

复合赋值运算符包括 +=、-=、*=、/=、%=(算术运算符);<<=、>>=、&=、^= 和 |=(位运算符)。
任意一种复合运算都完全等价于: a = a op b。

练习 4.13:在下述语句中,当赋值完成后 i 和 d 的值分别是多少?

int i;   double d;
//(a)
d = i = 3.5;
//(b)
i = d = 3.5;

答:(a)的含义是先把3.5赋值给整数 i,此时发生了自动类型转换,小数部分被舍弃,i 的值为3;接着 i 的值再赋给双精度浮点数 d,所以 d 的值也是3。
(b)的含义是先把3.5赋值给双精度浮点数 d,因此 d 的值是3.5;接着 d 的值再赋给整数 i,此时发生了自动类型转换,小数部分被舍弃,i 的值为3。

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

if (42 = i)   // ...
if (i = 42)   // ...

答:第一条语句发生编译错误,因为赋值运算符的左侧运算对象必须是左值,字面值常量42显然不是左值,不能作为左侧运算对象。

第二条语句从语法上来说是正确的,但是与程序的原意不符。程序的原意是判断 i 的值是否是42,应该写成 i==42;而 i==42 的意思是把42赋值给 i,然后判断 i 的值是否为真。因为所有非0整数转换成布尔值时都对应 true,所以该条件是恒为真的。

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

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

答:因为在这个多重赋值语句中要被赋值的对象类型不同。

正确的应该是:

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

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

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

答:赋值运算符的优先级较低,第一个 if 语句在判断后将布尔值赋值给 p,然而原意应该是将 getPtr() 的运算结果赋值给 p,然后再比较。

第二个 if 是先赋值,然后以 i 的值作为条件。
正确的:

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

递增和递减运算符

递增( ++ )和递减( – )运算符是为对象加1或减1,这两个运算符还可应用于迭代器,因为很多迭代器本身不支持算术运算。

递增和递减运算符分为前置版本和后置版本:

  1. 前置版本:首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。
  2. 后置版本:也会将运算对象加1(或减1),但求值结果是运算对象改变前的值的副本。
int i = 0, j;
j = ++i;    // j = 1, i = 1: 前置版本得到递增之后的值
j = i++;    // j = 1, i = 2: 后置版本得到递增之前的值

除非必须,否则不应该使用递增或递减运算符的后置版本。因为后置版本需要将原始值存储下来以便于返回修改前的内容,如果我们不需要这个值,那么后置版本的操作就是一种浪费。

在一条语句中混用解引用和递增运算符

使用后置版本可以使用变量递增或递减前的值:

auto pbeg = v.begin();
// 输出元素直至遇到第一个负值为止
while (pbeg != v.end() && *beg >= 0)
	// 输出当前值并将pbeg向前移动一个元素
	cout << *pbeg++ << endl;

该程序输出 vector 对象的值,直到末尾或遇见负数。
后置递增运算符的优先级高于解引用运算符,因此 *pbeg++ 等价于 *(pbeg++)。pbeg++ 把 pbeg 的值加1, 然后返回 pbeg 的初始值的副本作为其求值结果,此时解引用运算符的运算对象是 pbeg 未增加之前的值。最终,这条语句输出 pbeg 开始时指向的那个元素,并将指针向前移动一个位置。

练习 4.17:说明前置递增运算符和后置递增运算符的区别。

答:前置运算符先将运算对象加 1,然后把改变后的对象作为求值结果。
后置运算符将原始值存储为副本,求值后将副本作为求值结果。

练习 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 所指的整数是否为非0数。如果非0,则该表达式的最终求值结果为真;否则为假。最后把指针 ptr 向后移动一位。该表达式从语法上分析是合法的,但是最后的指针移位操作不一定有意义。如果 ptr 所指的是整型数组中的某个元素,则 ptr 可以按照预期移动到下一个元素。如果 ptr 所指的只是一个独立的整数变量,则移动指针操作将产生未定义的结果。

(b)的含义是先检查 ival 的值是否非0,如果非0继续检查(ival+1)的值是否非0。只有当两个值都是非0值时,表达式的求值结果为真;否则为假。如果二元运算符的两个运算对象涉及同一个对象并改变对象的值,则这是一种不好的程序写法,应该改写,所以按照程序的原意,本式应该改写成 ival && (ival + 1)。

(c)的含义是比较 vec[ival] 和 vec[ival+1] 的大小,如果前者较小则求值结果为真,否则为假。与(b)式一样,本式也出现了二元运算符的两个运算对象涉及同一个对象并改变对象的值的情况,应该改写为 vec[ival] <= vec[ival+1]。

成员访问运算符

点运算符 . 和箭头运算符 -> 都可以用来访问成员,其中,点运算符获取类对象的一个成员;箭头运算符与点运算符有关,表达式 ptr->mem 等价于 (*ptr).mem:

string s1 = "a string", *p = &s1;
auto n = s1.size(); // 运行string对象s1的size成员
n = (*p).size();    // 运行p所指对象的size成员
n = p->size();      // 等价于(*p).size()

因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。如果没如括号,代码的含义就大不相同了:

//运行p的size成员,然后解引用size的结果
*p.size (); // 错误:p是一个指针,它没有名为size的成员

练习 4

答:

条件运算符

条件运算符(? :)的使用形式如下:

cond ? expr1 : expr2;

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

string finalgrade = (grade < 60) ? "fail" : "pass";

只有当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果才是左值,否则运算的结果就是右值。

嵌套条件运算符

条件运算符可以嵌套,但是考虑到代码的可读性,运算的嵌套层数最好不要超过两到三层。

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

在输出表达式上使用条件运算符

条件运算符的优先级非常低,因此当一个长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。

cout << ((grade < 60) ? "fail" : "pass"); 	// 输出pass或者fail

cout << (grade < 60) ? "fail" : "pass"; 	// 输出1或者0
// 等价于<=>
// cout << (grade < 60); 					// 输出1或者0
// cout ? "fail" : "pass"; 					// 根据cout的值是true还是false产生对应的字面值

cout << grade < 60 ? "fail" : "pass"; 		// 错误:试图比较cout和60
// 等价于<=>
// cout << grade; 							// 小于运算符的优先级低于移位运算符,所以先输出grade
// cout < 60 ? "fail" : "pass"; 			// 然后比较cout和60

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

答:

#include 
#include 
#include 
#include 
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 * 2 : val;
	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 语句。哪个版本的程序更容易理解呢?为什么?

答:

#include 
#include 
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;
	}
	return 0;
}

#include 
#include 
using namespace std;
int main()
{
	string finalgrade;
	int grade;
	cout << "请输入您要检查的成绩:" << endl;
	while (cin >> grade && grade >= 0 && grade <= 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;
		cout << "请输入您要检查的成绩:" << endl;
	}
	return 0;
}

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

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

答:题中几个运算符的优先级次序从高到低是加法运算符、相等运算符、条件运算符和赋值运算符,因此式子的求值过程是先把 s 和 s[s.size-1() - 1] 相加得到一个新字符串,然后该字符串与字符 ‘s’ 比较是否相等,这是一个非法操作,并且与程序的原意不符。

要想实现程序的原意,即先判断字符串 s 的最后一个字符是否是 ‘s’,如果是,什么也不做;如果不是,在 s 的末尾添加一个字符 ‘s’,我们应该添加括号强制限定运算符的执行顺序。

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

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

答:如果条件运算符满足的是左结合律。那么对象按照从左向右的顺序组合,靠左边的条件运算构成了右边条件运算的条件。

先考察 grade > 90 是否成立,如果成立,第一个条件表达式的值为 “high”;如果不成立,第一个条件表达式的值为 grade < 60。这条语句是无法编译通过的,因为条件运算符要求两个结果表达式的类型相同或者可以互相转化。即使假设语法上通过,也就是说,第一个条件表达式的求值结果分为3中,分别是 “high pass”、1和0。接下来根据第一个条件表达式的值求解第二个条件表达式,求值结果是 “fail” 或 “pass”。这个求值过程显然与我们的期望是不符的。

位运算符

位运算符(左结合律):

c++ primer 笔记:表达式及练习题解_第3张图片
一般地,小整形在运算中会自动提升为较大的整型,位运算符如何处理运算对象的符号位依赖机器,所以强烈建议仅将位运算符用于无符号类型的处理。

移位运算符

左移运算符 << 在运算对象右侧插入值为0的二进制位,右移运算符 >> 的行为依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在其左侧插入值为0的二进制位;如果是带符号类型,在其左侧插入符号位的副本或者值为0的二进制位,如何选择视具体环境而定。
c++ primer 笔记:表达式及练习题解_第4张图片

位求反运算符
位求反运算符( ~ )将运算对象逐位求反而生成一个新值,将1置为0、将0置为1。

c++ primer 笔记:表达式及练习题解_第5张图片
char 类型的运算对象首先提升成 int 类型,提升时运算对象原来的位保持不变, 往 高位(high order position) 添0即可。

位与、位或、位异或运算符

与( & )、或( | )、异或( ^ )运算符在两个运算对象上逐位执行相应的逻辑操作。

c++ primer 笔记:表达式及练习题解_第6张图片

位异或运算符( ^ ) 来说,如果两个运算对象的对应位置元素不同则运算结果中该位为1,否则为0。

移位运算符满足左结合律

移位运算符的优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。

cout << 42 + 10; 	// 正确:+的优先级更高,因此输出求和结果
cout << (10 < 42); 	// 正确:括号使运算对象按照我们的期望组合在一起,输出1
cout << 10 < 42; 	// 错误:试图比较cout和42!
// 等价于<=>
// (cout << 10) < 42;

练习 4.25:如果一台机器上 int 占32位、char 占8位,用的是 Latin-1 字符集,其中字符 ‘q’ 的二进制形式是 01110001,那么表达式 ~‘q’ << 6 的值是什么?

答:在位运算符中,运算符 ~ 的优先级高于 <<,因此先对 q 按位求反,因为位运算符的运算对象应该是整数类型,所以字符 q 首先转换为整数类型。char 占8位而 int 占32位,所以字符 q 转换后得到 00000000 00000000 00000000 01110001,按位求反得到 11111111 11111111 11111111 10001110;接着执行移位操作,得到 11111111 11111111 11100011 10000000。

C++规定整数按照其补码形式存储,对上式求补,得到 10000000 00000000 00011100 100000000,即最终结果的二进制形式,转换成十进制形式是-7296。

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

答:原书中使用 unsigned long 作为 quizl 的数据类型是恰当的,因为C++规定 unsigned long 在内存中至少占32位,这样就足够存放30个学生的信息了。

如果使用 unsigned int 作为 quizl 的数据类型,则由于C++规定 unsigned int 所占空间的最小值是16,所以在很多机器环境中,该数据类型不足以存放全部学生的信息,从而造成了信息丢失,无法完成题目要求的任务。

练习 4.27:下列表达式的结果是什么?

unsigned long ul1 = 3, ul2 = 7;
(a) ul1 & ul2 
(b) ul1 | ul2 
(c) ul1 && ul2
(d) ul1 || ul2 

答:
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。

sizeof 运算符

sizeof 运算符返回一个表达式或一个类型名字所占的字节数,返回值是 size_t 类型的常量表达式。

该运算符的运算对象有两种:

sizeof(type)   //返回类型 type 所占的字节数
sizeof expr    //表达式对象的大小

sizeof 运算符的结果部分依赖于其作用的类型。

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

答:

#include  
using namespace std;
int main()
{
	cout << "类型名称\t" << "所占空间" << endl;
	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;
}

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

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

答:

  • sizeof(x) 的运算对象 x 是数组的名字,求值结果是整个数组所占空间的大小。
  • sizeof(*x) 的运算对象 *x 是一条解引用表达式,此处的 x 既是数组的名称,也表示指向数组首元素的指针,解引用该指针得到指针所指的内容,在这里是一个 int。所以 sizeof(*x) 在这里等价于 sizeif(int),即 int 所占的内存空间。
  • sizeof(x)/sizeof(*x) 可以理解为数组 x 所占的全部空间除以其中一个元素所占的空间,得到的结果应该是数组 x 的元素总数。实际上,因为C++的内置数组并没有定义成员函数 size(),所以通常无法直接得到数组的容量。本题的方法是计算得到数组容量的一种常规方法。

  • sizeof§ 的运算对象 p 是一个指针,求值结果是指针所占的空间大小。
  • sizeof(*p) 的运算对象 *p 是指针所指的对象,即 int 变量 x,所以求值结果是 int 值所占的空间大小。
#include  
using namespace std;
int main()
{
	int x[10];
	int *p = x;
	cout << sizeof(x) << endl;
	cout << sizeof(*x) << endl;
	cout << sizeof(x) / sizeof(*x) << endl;

	cout << endl;
	cout << "********************" << endl;
	cout << endl;

	cout << sizeof(p) << endl;
	cout << sizeof(*p) << endl;
	cout << sizeof(p) / sizeof(*p) << endl;

	return 0;
}

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

(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 的优先级,所以不需要添加括号。
©的含义是先求变量 a 在内存中所占空间的大小,再把求得的值与变量 b 的值比较。因为 sizeof 的优先级高于关系运算符的优先级,所以如果想求表达式 a (d)的含义是求函数 f() 返回值所占内存空间的大小,因为函数调用运算符的优先级高于 sizeof 的优先级,所以不需要添加括号。

逗号运算符

逗号运算符 , 含有两个运算对象,按照从左向右的顺序依次求值,最后返回右侧表达式的值。逗号运算符经常用在 for 循环中。

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.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.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) { /* ... */ }

答:首先定义一个常量表达式 size,它的值是5;接着以 size 作为维度创建一个整型数组 ia,5个元素分别是1~5。
for 语句头包括三部分:
第一部分定义整型指针指向数组 ia 的首元素,并且定义了一个整数 ix,赋给它初值0;
第二部分判断循环终止的条件,当 ix 没有达到 size 同时指针 ptr 没有指向数组最后一个元素的下一位置时,执行循环体;
第三部分令变量 ix 和指针 ptr 分别执行递增操作。

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

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

答:

C++规定条件运算符的优先级高于逗号运算符,所以 someValue ? ++x, ++y : --x, --y 实际上等价于 (someValue ? ++x, ++y : --x), --y。它的求值过程是:

首先判断 someValue 是否为真,
如果为真,依次执行 ++x 和 ++y,最后执行 --y;
如果为假,执行 --x 和 --y。
下面是检验上述推断的实例程序:

#include  
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;
	
	cout << endl;
	cout << "********************" << endl;
	cout << endl;
	
	x = 10, y = 20;
	// 检验条件为假的情况
	someValue = false;
	someValue ? ++x, ++y : --x, --y;
	cout << x << endl;
	cout << y << endl;
	cout << someValue << endl;

	return 0;
}

类型转换

在C++语言中,某些类型之间有关联。如果两种类型有关联, 那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。换句话说,如果两种类型可以 相互转换(conversion),那么它们就是关联的。

无须程序员介入,会自动执行的类型转换叫做 隐式转换(implicit conversions),可以尽可能地避免损失精度:

int ival = 3.14 + 3;

上面代码将浮点数与整数相加,先将 3 转化为浮点数,相加结果为 6.14,然后将浮点数赋值给整数对象,截去小数点后数字。

在下面这些情况下, 编译器会自动地转换运算对象的类型:

  • 在大多数表达式中,比 int 类型小的整型值首先提升为较大的整数类型。
  • 在条件中,非布尔值转换成布尔类型。
  • 在初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
  • 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
  • 函数调用时也会发生类型转换。

算术转换

**算术转换(arithmetic conversion)**是指把一种算术类型转换成另一种算数类型的过程,其中运算符的运算对象将被转换成最快的类型。

整型提升

整型提升(integral promotions) 负责把小整数类型转换成较大的整数类型。
bool、char、signed char、unsigned char、short 和 unsigned short 会提升为 int,int 盛不下时提升为 unsigned int。
较大的 char (wchar_t、char16_t、char32_t)提升成 int、unsigned int、long、unsigned long、long long、unsigned long long 中最小的类型。

无负号类型的运算对象

  • 如果某个运算符的运算对象类型不一致,且不包含无符号类型,则转换成同一种类型
  • 如果某个运算符类型是无符号类型,则转换结果依赖于机器中各个整数类型的相对大小。

首先进行整型提升。

  • 若类型匹配,则运算;
  • 若两个提升后的运算符都是带符号或无符号的,则小类型转化为大类型。
  • 如果一个有符号另一个无符号,且无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。
  • 如果一个有符号另一个无符号,且无符号类型小于带符号类型,此时转换结果依赖机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型转成带符号;如果不能,带符号转无符号。

理解算术转换

上面的规则很晦涩,看几个例子理解一下:

bool flag; 		char cval;
short sval; 	unsignedshort usval;
int ival; 		unsigned int uival;
long lval; 		unsigned long ulval;
float fval; 	double dval;

3.14159L + 'a';	// 'a'提升成int,然后该int值转换成long double
dval + ival; 	// ival转换成double
dval + fval;	// fval转换成double
ival = dval;	// dval转换成(切除小数部分后)int
flag = dval;	// 如果dval是0,则flag是false,否则flag是true
cval + fval;	// cval提升成int,然后该int值转换成float
sval + cval;	// sval和cval都提升成int
cval + lval;	// cval转换成long
ival + ulval;	// ival转换成unsigned long
usval + ival;	// 根据unsigned short和int所占空间的大小进行提升
uival + lval;	// 根据unsigned int和long所占空间的大小进行转换

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

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

答:
(a)if 语句的条件应该是布尔值,因此 float 型变量 fval 自动转换为 bool 类型,转换规则是所有非0值转换为 true,0转换为 false。
(b)ival 转换为 float,与 fval 相加的结果进一步转换为 double 类型。
(c)cval 执行整型提升转换为 int,与 ival 相乘后所得的结果转换为 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)字符 ‘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 类型。

其他隐式类型转换

数组转换成指针
数组转换成指针:在大多数表达式中,数组名字自动转换成指向数组首元素的指针。

int ia[10]; 	// 含有10个整数的数组
int* ip = ia;	// ia转换成指向放组首元素的指针

指针的转换
指针的转换:常量整数值0或字面值 nullptr 能转换成任意指针类型;指向任意非常量的指针能转换成 void*;指向任意对象的指针能转换成 const void*。

转换成布尔类型
任意一种算术类型或指针类型都能转换成布尔类型。如果指针或算术类型的值为0,转换结果是 false,否则是 true。

转换成常量
允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。

int i;
const int &j = i;	// 非常量转换成const int的引用
const int *p = &i;	// 非常量的地址转换成const的地址
int &r = j, *q = p;	// 错误:不允许const转换成非常量

类类型定义的转换
类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。

string s, t = "a value";	// 字符串字而位转换成string类型
while (cin >> s)			// while的条件部分把cin转换成布尔值

显式转换

显式类型转换 也叫做 强制类型转换(cast)。

命名的强制类型转换
命名的强制类型转换(named cast)具有如下形式:

cast-name<type>(expression);

其中 type 是转换的目标类型,expression 是要转换的值。如果 type 是引用类型,则转换结果是左值。cast-name 是 static_cast、dynamic_cast、const_cast 和 reinterpret_cast 中的一种,用来指定转换的方式。

  • dynamic_cast 支持运行时类型识别。
  • 任何具有明确定义的类型转换,只要不包含底层 const,都能使用 static_cast。当需要把一个较大的算术类型赋值给较小的类型时,static cast 非常有用。static cast 对于编译器无法自动执行的类型转换也非常有用。
  • const_cast 只能改变运算对象的底层 const,同时也只有 const_cast 能改变表达式的常量属性。const_cast 常常用于函数重载。
  • reinterpret_cast 通常为运算对象的位模式提供底层上的重新解释。reinterpret_cast 本质上依赖于机器。要想安全地使用reinterpret_cast 必须对涉及的类型和编译器实现转换的过程都非常了解。

早期版本的C++语言中,显式类型转换包含两种形式:

type (expression);    // 函数形式的强制类型转换
(type) expression;    // C语言风格的强制类型转换

根据所设计的类型不同,旧式的强制类型转换分别有显式转换相似的行为。与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不那么清晰明了了,容易被看漏,所以一旦转换过程出现问题,追踪起来也更加困难。
练习 4.36:假设 i 是 int 类型,d 是 double 类型,书写表达式 i*=d 使其执行整数类型的乘法而非浮点类型的乘法。

答:使用 static_cast 把 double 类型的变量 d 强制转换成 int 类型,就可以令 i*=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;

答:
(a) pv = static_cast(const_cast(ps));
(b) i = static_cast(*pc);
(c)pv = static_cast(&d);
(d) pc = static_cast(pv);

练习 4.38:说明下面这条表达式的含义。
double slope = static_cast(j/i);

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

请注意,如果 i 和 j 的类型都是 int,则 j/i 的求值结果仍然是 int,即使除不尽也只保留商的整数部分,最后再转换成 double 类型。

运算符优先级表

c++ primer 笔记:表达式及练习题解_第7张图片
c++ primer 笔记:表达式及练习题解_第8张图片

关注博仔不迷路,一起学好C++

你可能感兴趣的:(C++,primer,c++,开发语言,java)