目录
第四章 表达式
简介
C++语言提供了一套丰富的运算符,并定义了这些运算符作用于内置类型的运算对象时所执行的操作。同时,当运算对象是类类型时,C++语言也允许由用户指定上述运算符的含义。本章主要介绍由语言本身定义、并用于内置类型的运算符,同时简单介绍几种标准库定义的运算符。第14章会专门介绍用户如何自定义适用于类类型的运算符。
表达式是由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果(result)。字面值和变量是最简单的表达式(expression),其结果就是字面值和变量的值。把一个运算符(operator)和一个或多个运算对象组合起来可以生成较复杂的表达式。
4.1 基础
有几个基础概念对表达式求值过程中有影响,它们涉及大多数(甚至全部)表达式。本节先简要介绍这几个概念,后面小节将做更详细的讨论。
4.11 基本概念
C++定义了一元运算符(unary operator)和二元运算符(binary operator)。作用于一个运算对象的运算符是一元运算符,如取地址符(&)和解引用符(*
);作用于两个运算对象的运算符是二元运算符,如相等运算符(==)和乘法运算符(*
)。除此之外,还有一个作用域三个运算对象的三元运算符。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。
像符号*
这样的符号既能作为一元运算符也能作为二元运算。作为一元运算符时执行解引用操作,作为二元运算符时执行乘法操作。它到底是几元运算符是由它的上下文决定。对于这类符号来说,它的用法互不相干,完全可以当成两个不同的符号。
组合运算符和运算对象
对于含有多个运算符的复杂表达式来说,要想理解它的含义首先要理解运算符的优先级(precedence)、结合律(associativity)以及运算对象的求值顺序(order of evaluation)。例如,下面这条表达式的求值结果依赖于表达式中运算符和运算对象的组合方式:下一节将介绍如何理解这样一条语句。
5+10*20/2;
运算对象转换
在表达式求值的过程中,运算对象常常由一种类型转换成另外一种类型。尽管一般的二元运算符都要求两个运算对象的类型相同,但是很多时候即使运算对象的类型不同也没关系,只要它们能被转换成同一种类型即可。
例如,整数能转换成浮点数,浮点数也能转换成整数,但是指针不能转换成浮点数。让人有点意外的是,小整数类型(bool、char、short等)通常会被提升(promoted)成较大的整数类型,主要是int。4.11节将详细介绍类型转换的细节。
重载运算符
C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算对象作用于类类型的运算对象时,用户可以自定义其含义。因为这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义,所以称之为重载运算符(overloaded operator)。IO库的>>和<<运算符以及string对象、vector对象和迭代器使用的运算符都是重载运算符。
我们使用重载运算符时,其包括运算符的类型和返回值的类型,都是由该运算符定义的;但是运算对象的个数、运算符的优先级和结合律都是无法改变的。
左值和右值
C++表达式分为左值和右值。这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能。
在C++中二者的区别就没那么简单了。一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。 此外虽然某些表达式的求值结果是对象,但是它们是右值。可以做一个简单的归纳:当一个对象被用作右值时,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
不同的运算符对运算对象的要求各不相同,有的需要左值运算对象、有的需要右值运算对象;返回值也有差异,有的得到左值结果、有的得到右值结果。一个重要的原则:(参见13.6节)是在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。 当一个左值被当成右值使用时,实际使用的是它的内容(值)。到目前为止,已经右几种我们熟悉的运算符是要用到左值的。
1.赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也是一个左值。
2.取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针式一个右值。
3.内置解引用运算符、下标运算符、迭代器运算符、string和vector的下标运算符的求值结果都是左值。
4.内置类型和迭代器的递增递减运算符,作用于左值运算对象,其前置版本(本书之前章节所用的形式)所得的结果也是左值。
接下来在介绍运算符的时候,我们将会注明该运算符的运算对象是否必须是左值以及其求值结果是否是左值。
使用decltype的时候,左值和右值也有所不同。如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。举个例子,假定p的类型是int*,因为解引用运算符生成左值,所以decltype(*p)的结果是int&。另一方面,因为取地址运算符生成右值,所以decltype(&p)的结果是int**
,也就是说,结果是一个指向整型指针的指针。
4.1.2 优先级和结合律
复合表达式(compound expression)是指含有两个或多个运算符的表达式。求复合表达式的值需要首先将运算符和运算对象合理的组合在一起,优先级与结合律决定了运算对象的组合方式。也就是说,它们决定了表达式中每个运算符对应的运算对象来自表达式的哪一部分。表达式中的括号无视上述规则,程序员可以使用括号将表达式的某个局部括起来使其获得优先运算。
一般来说,表达式的最终值依赖于其子表达式的组合方式。高优先级运算符的运算对象要比低优先级的运算对象更为紧密的组合在一起。如果优先级相同,则其组合规则由结合律确定。 算术运算符满足左结合律,意味着如果运算符的优先级相同,将按照从左到右的顺序组合运算对象。
括号无视优先级和结合律
括号无视普通的组合规则,表达式中括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。
优先级与结合律有何影响
从上面的学习中可以看出,优先级会影响程序的正确性,这一点在3.5.3节介绍解引用和指针运算中也有体现:
int ia[]={0,2,4,6,8};//含有5个整数的数组
int last =*(ia+4);//把last初始化为8,也就是ia[4]的值
last =*ia+4;//last=4,等价于ia[0]+4
结合律对表达式产生影响的一个典型示例是输入输出运算。4.8节将要介绍IO相关的运算符满足左结合律。这一规则意味着我们把几个IO运算组合在一条表达式中:
cin>>v1>>v2;//先读入v1,再读入v2
4.12节罗列出了全部运算符,并用双横线将它们分隔成了若干组。同一组内的运算符优先级相同,组的位置越靠前的运算符优先级越高。大多数运算符的细节将在本章剩余部分逐一介绍,还有几个运算符将在后面的内容中提及。
4.13 求值顺序
优先级规定了运算对象的组合方式,但是并没有说明运算对象按照什么顺序求值。在大多数情况下,不会明确指定求值顺序。对于如下的表达式
int i=f1()*f2();
我们知道f1和f2一定会在执行乘法之前被调用,因为毕竟相乘的是这两个函数的返回值。但是我们无法知道到底f1在f2之前调用还是f2在f1之前调用。
对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。举个例子,<<运算符没有明确规定何时以及如何对运算对象求值,因此下面的输出表达式是未定义的:
int i=0;
cout<<i<<" "<<++i<<endl;//未定义的
因为程序是未定义的,所以我们无法推断它的行为。编译器可能先求++i的值再求i的值;也可能先求i的 值再求++i的值;甚至编译器还可能做完全不同的操作。因此此表达式的行为不可预知,因此不论编译器生成什么样的代码程序都是错误的。
有四种运算符明确规定了运算对象的求值顺序。第一种是3.2.3节提到的逻辑与运算符(&&),它规定先求左侧运算对象的值,只有当左侧运算符对象的值为真时才继续求右侧运算对象的值。另外三种分别是逻辑或运算符(||)、条件运算符(?:)和逗号运算符(,)。
求值顺序、优先级、结合律
运算对象的求值顺序与优先级和结合律无关,在一条形如f()+g()*h()+j()的表达式中:
优先级规定,g()的返回值和h()的返回值相乘。
结合律规定,f()的返回值f()的返回值先与g()和h()的乘积相加,所得结果再与j()返回值相加。
对于这些函数的调用顺序没有明确规定。
如果f、g、h、j是无关函数,它们既不会改变同一对象的状态也不执行IO认为,那么函数的调用顺序不受限制。反之,如果其中某几个函数影响同一对象,则它是一条错误的表达式,将产生未定义的行为。
建议:处理复合表达式
以下两条经验准则对书写复合表达式有益:
1.拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求
2.如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。
第二条规则有一个重要的例外,当改变运算对象的子表达式本身就是另外一个子表达式的运算对象时该规则无效。 例如再表达式*++iter中,递增运算符改变iter的值,iter(已经改变)的值又是解引用运算符的运算对象。此时(或类似情况下),求值的顺序不会成为问题。因为递增运算(即改变运算对象的子表达式)必须先求值,然后才轮到解引用运算。显然这是一种很常见的用法,不会造成什么问题。
4.2 算术运算符
表4.1
运算符 | 功能 | 用法 |
---|---|---|
+ | 一元正号 | + expr |
- | 一元负号 | - expr |
* | 乘法 | expr*expr |
/ | 除法 | expr/expr |
% | 求余 | expr%expr |
+ | 加法 | expr+expr |
- | 减法 | expr-expr |
表4.1按照运算符的优先级将其分组。一元运算符的优先级最高,接下来是乘法和除法,优先级最低的是加法和减法。优先级高的运算符比优先级低的运算符组合得更紧密。上面的所有运算都满足左结合律,意味着当优先级相同时,按照从左向右的顺序进行组合。
除非另做特殊说明,算术运算符都能作用于任意算术类型以及任意能转换成为算术类型的类型。算术运算符的运算对象和求值结果都是右值。如4.11节描述的那样,在表达式求值之前,小整数类型的运算对象被提升成较大的整数类型,所有运算对象最终会转换成同一类型。
一元正号运算符、加法运算符和减法运算符都能作用于指针。3.5.3节已经介绍过二元加减法运算符的情况。当一元正号运算符作用于一个指针或算术值时,返回运算对象的一个(提升后的)副本:
int i=1024;
int k=-i;//k是-1024
bool b=true;
bool b2=-b;//b2是true!
在2.1.1节,我们指出布尔值不应该参与运算,-b就是一个很好的例子。对大多数运算符来说,布尔类型的运算对象被提升为int类型。如上所述,布尔变量b的值为真,参与运算时被提升成整数值1,对它求负后的结果是-1**.将-1再转换回布尔值并将其作为b2的初始值,显然这个初始值不等于0,转换成布尔值后应该为1.所以,b2的值是真**。
溢出和其他算术运算异常
算术表达式有可能产生未定义的结果。一部分原因是数学性质本身:例如除数是0的情况;另外一方面则源于计算机的特点:例如溢出,当计算的结果超出该类型所能表示的范围就会产生溢出。
整数相除的结果还是整数,也就是说,如果商含有小数部分,直接丢弃;运算符%俗称取余或取模,负责计算两个整数相除所得的余数,参与取余运算的运算对象必须是整数类型;
在除法运算中,如果两个运算对象的负号相同,则商为正,否则为负。C++在早期的版本允许结果为负值的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接切除小数部分)。
根据取余运算的定义,如果m和n是整数且n非0,则表达式(m/n)*n+m%n的求值结果与m相等。隐含的意思是,如果m%n不等于0,则它的符号和m相同。C++语言早期版本允许m%n的符号匹配n的符号,而且商向负无穷一侧取整,这一方式在新标准中已经被禁止使用了。除了-m导致溢出的特殊情况,其他时候(-m)/n和m/(-n)都等于-(m/n),m%(-n)等于m%n,(-m)%n等于-(m%n)。具体示例如下:
21%6;//结果是3
21/6;//结果是3
21%7;//结果是0
21/7;//结果是3
-21%-8;//结果是-5,-(21%(-8))=-(21%8)=-5
-21/-8;//结果是2,-(21/(-8))=-(-(21/8))=--2=2
21%-5;//结果是1,=21%5=1
21/-5;//结果是-(21/5)=-4
4.3 逻辑和关系运算符
关系运算符作用于算术类型或者指针类型,逻辑运算符作用于任意能转换成布尔值的类型。 逻辑运算符和关系运算符的返回值都是布尔类型。值为0的运算对象(算术类型或指针类型)表示假,否则表示真。对于这两类运算符来说,运算对象和求值结果都都是右值。
结合律 | 运算符 | 功能 | 用法 |
---|---|---|---|
右 | ! | 逻辑非 | !expr |
左 | < | 小于 | expr<expr |
左 | <= | 小于等于 | expr<=expr |
左 | > | 大于 | expr>expr |
左 | >= | 大于等于 | expr>=expr |
左 | == | 相等 | expr==expr |
左 | != | 不相等 | expr!=expr |
左 | && | 逻辑与 | expr&&expr |
左 | || | 逻辑或 | expr|| expr |
逻辑与和逻辑或运算符
逻辑与和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的值时才会计算右侧运算对象的值。对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。对于逻辑或运算来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。这种策略叫做短路求值。
在第三章中我们用到了逻辑与运算符,它们的左侧运算对象是为了确保右侧运算对象求值过程中的正确性和安全性,例如85页的循环条件:
index!=s.size()&&!isspace(s[index])
我们首先先检查index是否到达string对象的末尾,以此确保只有当index在合理范围之内时才会计算右侧运算对象的值。
举一个使用逻辑或运算符的例子,假定有一个存储着若干string对象的vector对象,要求输出string对象的内容并且在遇到空字符串或者以句号介绍的字符串时进行换行。使用基于范围的for循环处理string对象的每个元素:
//s是对常量的引用;元素既没有被拷贝也不会被改变
for(const auto &s:text){//对于text的每个元素,输出当前元素
cout<<s;
if(s.empty()||s[s.size()-1]=='.')//遇到空字符串或者以句号结束的字符串时进行换行
cout<<endl;
else
cout<<" ";
if语句的条件部分首先检查s是否是一个空string,如果是,则不论右侧运算对象的值如何都应该换行。当string对象的值非空时,才应该检查它是否以句号结束。
值得注意的是,s被声明成了对常量的引用。因为text的元素是string对象,可能非常大,所以将s声明成引用类型可以避免对元素的拷贝(提高程序运行速率);又因为不需要对string对象进行写操作,所以s被声明成对常量的引用。
逻辑非运算符
之前我们曾经在3.2.2节使用过这个运算符。下面再举一个例子,假设vec是一个整数类型的vector对象,可以使用逻辑非运算符将empty函数的返回值取反从而检查vec是否含有运算:
//输出vec的首元素(如果有的话)
if(!vec.empty())
cout<<vec[0];
关系运算符
关系运算符都满足左结合律,因为关系运算符求值结果是布尔值,所以将几个关系运算符连写在一起会产生意想不到的结果:
if(i<j<k)//拿i<j的布尔值结果与k比较,若k大于1则为真
若想实现我们的目的,其实应该使用下面的表达式:
if(i<j&&j<k)
相等性测试与布尔值字面值
我们之前其实使用过。如果想测试一个算术对象或指针对象的真值,最直接的方法就是将其作为if语句的条件:
if(val)//如果val是任意的非0值,条件为真
if(!val)//如果val是0,条件为真
在上面的两个条件中,编译器都将val转换成布尔值。如果val非0则第一个条件为真,如果val的值为0则第二个条件为真。
有时我们会试图将上面的真值测试写成如下形式:
if(val==true)//只有当val等于1时条件才为真
但这种写法存在两个问题:首先与之前的代码相比,上面的这种写法较长而且不太直接(尽管大家都认为缩写的形式对于初学者来说有点难以理解);更重要的一点是,如果val不是布尔值,这样的比较就失去了原来的意义。
如果val不是布尔值,那么进行比较之前会首先把true转换成val的类型。也就是说,如果val不是布尔值,则代码可以改写成如下形式:
if(val==1)
正如我们已经非常熟悉的那样,当布尔值转换成其他算术类型时,false转换成0而true转换成1。如果真想知道val的值是否是1,应该直接写出1这个数值来,而不要与true比较。进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值true和false来作为运算对象。
4.4 赋值运算符
赋值运算符的左侧运算对象必须是一个可以修改的左值。赋值运算的结果是它的左侧运算对象,并且是一个左值。相应的,结果的类型就是左侧运算对象的类型。如果赋值运算符的左右两个运算对象的类型不同,则右侧运算对象将转换成左侧运算对象的类型。
C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象:
k={3.14};//错误,窄化转换
vector<int> vi;
vi={0,1,2,3,4,5,6,7,8,9};
如果左侧运算对象是内置类型,那么初始值列表最多只能包含一个值,而且该值即使转换的话其所占空间也不应该大于目标类型的空间。
对于类类型来说,赋值运算的细节由类本身决定。对于vector来说,vector模板重载了赋值运算符并且可以接收初始值列表,当赋值发生时用右侧运算对象的元素替换左侧运算对象的元素。
无论左侧运算对象的类型是什么,初始值列表都可以为空。此时,编译器创建一个值初始的临时量并将其赋给左侧运算对象。
赋值运算满足右结合律
这一点与其他二元运算符不太一样:
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对象
因为ival和pval的类型不同,而且pval的类型(int*)无法转换成ival类型,所以尽管这个值能赋给任何对象,但是第一条赋值语句依然非法。
与之相反,第二条赋值语句合法。这是因为字符串字面值可以转换成string对象并赋给s2,而s2和s1的类型相同,所以s2的值可以继续赋给s1。
赋值运算符优先级较低
赋值运算符会经常出现在条件语句中,因为它的优先级较低,所以我们经常需要给它加上括号使其符合我们的原意。
int i;
while((i=get_value())!=42)
不断循环读取数据直至遇到42为止。其处理过程是首先将get_value函数的返回值赋给i,然后比较i和42是否相等。
切勿混淆相等运算符和赋值运算符
C++语言允许使用赋值运算作为条件,但这一特性可能带来意想不到的后果:
if(i=j)
此时,if语句的条件部分把j的值赋给i,然后检查赋值的结果是否为真。如果j不为0,条件将为真。然而程序员的初衷很可能是想判断i和j是否相等。
复合赋值运算符
我们经常需要对对象施加某种运算,然后把计算结果再赋给该对象。举个例子(1.4.2节的求和程序):
int sum=0;
for(int val=1;val<=10;++val)
sum+=val;
这种复合操作不仅对加法来说很常见,常常也可以用在其他算术元年是否或4.8节要介绍的位运算符。每种运算符都有相应的复合赋值形式。任意一种复合运算符都完全等价于a=a op b; 唯一的区别是左侧运算对象的求值次数:使用复合运算符只求值一次,使用普通的运算符则求值两次。 一次是作为右边子表达式的一部分求值,另一次是作为赋值运算的左侧运算对象求值。其实在很多地方,这种区别对于程序性能有些许影响外,几乎可以忽略不计。
4.5 递增和递减运算符
这两种符号为加一减一操作提供了一种简洁的书写形式。此外,它们还可以应用于迭代器。因为迭代器本身不支持算术运算,所以递增递减运算符除了书写简洁外还是必须的。
递增和递减运算符分为两种版本:前置和后置。前置运算符首先将运算对象加一或减一,然后将改变后的对象作为求值结果。后置版本也会将运算对象加一或减一,但求值结果时运算对象改变之前那个值的副本。这两种运算符必须用于左值运算对象。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
建议:除非必须,否则不用递增递减运算符的后置版本
有C语言背景的读者可能对优先使用前置版本递增运算符有所疑问,其实原因非常简单:前置版本的递增运算符避免了不必要的工作,它把值加一后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。
在一条语句中混用解引用和递增运算符
如果我们想在一条复合表达式中既将变量加一或减一又能使用它原来的值,这时就可以使用递增和递减运算符的后置版本。
举一个例子,可以使用后置的递增运算符来控制循环输出一个vector对象内容直至遇到(但不包括)第一个负值为止:
auto pbeg=v.begin();
//输出元素直至遇到第一个负值为止
while(pbeg!=v.end()&&*beg>=0)
cout<<*pbeg++<<endl;//输出当前值并将pbeg向前移动一个元素
对于刚接触C++和C的程序员来说,*pbeg++不太容易理解。这种写法其实非常普遍,所以程序员一定要理解其含义。
后置递增运算符的优先级高于解引用运算符,因为 *pbeg++
等价于*(pbeg++)
。pbeg++先把pbeg的值加一,然后返回pbeg的初始值的副本作为其求值结果,此时解引用运算符的运算对象是pbeg未增加之前的值。最终,这条语句输出pbeg开始时指向的那个元素,并将指针向前移动一个位置。
这里我们做一个总结:
前缀递增递减和*优先级相同,从右到左;
后缀递增递减比前缀优先级高,从左到右。
后缀递增递减比解引用优先级高,从右到左。
*++p
:p先自+,然后*p,最终为3——马上改变指针,*下一个指针
*p++
:值为arr[0],即1,该语句执行完毕后,p指向arr[1] ————之后改变指针,*原数据,指针++
*(p++)
:效果等同于*p++ ——之后改变指针,*原数据,指针++
(*p)++
:先*p,即arr[0]=1,然后1++,该语句执行完毕后arr[0] =2——不改变指针,数据++
++*p
:先*p,即arr[0]=1,然后再++,最终为2——不改变指针,++数据
**只有++(*p),(p)++,++p使指针的位置不变,改变数组的原始数值,
其他的任何情况都是指针指数组的下一个,指针的位置+1
建议:简洁可以成为一种美德
上面我们介绍的形如*pbeg++
的表达式一开始可能不太容易理解,但其实这是一种被广泛使用的、有效的写法。当对这种形式熟悉之后,书写
cout<<*iter++<<endl;
要比书写下面这种等价语句更简洁、也更少出错
cout<<"iter<<endl;
++iter;
我们应该不断研究和适应这种写法直到对它们的含义一目了然。大多数C++程序追求简洁、摒弃冗长,因此C++程序员应该习惯这种写法。而且一旦熟练掌握这种写法之后,程序出错的可能性也会降低。
运算对象可按任意顺序求值
大多数运算符都没有规定运算对象的求值顺序,这在一般情况下不会有什么影响。然而,如果一条子表达式改变了某个运算对象的值,另一条子表达式又要使用该值的话,运算对象的求值顺序就很重要了。 因为递增递减运算符会改变运算对象的值,所以要提防在复合表达式中错用这两个运算符。
为了说明这个问题,我们将重写3.4.1节那个使用for循环将输入的第一个单词改写成大写形式的程序:
for(auto it =s.begin();it!=s.end()&&!isspace(*it);++it)
*it=toupper(*it);//将当前字符改成大写形式
在上述程序中,我们将解引用it和递增it两项任务分开来完成。如果用一个看似等价的while循环进行代替:
//该循环的行为是未定义的
while(beg!=s.end()&&!isspace(*beg))
*beg=toupper(*beg++);//错误,该赋值语句未定义
问题在于:赋值运算符左右两端的运算对象都用到了beg,并且右侧的运算对象还改变了beg的值,所以该赋值语句是未定义的。编译器可能按照下面的任意一种思路处理该表达式,也可能采用别的什么方式处理它:
*beg=toupper(*beg);//如果先求左边的值
*(beg+1)=toupper(*beg);//如果先求右边的值
4.6 成员访问运算符
点运算符和箭头运算符都可用于访问成员,其中点运算符获取类对象的一个成员;箭头运算符与点运算符有关,表达式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.7 条件运算符
条件运算符(?:)允许我们把简单的if-els逻辑嵌入到单个表达式中,条件运算符按照如下形式使用:
cond?expl1:expr2;
其中cond是判断条件的表达式,而expr1和expr2是两种类型相同或可能转换为某个公共类型的表达式。条件运算符的执行过程是:首先求cond的值,如果条件为真对expl1求值并返回该值,否则对expl2求值并返回该值。举个例子,我们可以哦那个条件运算符判断成绩是否合格:
string finalgrade=(grade<60)? "fail" :"pass";
条件部分判断成绩是否小于60。如果小于则表达式结果为fail,否则为pass。有点类似于逻辑与运算符和逻辑或运算符(&&和||),条件运算符只对expr1和expr2中的一个求值。当条件运算符的两个表达式都是左值或能转换成同一种左值类型时,运算结果为左值;否则运算的结果为右值。
嵌套条件运算符
允许在条件运算符内部嵌套另一个条件运算。也就是说,条件运算符可以作为另一个条件运算符的cond或expr。举个例子,使用一对嵌套的条件运算符可以将成绩分为三档:优秀(high pass)、合格(pass)和不合格(fail):
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 ? "fail" : "pass";//错误试图比较cout和60
在第二条语句中,grade和60的比较结果是<<运算符的运算对象,因此如果grade<60为真输出1,否则输出0。<<运算符的返回值是cout,接下来cout作为条件运算符的条件。也就是说,第二条表达式等价于:
cout<<(grade<60);//输出1或者0
cout ? "fail" : "pass";//根据cout的值是true还是false产生对于的字面值
因为第三天表达式等价于下面的语句,所以它是错误的:
cout<<grade;//小于运算符的优先级低于移位运算符,所以先输出grade
cout<60 ? "fail" : "pass" ;//然后比较cout和60,错误。
4.8 位运算符
位运算符用于整数类型的运算对象,并把运算对象看成是二进制位的集合。位运算符提供检查和设置二进制位的功能,如17.2节将要介绍的,一种名为bitset的标准库类型也可以表示任意大小的二进制位集合,所以位运算符同样能用于bitset类型。
表4.3:位运算符(左结合律)
运算符 | 功能 | 用法 |
---|---|---|
~ | 位求反 | ~ expr |
<< | 左移 | expr1<<expr2 |
>> | 右移 | expr1>>expr2 |
& | 位与 | expr&expr |
^ | 位异或 | expr^expr |
| | 位或 | expr| expr |
一般来说,如果运算对象是“小整型”,则它的值会被自动提升成较大的整数类型。运算对象可以是带符号的,也可以是无符号的。如果运算对象是带符号的且它的值为负,那么位运算符如何处理运算对象的符号位依赖于机器。而且,此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。关于符号位如何处理没有明确的规定,所以强烈建议仅仅将位运算符用于处理无符号类型。
移位运算符
之前在处理输入和输出操作时,我们已经使用过标准IO库定义的<<、>>运算符的重载版本。这两种运算符的内置含义是对其运算对象执行基于二进制的移动操作,首先令其左侧运算对象的内容按照右侧运算对象的要求移动指定位数,然后将经过移动的(可能还进行了提升)左侧运算对象的拷贝作为求值结果。 其中右侧的运算对象一定不能为负,而且值必须严格小于结果的位数,否则就会产生未定义的行为。二进制位或者向左移动(<<)或者向右移(>>),移出边界之外的位就被舍弃掉了。
左移运算符在右侧插入值为0的二进制位。右移运算符(>>)的行为则依赖于其左侧运算对象的类型:如果该运算符对象是无符号类型,在左侧插入值为0的二进制位;如果是带符号类型,在左侧插入符号位的副本或值为0的二进制位,如何选择要视具体环境而定。
位求反运算符
它将运算对象逐个位求反后生成一个新值。
unsigned char bits=0227;
~bits
char类型的运算对象首先提升成int类型,提升时运算对象原来的位保持不变,往高位(high order position)添加0即可。因此在本例中,首先将bits提升成int类型,增加24个高位0,随后将提升后的值逐个求反。
位与、位或、位异或运算符
与(&)、或(|)、异或(^)运算符在两个运算对象上逐位执行相应的逻辑操作。
unsigned char b1=0145;//01100101
unsigned char b2=0257;//10101111
b1&b2; //00100101
b1|b2; //11101111
b1^b2; //11001010相同为0不相同为1
使用位运算符
举一个使用位运算符的例子:假设班中有30个学生,每周都会对学生进行一次小测验,测验的结果只有通过和不通过两种。为了更好地追踪测验的结果,我们用一个二进制位代表某个学生在一次测验中是否通过,显然全班的测验结果可以用一个无符号整数来表示:
unsigned long quiz1=0;//我们把这个值当成是位的集合来使用
定义quize1的类型是unsigned long,这样quiz1在任何机器上都将至少拥有32位;给quiz1赋个明确的初始值,使得它的每一位在开始时都有同样且固定的值。
教师必须有权设置并检查每一个二进制位。例如,我们需要对序号27的学生对应的位进行设置,以表示他通过了测试。为了达到这一目的,首先创建一个值,该值只有第27位是1其他位都是0,然后将这个值与quiz1进行位或运算,这样就能强行将quiz1的第27位设置为1,其他位都保持不变。
为了实现本例的目的,我们将quiz1的低阶位赋值为0、下一位赋值为1,一次类推,最后统计quiz1各个位的情况。
使用左移运算符和一个unsigned long类型的整数字面值1,就能得到一个表示学生27通过了测验的数组:
1UL<<27//生成一个值,该值只有第27为为1
1UL的低阶位上只有一个1,除此之外(至少)还有31个值为0的位。之所以用unsigned long 类型,是因为int类型只能确保占用16位,而我们至少需要27位。上面这条表达式通过在值为1的二进制后面添加0,使得他向左移动了27位。
接下来将所得的值与quiz1进行位或运算。为了同时更新quiz1的值,使用一条复合赋值语句:
quiz1 |= 1UL<<27;//表示学生27通过了测验
//等价于
quiz1 = quiz1 | 1UL<<27;
假定教师在重新核对测验结果时发现学生27实际上并没有通过测验,他必须要把第27位的值置为0.此时我们需要使用一个特殊的整数,它的第27位是0、其他所有位都是1.将这个值与quiz进行位运算就能实现目的了:
quiz &=(1UL<<27);//学生27没有通过测验
通过之前的值按位求反得到一个新值,除了27位以外都是1,只有27位的值是0。随后将该值与quiz1进行位运算,所得结果除了第27位外都保持不变。
最后我们试图检查学生27测验的情况到底怎么样:
bool status =quiz1&(1UL<<27);//学生27是否通过了测验
我们将quiz1和一个只有第27位是1的值按位求与,如果quiz1的第27位是1,计算的结果就是非0;否则就是0。
移位运算符(又叫IO运算符)满足左结合律
尽管很多程序员从未直接用过位运算符,但是几乎所有人都用过它们的重载版本来进行IO操作。重载运算符的优先级和结合律都与它们的内置版本一样,因此即使程序员用不到移位运算符的内置含义,也仍然有必要理解其优先级和结合律。
因为移位运算符满足左结合律,所以表达式
cout<<"hi"<< " there"<<endl;
的执行过程实际上等同于:
((cout<<"hi")<<" there")<<endl;
移位运算符的优先级不高不低,介于中间:比算术运算符的额优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。 因此在一次使用多个运算符时,有必要在适当的地方加上括号使其满足我们的要求。
cout<<42+10;//正确,+的优先级更高,因此输出求和结果
cout<<(10<42);//正确,括号使运算符按照我们的期望组合在一起,输出1
cout<<10<42;//错误,试图比较cout和42
4.9 sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的自己数。sizeof运算符满足右结合律 ,其所得的值是一个size_t类型的常量表达式。运算符的运算对象有两种形式:
sizeof (type)
sizeof expr
在第二种形式中,sizeof返回的是表达式结果类型的大小。与众不同的是,sizeof并不计算其运算对象的值。
Sales_data data,*p;
sizeof(Sales_data);//存储Sales_data类型的对象所占的空间大小
sizeof data;//data类型的大小,即sizeof(Sales_data)
sizeof p;//指针所占空间大小
sizeof *p;//p所指类型的空间大小,即sizeof(Sales_data)
sizeof data.revenue;//Sales_data的revenue成员对应类型的大小
sizeof Sales_data::revenue;//另一种获取revenu大小的方式
这些例子中最有趣的是sizeof *p
。首先,因为sizeof满足右结合律并且与*运算符的优先级一样,所以表达式按照从右向左的顺序组合。也就是说,它等价于sizeof(*p)。其次,因为sizeof不会实际求运算对象的值,即使p是一个无效(即未初始化)的指针也不会有什么影响。在sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。sizeof不需要真的解引用指针也能知道它所指对象的类型。
C++新标准允许我们使用作用域函数来获取成员的大小。 通常情况下只有通过类的对象才能访问到类的成员,但是sizeof运算符无须我们提供一个具体的对象,因为要想知道类成员的大小无须真的获取该成员。
sizeof运算符的结果部分地依赖于其作用的类型:
对char或者类型为char的表达式执行sizeof运算,结果得1。
对引用类型执行sizeof运算符得到被引用对象所占用空间的大小。
对指针执行sizeof运算得到指针本身所占空间的大小。
对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需要有效。
对数组执行sizeof 运算符得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。注意sizeof运算不会把数组转换成指针来处理。
对string对象或vec对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中元素占用了多少空间。
因为执行sizeof运算得到整个数组的大小,所以可以用数组的大小除以单个元素的大小得到数组中元素的个数:
//sizeof(ia)/sizeof(*ia)返回ia的元素数量
constexpr size_t sz=sizeof(ia)/sizeof(*ia);
int arr2[sz];//正确,sizefo返回一个常量表达式,参见2.4.4节
因为sizeof的返回值是一个常量表达式,所以我们可以用sizeof的结果声明数组的维度。
4.10 逗号运算符
逗号运算符(comma operato)含有两个运算对象,按照从左到右的顺序依次求值。和逻辑与、逻辑或以及条件运算符一样,逗号运算符也规定了运算对象求值的顺序。
对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符的真正结果是右侧表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值。
逗号运算符经常被用在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;
这个循环在for语句的表达式中递增ix、递减cnt,每次循环迭代ix和cnt相应改变。只要ix满足条件,我们就把当前元素设置成cnt的当前值。
4.11 类型转换
在C++语言中,某些类型之间有关联。如果两种类型有关联,那么当程序需要其中一种类型的运算对象时,可以用另外一种关联类型的对象或值来代替。换句话说,如果两种类型可以相互转换(conversion),那么它们就是有关联的。
举个例子,考虑下面这条表达式,它的目的是将ival初始化为6:
int ival =3.541+3;//编译器可能会警告该运算损失了精度
加法的两个运算对象类型不同:3.541的类型是double,3的类型是int。C++语言不会直接将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象的类型统一后再求值。上述类型的转换是自动执行的,无须程序员介入,有时甚至不需要程序员了解。因此,它们被称为隐式类型转换(implicit conversion)。
算术类型之间的隐式转换被设计得尽可能避免损失精度。很多时候,如果表达式中既有整数类型的运算对象也有浮点数类型的运算对象,整型会转换成浮点数。在上面的例子中,3转换成double类型,然后执行浮点数加法,所得结果的类型是double。
接下来就是要完成初始化任务了。在初始化过程中,因为被初始化的对象的类型无法改变,所以初始值被转换成该对象的类型。仍以这个例子说明,加法运算得到double类型的结果转换成int类型的值,这个值被用来初始化ival。由double向int转换时忽略掉了小数部分,上面的表达式中,数值6被赋值给了ival。
何时发生隐式类型转换
在下面这些情况下,编译器会自动地转换运算对象的类型:
在大多数表达式中,比int类型小的整型值首先被提升为较大的整数类型。
在条件中,非布尔值转换成布尔类型。
初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
如果算术运算或关系运算的运算对象由多种类型,需要转换成同一种类型。
如第六章将要介绍的,函数调用也会发生类型转换。
4.11.1 算术转换
算术类型转换(arithmetic conversion)的含义时把一种算术类型转换成另一种算术类型。算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。例如,如果一个运算对象的类型是long double,那么不论另外一个运算对象的类型是什么都会转换成,那么不论另一个运算对象的类型是什么都会转换成long double。还有一种更普遍的情况,当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型。
整数提升
整数提升(integral promotion)负责把小整数类型转换成较大的整数类型。对于bool、char、signed char、unsigned char、short和unsigned short 等类型来说,只要它们所有可能的值都存在int里,它们就会提升成int类型;否则提升成unsigned int类型。就如我们所熟知的,布尔值false提升成0、true提升成1。
较大的char类型(wchar_t、char16_t、char32_t)提升成int、unsigned int、long 、unsigned long、long long和unsigned long long中最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值。
无符号类型的运算对象
如果某个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型。但是某个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。
像往常一样,首先执行整型提升。如果结果的类型匹配,无须进行进一步的转换。如果两个(提升后的)运算对象的类型要么都是带符号的要么都是无符号的,则小类型的运算对象转换成较大的类型。
如果一个运算对象是无符号类型,另一个是带符号类型,而且一种的无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。例如,假设两个类型分别是unsigned int和int ,这int类型的运算对象转换成unsigned int类型。需要注意的是,如果int型的值恰好为负值,其结果将以2.1.2节介绍的方法转换,并带来该节描述的所有副作用。
**剩下的一种情况是带符号类型大于无符号类型,此时转换的结果依赖于机器。**如果无符号类型的所有值都能存在该带符号类型中,则无符号类型转换成带符号类型。如果不能,那么带符号类型的运算对象转换成无符号类型。例如,如果两个运算对象的类型分别是long和unsigned int,并且int和long的大小相同,则long转换成unsigned int类型;如果long类型占用的空间比int更多,则unsigned int类型转换成long类型。
理解算术转换
要想理解算术转换,办法之一就是研究大量的例子:
bool flag;char cval;
short sval;unsigned short usval;
int ival;unsigned int uvial;
long lval;unsigned long ulval;
float fval;double dval;
3.14150L+'a';//'a'提升成int,然后该int值转换成long double
dval+ival;//ival转换成dval
dval+fval;//fval转换成dval
ival=dval;//dval转换成切除小数部分的int
flag=dval;//如果dval是0,则flag是fasle,否则是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 所占空间的大小进行转换。
第一个加法运算中,小写字母’a’是char型的字符常量,它其实能表示一个数字值。到底这个数字值是多少完全依赖于机器上的字符集,在我们的环境中,‘a’对应的数字值是97.当把‘a’和一个long double 类型的数相加时char类型的值首先提升成int类型,然后int类型的值再转换成long double 类型。最终我们把这个转换后的值与那个字面值相加。最后的两个含有无符号类型的表达式也比较有趣,他们的结果依赖于机器。
4.11.2 其他隐式类型转换
除了算术转换之外还有几种隐式类型转换,包括以下几种。
数组转换成指针 :在大多数用到数组的表达式中,数组会自动转换成指向数组首元素的指针。
当数组被用作decltype 关键字的参数,或者作为取地址符(&)、sizeof及typeid(第19.2.2节)等运算符的运算对象时,上述转换不会发生。 同样的,如果用一个引用来初始化数组,上述转换也不会发生。我们将在6.7节看到,当在表达式中使用函数函数时会发生类似的指针转换。
指针的转换 :C++还规定了几种其他的指针转换方式,包括常量整数值0或者字面值nullptr能转换成任意指针类型;指向任意非常量的指针void*;指向任意对象的指针能转换成const void*。15.2.2 节将要介绍,在有继承关系的类型间还有另外一种指针转换方式。
转换成布尔类型 :存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果为false;否则为true:
char *cp=get_string();
if(cp) //如果指针cp不是0,条件为真
while(*cp) //如果*cp不是空字符,条件为真
转换成常量 :允许将指向非常量类型的指针转换成相应的常量类型指针,对于引用也是这样。也就是说,如果T是一种类型,我们就能将指向T的指针或引用分别转换成指向const T的指针或引用:
int i;
const int &j=i;//非常量转换成const int的引用
const int *p=&i;//非常量的地址转换成cosnt的地址
int &r=j,*q=p;//错误:不允许将const转换成非常量
相反的转换并不存在,因为它试图删除掉底层const。
类类型定义的转换 类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。在7.5.4节中我们将看到一个例子,如果同时提出多个转换请求,这些请求将被拒绝。
我们之前的程序已经使用过类类型转换:一处是在需要标准库string类型的地方使用C风格字符串;另一处是在条件部分读入istream:
string s,t="a value";//字符串字面值转换成string类型
while(cin>>s)//while的条件部分把cin转换成布尔值
条件(cin>>s)读入cin的内容并将cin作为其求值结果。条件部分本来需要一个布尔类型的值,但是这里实际检查的是istream类型的值。幸好IO库定义了从istream向布尔值转换的规则,根据这一规则,cin自动地转换成布尔值。所得的布尔值到底是什么由输入流的状态决定。如果最后一次读入成功,转换得到的布尔值就是true;相反,如果最后一次读入不成功,转换得到的布尔值是fasle。
4.11.3 显式转换
有时我们希望显式地将对象强制转换成另外一种类型。例如,我们想在下面的代码中执行浮点数除法:
int i,j;
double slope=i/j;
就要使用某种方法将i/j显式地转换成double,这种方法称作强制类型转换(cast)。虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。
命名的强制类型转换
一个命名的强制类型转换具有如下形式:
cast-name<type>(expression);
其中,type是转换的目标类型而expression是要转换的值。如果type是引用类型,则结果是左值。 cast-name 是 static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。dynamic_cast支持运行时类型识别,我们将在19.2节中对其做更详细的介绍。cast-name指定了执行的是哪种转换。
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。 例如,通过将一个运算对象强制转换成double类型就能使表达式执行浮点数除法:
//进行强制类型转换以便执行浮点数除法
double slope=static_cast<double>(j)/i;
当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。此时,强制类型转换告诉程序的作者和编译器:我们知道并且不在乎潜在的精度损失。一般来说。如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警示信息;但是当我们执行了显式类型转换后,警告信息就会被关闭。
static_cast对于编译器无法自动执行的类型转换也非常有用。例如,我们可以使用static_cast找回存在于void*指针中的值:
void* p=&d;//正确,任何非常量对象的值都能存入void*
double *p=static_cast<double*>(p);//正确,将void*转换回初始的指针类型
当我们将指针存放在void*中,并且使用static_cast将其强制转换回原来的类型时,应该确保指针的值不变。也就是说,强制类型转换的结果与原始的地址值相等,因此我们必须保证转换后所得的类型就是指针所指的类型。类型一旦不符,将产生未定义的后果。
const_cast
const_cast只能改变运算对象的底层const:
const char *pc;
char *p=const_cast<char*>(pc);//正确,但是通过p写值时未定义的行为
对于常量对象转换成非常量对象的行为,我们一般称其为去掉const性质(cast away the const)。一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。然而对象是一个常量,再使用const_cast执行写操作就会产生未定义的结果。
只有cosnt_cast能改变表达式的常量属性,其他形式的命名强制转换改变表达式的常量属性都将引发编译器错误。同样的,也不能用const_cast改变表达式的类型:
const char *cp;
char *q=static_cast<char*>(cp);//错误,static_cast不能转换掉const性质
static_cast<string>(cp);//正确,字符串字面值转换成string类型
const_cast<string>(cp);//错误,const_cast只改变常量属性
const_cast常常用于有函数重载的上下文中,关于函数重载将在6.4节进行详细介绍。
reinterpret_cast
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。举个例子,假设有如下的转换
int *ip;
char *pc=reinterpret_cast<char*>(ip);
我们必须牢记pc所指的真实对象是一个int而非字符,如果把pc当成普通的字符指针使用就可能在运行时发生错误。例如:
string str(pc);//可能导致异常的运行时行为
使用reinterpret_cast是非常危险的,用pc初始化str的例子很好的证明了这一点。其中的关键问题是类型改变了,但编译器没有给出任何警告或者错误的提示信息。 但我们用一个int的地址初始化pc时,由于显式地声称这种转换合法,所以编译器不会发出任何警告或错误信息。接下来再使用pc时就会认定它的值就是char*类型,编译器没法知道它实际存放的是指向int的指针。最终的结果是,在上面的例子中虽然用pc初始化str没什么实际意义,甚至还可能引发更糟糕的后果,但仅从语法上而言这种操作无可指摘。查找这类问题的原因非常困难,如果将ip强制转换成pc的语句和 用pc初始化string对象的语句分属不同文件就更是如此。
reinterpret_cast本质上依赖于机器。要想安全地使用reinterpret_cast必须对涉及的类型和编译器实现转换的过程非常了解。
建议:避免强制类型转换
强制类型转换干扰了正常的类型检查,因此我们强烈建议程序员避免使用强制类型转换。这个建议对于reinterpret_cast尤其适用,因为此类类型转换总是充满了风险。在有重载函数的 上下文中使用const_cast无可厚非,关于这一点将在6.4节中详细介绍;但是在其他情况下适用const_cast也就意味着程序存在某种设计缺陷。其他强制类型转换语句,都应该反复斟酌能否以其他方式实现相同的目标。就算实在无法避免,也应该尽量限制类型转换值的作用域,并且记录对相关类型的所有假定,这样可以减少错误发生的机会。
旧式的强制类型转换
在早期的C++语言中,显式地进行强制类型转换包含两种形式:
type(expr);//函数形式的强制类型转换
(type) expr;//c风格的强制类型转换
根据所涉及的类型不同,旧式的强制类型转换分别具有const_cast,static_cast,reinterpret_cast相似的行为。当我们在某处执行旧式的强制类型转换时,如果换成const_cast,static_cast也合法,则其行为与对应的命名转换一致。如果替换后不合法,则旧式强制类型转换执行与reinterpret_cast类似的功能:
char *pc=(char*) ip;//ip是指向整数的指针
与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不那么清晰明了,容易被看漏,所以一旦转换过程出现问题,追踪起来也更加困难。
4.12 运算符优先级表
见书第166页。
小结
C++语言提供了一套丰富的运算符,并定义了这些运算符作用于内置类型的运算对象时所执行的操作。此外,C++语言还支持运算符重载的机制,允许我们自己定义运算符作用于类类型时的定义。第14章将介绍如何定义作用于用户类型的运算符。
对于含有超过一个运算符的表达式,要想理解其含义关键是要理解优先级、结合律和求值顺序。每个运算符都有其对应的优先级和结合律,优先级规定了复合表达式组合的方式,结合律则说明当运算符优先级相同 时该如何组合。
大多数运算符并不明确规定对象的求i之顺序:编译器有权自由选择先对左侧运算对象求值还是先对右侧运算对象求值。一般来说,运算对象的求值顺序对表达式的最终结果没有影响。但是,两个运算对象指向同一个对象而且其中一个改变了对象的值,就会导致程序出现不易发现的严重缺陷。
最后一点,运算对象经常从原始类型自动转换成某种关联的类型。例如,表达式中的小整型会自动提升成大整型。不论内置类型还是类类型都涉及类型转换问题。如果需要,我们还可以显式地进行强制类型转换。
习题解答
4.1.2节练习
练习4.1 表达式5+10*20/2的求值结果是多少?
解答:
105
练习4.2 根据4.12节中的表,在下述表达式的合理位置添加括号,使得添加括号后运算对象的组合顺序与添加括号前一致。
*vec.begin()
*vec.begin()+1
解答:
在本题涉及的运算符中,优先级最高的是成员运算符和函数调用符,其次是解引用运算符,最后是加法运算符。因此添加括号后等价的式子是:
*(vec.begin())
(*(vec.begin()))+1
4.13节练习
练习4.3 C++语言没有明确规定大多数二元运算符的求值顺序,给编译器优化留下了余地。这种策略实际上是在代码生成效率和程序潜在缺陷之间进行了权衡,你认为可以接收吗?
解答:
正如题目所说,这样做提高了代码生成的效率,但是可能引发潜在的缺陷。
关键是缺陷的风险有多大?我们知道,对于没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为;而如果运算对象彼此无关,它们既不会改变同一对象的状态也不执行IO认为,则函数的调用顺序不受影响。
就作者的观点来说,这样的做法在一定程度上是可以接收的,前提是在编写程序时注意之前说过的那两点。
4.2节练习
练习4.4 在下面的表达式中添加括号,说明其求值的过程及最终结果。编写程序编译该(不加括号的)表达式并输出其结果验证之前的判断。
12/34+515+24%4/2
解答:
最终的结果是91.((12/3)4)+(515)+((24%4)/2)
练习4.5 写出下列表达式的求值结果
-303+21/5
-30+321/5
30/321%5
-30/321%4
解答:
-86,-18,0,-210%4=-(210%4)=-2
练习4.6 写出一条表达式用于确定一个整数是奇数还是偶数。
解答:
num%2==0
练习4.7 溢出是何种含义?写出三条将导致溢出的表达式。
解答:
溢出是一种常见的算术运算错误。因为在计算机中存储某种类型的内存空间有限,所以该类型的表示能力(范围)也是有限的,当计算的结果超出这个范围时,就会产生未定义的数值,这种错误称为溢出。
假定编译器规定int占32位,则下面的3条表达式都将产生溢出错误:
int i=2147483647+1;
int j=-100000*300000;
int k=2015*2015*2015*2015;
4.3节练习
练习4.8 说明在逻辑与、逻辑或及相等性运算符中运算对象求值的顺序。
解答:
对于逻辑与来说,当且仅当左侧运算对象为真时,才计算右侧运算对象;对于逻辑或运算符来说,当且仅当左侧运算对象为假时才计算右侧运算对象。相等性运算符的两个运算对象都需要求值,C++没有规定其求值顺序。
练习4.9 解释在下面的if语句中条件部分的判断过程。
const char *cp="hello world";
if(cp&&*cp)
解答:
cp是指向字符串的指针,因此上式的条件部分含义是首先检查指针cp是否有效。如果cp为空指针或无效指针,则条件不满足。如果cp有效,即cp指向了内存中的某个有效地址,继续解引用指针cp并检查cp所指的对象是否为空字符’\0’,如果cp所指的对象不是空字符则条件不满足;否则不满足。
练习4.10 为while循环写一个条件,使其从标准输入中读取整数,遇到42时停止。
解答:
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。
解答:
a>b&&b>c&&c>d
练习4.12 假设i,j和k是三个整数,说明表达式i!=j<k的含义。
解答:
C++规定<,<=,>,>=的优先级高于==和!=,所以表达式的含义是先比较j和k的大小,得到一个布尔值,然后再判断i的值是否与之相等。
4.4节练习
练习4.13 在下述语句中,当赋值完成后i和d的值分别是多少?
int i;double d;
d=i=3.5;
i=d=3.5;
解答:
第一行的含义是先把3.5赋值给整数i,此时发生了自动类型转换,小数部分被丢弃,i的值为3;接着再把i的值赋给双精度浮点数d,此时d的值也是3.
第二行的含义是先把3.5赋值给双精度浮点数d,此时d的值为3.5;接着把d的值赋给整数i,此时发生自动类型转换,小数部分被舍弃,i的值为3。
练习4.14 执行下述if语句后将发生什么情况?
if(42=i)//编译错误
if(i=42)//给i赋值42,然后判断i的值是否为真
练习4.15 下面的赋值是非法的,为什么?应该如何修改?
double dval;int ival;int *pi;
dval=ival=pi=0;
解答:
参与赋值的几个变量类型不同。自右向左分析赋值操作的含义,pi=0表示一个空指针,接下来ival=pi试图把整型指针的值赋给整数,这是不符合语法规范的操作,无法编译通过。可以这么改写:
double dval;int ival;int *pi;
dval=ival=0;
pi=0;
练习4.16 尽管下列语句合法,但它们实际执行的行为可能和预期并不一样,为什么?如何修改。
if(p=getPtr()!=0)
if(i=1024)
解答:
第一行的原意是把getPtr()得到的指针赋值给p在,然后判断p是否为空指针。现在的意思却是先判断指针是否为空,如果是,则p值为1,否则为0。因为赋值运算符的优先级低于不相等运算符。要想符合原意,应该修改为:if(p=getPtr()!=0)
第二行的原意是判断i的值是否为1024,但上述语句实际上是把1024赋给i,然后以i作为if语句的条件。因为所有非0整数转换成布尔值时都对应true,所以该条件恒为真。
4.5节练习
练习4.17 说明前置递增运算符和后置递增运算符的区别。
解答:
前面已经讲解过,这里不再赘述。对于整数和指针来说,编译器可能对这种额外的工作进行了一定优化。但对于相对复杂的迭代器类型来说,这种额外的工作就消耗巨大了。建议养成使用前置版本的习惯,这样既不用担心性能问题,而且更重要的是写出的代码会更符合编程人员的初衷。
练习4.18 如果第132页那个输出vector对象元素的while循环使用前置递增运算符会得到什么结果?
解答:
首先是无法输出vec对象的第一个元素,其次当所有元素都不为负时,移动到最后一个元素的地方,程序试图继续向前移动迭代器并解引用一个根本不存在的元素。
练习4.19 假设ptr的类型是指向int的指针、vec的类型是vector<int>
、ival的类型是int,说明下面表达式是何含义?如果有表达式不正确,为什么?
ptr!=0&&*ptr++
ival++&&ival
vec[ival++]<=vec[ival]
解答:
第一行的含义是先判断ptr指针是否为空,如果不为空继续判断ptr所指的元素是否为0。如果非0,则表达式为真,否则为假。最后将指针ptr向后移动一位。该表达式从语法上是合法的,但最后指针引动操作不一定有意义。如果ptr所指的是整型数组中的某个元素,则ptr可以按照预期移动,如果ptr指向的只是一个独立的int变量,则移动操作将产生未定义的结果。
第二行的含义是先检查ival的值是否为0,如果非零则继续检查ival+1的值是否为0。当两个值都不为0时,表达式求值为真,否则为假。在4.1.3节中我们学过,如果二元运算符的两个运算对象涉及同一个对象并改变对象的值,则这是一种不好的程序写法,应该改写。所以我们应该改成ival&&((val+1)
。
第三行的含义是比较vec[ival]和vec[ival+1]的大小。与第二行一样也出现了二元运算符的两个运算对象涉及同一个对象并改变对象值得情况,应该改写成vec[ival+1]<=vec[ival]。
4.6节练习
练习4.20 假设iter的类型是vector<string>::iterator
,说明下面的表达式是否合法。如果合法,表达式的含义是什么?如果不合法,错在何处?
*iter++;
(*iter)++;
*iter.empty();
iter->empty();
++*iter;
iter++->empty();
解答:
第一行合法,后置递增运算符的优先级高于解引用运算符,该语句等价于*(iter++),iter迭代器先加一,再返回iter初始值的副本,最后解引用这个副本。其含义是解引用当前迭代器的对象内容,然后把迭代器的位置向后移动一位。
第二行非法,解引用iter得到vector对象当前的元素,结果是一个string,显然string没有后置递增操作。
第三行非法,解引用运算符的优先级低于点运算符,所以该表达式先计算iter.empty(),而迭代器并没有定义empty函数,所以无法通过编译。
第四行合法,该语句等价于(iter).empty();。解引用迭代器得到迭代器当前所指的元素,结果是一个string,string定义了empty函数,可以通过编译。
第五行非法,该表达式先解引用iter,得到迭代器当前所指元素,结果是一个string,而string并没有递增操作。
第六行合法,该语句实际上等价于(iter++).empty();,其含义是iter先自加,然后返回iter的初始值的副本用来解引用,得到一个字符串,判断该字符串是否为空。
这里我们做一个总结:
前缀递增递减和*优先级相同,从右到左;
后缀递增递减比前缀优先级高,从左到右。
*++p
:p先自+,然后*p,最终为3——马上改变指针,*下一个指针
*p++
:值为arr[0],即1,该语句执行完毕后,p指向arr[1] ————之后改变指针,*原数据,指针++
*(p++)
:效果等同于*p++ ——之后改变指针,*原数据,指针++
(*p)++
:先*p,即arr[0]=1,然后1++,该语句执行完毕后arr[0] =2——不改变指针,数据++
++*p
:先*p,即arr[0]=1,然后再++,最终为2——不改变指针,++数据
**只有++(*p),(p)++,++p使指针的位置不变,改变数组的原始数值,
其他的任何情况都是指针指数组的下一个,指针的位置+1
4.7节练习
练习4.21 编写一段程序,使用条件运算符从vector 中找到哪些元素是奇数,然后将这些奇数值翻倍。
解答:
#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;
}
练习4.22 本节的示例程序将成绩化为三档,扩展该程序使其进一步将60分到75分之间的成绩设定为low pass。要求程序包含两个版本:一个版本只使用条件运算符;另一个版本使用一个或多个if语句。哪个版本的程序更容易理解呢?为什么?
解答:
#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;
}
return 0;
}
#include <iostream>
#include <string>
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="low pass";
else if(grade>60)
finalgrade="pass";
else
finalgrade="fail";
cout<<"该成绩所处的档次是:"<<finalgrade<<endl;
cout<<"请输入您要检查的成绩"<<endl;
}
return 0;
}
练习4.23 因为运算符的优先级问题,下面这条表达式无法通过编译。根据4.12节中的表(147页)指出它的问题所在。应该如何修改?
string s="word";
string p1= s+s[s.size()-1]=='s' ? "" : "s";
解答:
首先我们要知道,条件运算符的优先级非常低,因此在使用条件运算符构成复合表达式时,必须在适当的位置添加括号。
题目中的几个运算符次序从高到低是加法运算符、相等运算符条件运算符和赋值运算符,因为这个语句的含义是先将s和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。这条语句是无法编译通过的。因为条件运算符要求两个结果表达式的类型相同或者可以互相转换。即使假设语法上通过,也就是说,第一个条件表达式的求值结果分为三种,分别是high pass、1和0。接下来根据第一个条件表达式的值求解第二个条件表达式,求值结果式fail或pass。上述求值结果显然与我们的期望式不符的。
4.8节练习
练习4.25 如果一台机器上int占32位、char占8位,用的是Latin-1字符集,其中字符q的二进制形式是01110001,那么表达式~‘q’<<6的值是什么?
解答:
在位运算符中,运算符~的优先级高于<<,因此先对q按位取反,因为位运算符的运算对象应该是整数类型,所以字符q首先转换成整数类型,char类型占8位,而int占32位,转换后的结果是 0 0 0 01110001,按位取反后得到,1 1 1 10001110,接着执行左移操作,得到1 1 11100011 10000000。
C++规定整数按照其补码形式存储,对上式求补(负数求补:除符号位以外各位取反最后加一)得到10000000 0000000 0011100 10000000,即最终的二进制形式,转换成十进制形式是-7296。
练习4.26 在本节关于测验成绩的例子中,如果使用unsigned int作为quiz1的类型会发生什么情况?
解答:
C++规定,unsigned long 在内存中至少占32位,只有就足够存放30个学生的信息。如果使用unsigned int 作为quiz1的类型,则由于C++规定unsigned int所占空间的最小值是16,所以在很多机器环境中,该数据类型不足以存放全部学生的信息,从而造成信息丢失。
练习4.27 下面表达式的结果是什么?
unsigned long u11=3,u12=7;
u11&u12
u11|u12
u11&&u12
u11||u12
解答:
u11转换位二进制形式是0 0 0 00000011,u12转换为二进制形式为0 0 0 00000111。
按位与的结果为:0 0 0 00000011。
按位或的结果为:0 0 0 00000111。
逻辑与,所有非0整数对应的布尔值都为true,所有该式等价于true&&true,结果为true。
逻辑或,所有非0整数对应的布尔值都为true,所有该式等价于true||true,结果为true。
4.9节练习
练习4.28 编写一段程序,输出每一种内置类型所占空间的大小。
解答:
#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"<<sizeof(int)<<endl;
cout<<"long\t\t"<<sizeof(long)<<endl;
cout<<"long long\t\t"<<sizeof(long long)<<endl;
cout<<"float\t\t"<<sizeof(float)<<endl;
cout<<"double\t\t"<<sizeof(double)<<endl;
cout<<"long double\t\t"<<sizeof(long double)<<endl;
return 0;
}
我们可以看到,sizeof运算符的求值结果是在内存中所占的字节数。正常情况下,求值结果应该至少等于C++规定的最小值,依赖于机器的不同,部分类型实际占用的空间会大于这个最小值。在作者的编译环境中,int占4个字节,超过了C++规定的2字节。
练习4.29 推断下面代码的输出结果并说明理由。实际允许这段程序。
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++的内置数组并没有定义成员函数size(),所以通常无法直接求得数组的容量。本题所示的方法是计算得到数组容量的一种常规方法。
sizeof§运算对象是一个指针,求值结果是指针所占空间的大小。
sizeof(*p)的运算对象是指针p所指的对象,即int变量x,所以求值结果是int值所占空间的大小。
在作者的编译环境中,int占4字节,指针也占4字节,所以本题程序的输出结果是:
10
1
练习4.30 根据4.12节中的表,在下面表达式的适当位置加上括号,使得加上括号之后的表达式的含义与原来的含义相同。
sizeof x+y
sizeof p->mem[i]
sizeof a<b
sizeof f()
解答:
第一行的含义是先求变量x所占的空间的大小,然后与变量y的值相加;因为sizeof运算符的优先级高于加法运算符的优先级,所以如果想求表达式x+y所占的内容空间,应该改为sizeof(x+y)。
第二行的含义是先定位到指针p所指的对象,然后求该对象中名为mem的数组成员第i个元素的内存大小。因为成员选择运算符的优先级高于sizeof的优先级,所以本例无须添加括号。
第三行的含义是先求变量a在内存中所占空间的大小,再把求值得到的值与变量b的值比较。因为sizeof运算符的优先级高于关系运算符的优先级,所以如果想求表达式a<b所占的内存空间,应该改为sizeof(a<b)。
第四行的含义是求函数f()返回值所占内存空间的大小,因为函数调用运算符的优先级高于sizeof的优先级,所以本例无须添加括号。
4.10节练习
练习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节的介绍我们知道,除非必须,否则不用递增(递减)运算符的后置版本。前置版本的递增运算符避免了不必要的工作,他把值加一后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改之前的值,那么后置版本的操作就是一种浪费。就本题来说,使用前置版本是更好的选择。
练习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语句包括三个部分:第一个部分定义了整型指针指向数组的首元素,并且定义了一个整数ix,赋给它初值0;第二部分判断循环终止的条件,当ix没有达到size同时指针没有指向数组最后一个元素的下一个位置时,执行循环体; 第三部分令变量ix和指针ptr分别执行递增操作。
练习4.33 根据4.12节中的表(第147页)说明下面这条表达式的含义。
someValue ? ++ix, ++y :--x, --y;
解答:
C++规定条件运算符的优先级高于逗号运算符,所以上式子实际上等价于(someValue ? ++ix,++y : --x),–y。它的求值过程时,首先判断someValue是否为真,如果为真,依次执行++ix和++y,最后执行–y。如果为假,执行–x和–y。
4.11.1节练习
练习4.34 根据本节给出的变量定义,说明在下面的表达式中将发生什么样的的类型转换:(需要注意每种运算符遵循的是左结合律还是右结合律。
if(fval)
dval=fval + ival;
dval+ival*cval;
解答:
if语句的条件应该是布尔值,因此float型变量自动转换成布尔值,转换规则是所有非0值转换为true,0转换成false。
ival转换成float,与fval求和后所得的结果进一步转换成double类型。
cval执行整型提升转换为int,与ival相乘后所得的结果转换为double类型,最后再与dval相加。
练习4.35 假设有如下定义,
char cval;
int ival;
unsigned int ui;
float fval;
double dval;
请回答在下面的表达式中发生了隐式类型转换吗?如果有,指出了。
cval='a'+3;
fval=ui-ival*1.0;
dval=ui*fval;
cval=ival+fval+dval;
解答:
字符a提升为int型,与3相加所得的结果再转换为char并赋给cval。
ival转换为double,与1.0相乘的结果是double,ui转换成double与乘法所得的结果相减,最终的结果转换为float并赋给fval。
ui转换成float,与fval相乘的结果转换为double类型并赋给dval。
ival转换为float,与fval相加所得的结果转换为double类型,再与dval相加后结果转换为char类型。
4.11.3节练习
练习4.36 假设i是int类型,d是double类型,书写表达式i*=d使其执行整数乘法而非浮点类型的乘法。
解答:
任何具有明确定义的类型转换,只有不包含底层const,都可以使用static_cast。我们使用它把double类型的变量d强制转换成int类型,就可以令表达式执行整数类型的乘法:
i*=static_cast<int>(d);
练习4.37 用命名的强制类型转换改写下列旧式的转换语句。
int i;double d;const string *ps;char *pc;void *pv;
pv=(void*) ps;
i=int(*pc);
pv=&d;
pc=(char*) pv;
解答:
利用static_cast执行强制类型转换,对于底层const则使用const_cast。
pv=static_cast<void*>(const_cast<string*>(ps));
i=static_cast<int*>(*pc);
pv=static_cast<void*>(&d);
pc=static_cast<char*>(pc);
说明下面这条表达式的含义
double slope=static_cast<double>(j/i);
解答:
把j/i的值强制类型转换成double,然后赋值给slope。请注意,如果i和j都是int,则j/i的结果仍然是int。