2 试列出网站开发建设的步骤,征婚网站建设,网站文章伪原创怎么做,外链的论坛网站表达式由一个或多个运算对象(operand)组成#xff0c;对表达式求值将得到一个结果(result)字面值和变量是最简单的表达式(expression),其结果就是字面值和变量的值。把一个运算符(operator)和一个或多个运算对象组合起来可以生成较复杂的表达式
4.1基础
有几个基础概念对表达…表达式由一个或多个运算对象(operand)组成对表达式求值将得到一个结果(result)字面值和变量是最简单的表达式(expression),其结果就是字面值和变量的值。把一个运算符(operator)和一个或多个运算对象组合起来可以生成较复杂的表达式
4.1基础
有几个基础概念对表达式的求值过程有影响它们涉及大多数(甚至全部)表达式。本节先简要介绍这几个概念后面的小节将做更详细的讨论。
4.1.1基本概念
C定义了一元运算符和二元运算符。作用于一个运算对象的运算符是一元运算符如取地址符()和解引用符(*);作用于两个运算对象的运算符是二元运算符如相等运算符()和乘法运算符(*)。除此之外还有一个作用于三个运算对象的三元运算符。函数调用也是一种特殊的运算符它对运算对象的数量没有限制。一些符号既能作为一元运算符也能作为二元运算符。以符号*为例作为一元运算符时执行解引用操作作为二元运算符时执行乘法操作。一个符号到底是一元运算符还是二元运算符由它的上下文决定。对于这类符号来说它的两种用法互不相干完全可以当成两个不同的符号。
组合运算符和运算对象
对于含有多个运算符的复杂表达式来说要想理解它的含义首先要理解运算符的优先级(precedence),结合律(associativity)以及运算对象的求值顺序例如下面这条表达式的求值结果依赖于表达式中运算符和运算对象的组合方式510*20/2;乘法运算符(*)是一个二元运算符它的运算对象有4种可能10和20、10和20/2、15和20、15和20/2。下一节将介绍如何理解这样一条表达式。
运算对象转换
在表达式求值的过程中运算对象常常由一种类型转换成另外一种类型。例如尽管一般的二元运算符都要求两个运算对象的类型相同但是很多时候即使运算对象的类型不相同也没有关系只要它们能被转换(参见2.1.2节第32页)成同一种类型即可。类型转换的规则虽然有点复杂但大多数都合乎情理、容易理解。例如整数能转换成浮点数浮点数也能转换成整数但是指针不能转换成浮点数。让人稍微有点意外的是小整数类型(如bool、char,short等)通常会被提升(promoted)成较大的整数类型主要是int。4.11节(第141页)将详细介绍类型转换的细节。
重载运算符
C语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时用户可以自行定义其含义。因为这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义所以称之为重载运算符。IO库的和运算符以及string对象、vector对象和迭代器使用的运算符都是重载的运算符。我们使用重载运算符时其包括运算对象的类型和返回值的类型都是由该运算符定义的但是运算对象的个数、运算符的优先级和结合律都是无法改变的。
左值和右值
C的表达式要不然是右值,要不然就是左值。这两个名词是从C语言继承过来的原本是为了帮助记忆左值可以位于赋值语句的左侧右值则不能。std::move 将右值转化为左值进行计算在C语言中二者的区别就没那么简单了。一个左值表达式的求值结果是一个对象或者一个函数然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。此外虽然某些表达式的求值结果是对象但它们是右值而非左值。可以做一个简单的归纳当一个对象被用作右值的时候用的是对象的值内容当对象被用作左值的时候用的是对象的身份在内存中的位置。不同的运算符对运算对象的要求各不相同有的需要左值运算对象、有的需要右值运算对象返回值也有差异有的得到左值结果、有的得到右值结果。一个重要的原则参见13.6节第470页将介绍一种例外的情况是在需要右值的地方可以用左值来代替但是不能把右值当成左值也就是位置使用。当一个左值被当成右值使用时实际使用的是它的内容值。到目前为止已经有几种我们熟悉的运算符是要用到左值的。赋值运算符需要一个非常量左值作为其左侧运算对象得到的结果也仍然是一个左值。取地址符参见2.3.2节第47页作用于一个左值运算对象返回一个指向该运算对象的指针这个指针是一个右值。内置解引用运算符、下标运算符参见2.3.2节第48页参见3.5.2节第104页、迭代器解引用运算符、string和vector的下标运算符参见341节第95页参见3.2.3节第83页参见3.3.3节第91页的求值结果都是左值。内置类型和迭代器的递增递减运算符参见1.4.1节第11页参见3.4.1节第96页作用于左值运算对象其前置版本本书之前章节所用的形式所得的结果也是左值。接下来在介绍运算符的时候我们将会注明该运算符的运算对象是否必须是左值以及其求值结果是否是左值。使用关键字decltype参见2.5.3节第62页的时候左值和右值也有所不同。如果表达式的求值结果是左值decltype作用于该表达式不是变量得到一个引用类。举个例子假定p的类型是int*,因为解引用运算符生成左值所以decltype*p的结果是int。另一方面因为取地址运算符生成右值所以decltypep的结果是int**,也就是说结果是一个指向整型指针的指针。
4.1.2优先级与结合律
复合表达式compoundexpression是指含有两个或多个运算符的表达式。求复合表达式的值需要首先将运算符和运算对象合理地组合在一起优先级与结合律决定了运算对象组合的方式。也就是说它们决定了表达式中每个运算符对应的运算对象来自表达式的哪一部分。表达式中的括号无视上述规则程序员可以使用括号将表达式的某个局部括起来使其得到优先运算。一般来说表达式最终的值依赖于其子表达式的组合方式。高优先级运算符的运算对象要比低优先级运算符的运算对象更为紧密地组合在一起。如果优先级相同则其组合规则由结合律确定。例如乘法和除法的优先级相同且都高于加法的优先级。因此乘法和除法的运算对象会首先组合在一起然后才能轮到加法和减法的运算对象。算术运算符满足左结合律意味着如果运算符的优先级相同将按照从左向右的顺序组合运算对象根据运算符的优先级表达式34*5的值是2 3 ,不是35。根据运算符的结合律表达式20 -1 5 -3 的值是2 , 不是8。
括号无视优先级与结合律
括号无视普通的组合规则表达式中括号括起来的部分被当成一个单元来求值然后再与其他部分一起按照优先级组合。例如对上面这条表达式按照不同方式加上括号就能得到4种不同的结果
先级与结合律有何影响
由前面的例子可以看出优先级会影响程序的正确性这一点在3.5.3节(第107页)介绍的解引用和指针运算中也有所体现如果想访问ia4位置的元素那么加法运算两端的括号必不可少。一旦去掉这对括号*ia就会首先组合在一起然后4再与*ia的值相加。结合律对表达式产生影响的一个典型示例是输入输出运算4.8节(第138页)将要介绍IO相关的运算符满足左结合律。这一规则意味着我们可以把几个IO运算组合在一条表达式当中cinvlv2;//先读入vl,再读入v24.12节(第147页)罗列出了全部的运算符并用双横线将它们分割成若干组。同一组内的运算符优先级相同组的位置越靠前组内的运算符优先级越高。例如前置递增运算符和解引用运算符的优先级相同并且都比算术运算符的优先级高。表中同样列出了每个运算符在哪一页有详细的描述有些运算符之前已经使用过了大多数运算符的细节将在本章剩余部分逐一介绍还有几个运算符将在后面的内容中提及。
4.1.3求值顺序
优先级规定了运算对象的组合方式但是没有说明运算对象按照什么顺序求值。在大多数情况下不会明确指定求值的顺序。对于如下的表达式intifl()*f2();我们知道fl和f2-定会在执行乘法之前被调用因为毕竟相乘的是这两个函数的返回值。但是我们无法知道到底fl在f2之前调用还是f2在fl之前调用。对于那些没有指定执行顺序的运算符来说如果表达式指向并修改了同一个对象将会引发错误并产生未定义的行为(参见2.1.2节第33页)。举个简单的例子运算符没有明确规定何时以及如何对运算对象求值因此下面的输出表达式是未定义的inti0;cout « i ” i endl; // 未定义的因为程序是未定义的所以我们无法推断它的行为。编译器可能先求i的值再求i的值此时输出结果是11;也可能先求i的值再求i的值输出结果是01甚至编译器还可能做完全不同的操作。因为此表达式的行为不可预知因此不论编译器生成什么样的代码程序都是错误的。有4种运算符明确规定了运算对象的求值顺序。第一种是323节(第85页)提到的逻辑与()运算符它规定先求左侧运算对象的值只有当左侧运算对象的值为真时才继续求右侧运算对象的值。另外三种分别是逻辑或(||)运算符(参见4.3节第126页)、条件()运算符(参见4.7节第134页)和逗号(,)运算符(参见4.10节第140页)。
求值顺序、优先级、结合律
运算对象的求值顺序与优先级和结合律无关在一条形如f()g()*h()j()的表达式中优先级规定g()的返回值和h()的返回值相乘。结合律规定f()的返回值先与g()和h()的乘积相加所得结果再与j()的返回值相加。对于这些函数的调用顺序没有明确规定。如果f、g、h和j是无关函数它们既不会改变同一对象的状态也不执行IO任务那么函数的调用顺序不受限制。反之如果其中某几个函数影响同一对象则它是一条错误的表达式将产生未定义的行为。
注意事项
1,拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求。2,如果改变了某个运算对象的值在表达式的其他地方不要再使用这个运算对象。第2条规则有一个重要例外当改变运算对象的子表达式本身就是另外一个子表达式的运算对象时该规则无效。例如在表达式*iter中递增运算符改变iter的值iter已经改变的值又是解引用运算符的运算对象。此时或类似的情况下求值的顺序不会成为问题因为递增运算即改变运算对象的子表达式必须先求值然后才轮到解引用运算。显然这是一种很常见的用法不会造成什么问题。先求值再解引用
4 . 2 算术运算符 表4.1以及后面章节的运算符表按照运算符的优先级将其分组。一元运算符的优先级最高接下来是乘法和除法优先级最低的是加法和减法。优先级高的运算符比优先级低的运算符组合得更紧密。上面的所有运算符都满足左结合律意味着当优先级相同时按照从左向右的顺序进行组合。除非另做特殊说明算术运算符都能作用于任意算术类型参见2.1.1节第30页以及任意能转换为算术类型的类型。算术运算符的运算对象和求值结果都是右值。如4.11节第141页描述的那样在表达式求值之前小整数类型的运算对象被提升成较大的整数类型所有运算对象最终会转换成同一类型。一元正号运算符、加法运算符和减法运算符都能作用于指针。3.5.3节第106页已经介绍过二元加法和减法运算符作用于指针的情况。当一元正号运算符作用于一个指针或者算术值时返回运算对象值的一个提升后的副本。一元负号运算符对运算对象值取负后返回其提升后的副本:在2.1.1节(第31页)我们指出布尔值不应该参与运算-b就是一个很好的例子。对大多数运算符来说布尔类型的运算对象将被提升为int类型。如上所示布尔变量b的值为真参与运算时将被提升成整数值1(参见2.1.2节第32页)对它求负后的结果是-1。将-1再转换回布尔值并将其作为b2的初始值显然这个初始值不等于0,转换成布尔值后应该为1。所以b2的值是真整数相除结果还是整数也就是说如果商含有小数部分直接弃除运算符俗称“取余”或 “取模”运算符负责计算两个整数相除所得的余数参与取余运算的运算对象必须是整数类型C11新标准则规定商一律向0取整(即直接切除小数部分)
4 .3 逻辑和关系运算符
关系运算符作用于算术类型或指针类型逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。值为0的运算对象(算术类型或指针类型)表示假否则表示真。对于这两类运算符来说运算对象和求值结果都是右值逻辑与和逻辑或运算符
对于逻辑与运算符()来说当且仅当两个运算对象都为真时结果为真对于逻辑或运算符(||)来说只要两个运算对象中的一个为真结果就为真。逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为短路求值(short-circuitevaluation)对于逻辑与运算符来说当且仅当左侧运算对象为真时才对右侧运算对象求值。对于逻辑或运算符来说当且仅当左侧运算对象为假时才对右侧运算对象求值第3章中的几个程序用到了逻辑与运算符它们的左侧运算对象是为了确保右侧运算对象求值过程的正确性和安全性。例如85页的循环条件index!s.size()!isspace(s[index])首先检查index是否到达string对象的末尾以此确保只有当index在合理范围之内时才会计算右侧运算对象的值。举一个使用逻辑或运算符的例子假定有一个存储着若干string对象的vector对象要求输出string对象的内容并且在遇到空字符串或者以句号结束的字符串时进行换行。使用基于范围的for循环(参见3.2.3节第81页)处理string对象中的每个元素输出当前元素后检查是否需要换行。if语句的条件部分首先检查S是否是一个空string,如果是则不论右侧运算对象的值如何都应该换行。只有当string对象非空时才需要求第二个运算对象的值也就是检查string对象是否是以句号结束的。在这条表达式中利用逻辑或运算符的短路求值策略确保只有当s非空时才会用下标运算符去访问它。值得注意的是s被声明成了对常量的引用(参见2.5.2节第61页)。因为text的元素是string对象可能非常大所以将s声明成引用类型可以避免对元素的拷贝又因为不需要对string对象做写操作所以s被声明成对常量的引用。
逻辑非运算符
逻辑非运算符()将运算对象的值取反后返回之前我们曾经在3.2.2节(第79页)使用过这个运算符。下面再举一个例子假设vec是一个整数类型的vector对象可以使用逻辑非运算符将empty函数的返回值取反从而检查vec是否含有元素关系运算符
顾名思义关系运算符比较运算对象的大小关系并返回布尔值。关系运算符都满足左结合律。因为关系运算符的求值结果是布尔值所以将几个关系运算符连写在一起会产生意想不到的结果但是这种写法存在两个问题首先与之前的代码相比上面这种写法较长而且不太直接(尽管大家都认为缩写的形式对初学者来说有点难理解)更重要的一点是如果val不是布尔值这样的比较就失去了原来的意义。如果val不是布尔值那么进行比较之前会首先把true转换成val的类型。也就是说如果val不是布尔值则代码可以改写成如下形式if(val1){/*...*/}正如我们已经非常熟悉的那样当布尔值转换成其他算术类型时,false转换成0而true转换成1(参见2.1.2节第32页)。如果真想知道val的值是否是1,应该直接写出1这个数值来而不要与true比较。进行比较运算时除非比较的对象是布尔类型否则不要使用布尔字面值true 和 false作为运算对象。
4 . 4 赋值运算符 赋值运算的结果是它的左侧运算对象并且是一个左值。相应的结果的类型就是左侧运算对象的类型。如果赋值运算符的左右两个运算对象类型不同则右侧运算对象将转换成左侧运算对象的类型如果左侧运算对象是内置类型那么初始值列表最多只能包含一个值而且该值即使转换的话其所占空间也不应该大于目标类型的空间参见2.2.1节第39页。对于类类型来说赋值运算的细节由类本身决定。对于vector来说vector模板重载了赋值运算符并且可以接收初始值列表当赋值发生时用右侧运算对象的元素替换左侧运算对象的元素。无论左侧运算对象的类型是什么初始值列表都可以为空。此时编译器创建一个值初始化参见3.3.1节第88页的临时量并将其赋给左侧运算对象。
赋值运算满足右结合律
赋值运算符满足右结合律这一点与其他二元运算符不太一样:int ival, jval; ival jval 0; / / 正确都被赋值为0因为赋值运算符满足右结合律所以靠右的赋值运算jval0作为靠左的赋值运算符的右侧运算对象。又因为赋值运算返回的是其左侧运算对象所以靠右的赋值运算的结果即jval被赋给了ival。对于多重赋值语句中的每一个对象它的类型或者与右边对象的类型相同、或者可由右边对象的类型转换得到参见4.11节第141页因为ival和pval的类型不同而且pval的类型int*无法转换成ival的类型(int),所以尽管这个值能赋给任何对象但是第一条赋值语句仍然是非法的。与之相反第二条赋值语句是合法的。这是因为字符串字面值可以转换成string对象并赋给s2,而s2和si的类型相同所以s2的值可以继续赋给si。
赋值运算优先级较低
赋值语句经常会出现在条件当中。因为赋值运算的优先级相对较低所以通常需要给赋值部分加上括号使其符合我们的原意。下面这个循环说明了把赋值语句放在条件当中有什么用处它的目的是反复调用一个函数直到返回期望的值(比如42)为止这个版本的while条件更容易表达我们的真实意图:不断循环读取数据直至遇到42为止。其处理过程是首先将get_value函数的返回值赋给i,然后比较i和42是否相等。如果不加括号的话含义会有很大变化比较运算符!的运算对象将是get_value函数的返回值及42,比较的结果不论真假将以布尔值的形式赋值给i,这显然不是我们期望的结果。因为赋值运算符的优先级低于关系运算符的优先级所以在条件语句中赋值部分通常应该加上括号。
复合赋值运算符 唯一的区别是左侧运算对象的求值次数使用复合运算符只求值一次使用普通的运算符则求值两次。这两次包括一次是作为右边子表达式的一部分求值另一次是作为赋值运算的左侧运算对象求值。其实在很多地方这种区别除了对程序性能有些许影响外几乎可以忽略不计。
4 .5 递增和递减运算符
递增运算符()和递减运算符(一)为对象的加1和减1操作提供了一种简洁的书写形式。这两个运算符还可应用于迭代器因为很多迭代器本身不支持算术运算所以此时递增和递减运算符除了书写简洁外还是必须的。递增和递减运算符有两种形式前置版本和后置版本。到目前为止本书使用的都是前置版本这种形式的运算符首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。后置版本也会将运算对象加1(或减1),但是求值结果是运算对象改变之前那个值的副本这两种运算符必须作用于左值运算对象。前置版本将对象本身作为左值返回后置版本则将对象原始值的副本作为右值返回。在一条语句中混用解引用和递增运算符
如果我们想在一条复合表达式中既将变量加1或减1又能使用它原来的值这时就可以使用递增和递减运算符的后置版本。举个例子可以使用后置的递增运算符来控制循环输出一个vector对象内容直至遇到(但不包括)第一个负值为止对于刚接触C和C的程序员来说*pbeg不太容易理解。其实这种写法非常普遍所以程序员一定要理解其含义。后置递增运算符的优先级高于解引用运算符因此*pbeg等价于*(pbeg)。pbeg把pbeg的值加1,然后返回pbeg的初始值的副本作为其求值结果此时解引用运算符的运算对象是pbeg未增加之前的值。最终这条语句输出pbeg开始时指向的那个元素并将指针向前移动一个位置。这种用法完全是基于一个事实即后置递增运算符返回初始的未加1的值。如果返回的是加1之后的值解引用该值将产生错误的结果。不但无法输出第一个元素而且更糟糕的是如果序列中没有负值程序将可能试图解引用一个根本不存在的元素。运算对象可按任意顺序求值
大多数运算符都没有规定运算对象的求值顺序(参见4.1.3节第123页)这在一般情况下不会有什么影响。然而如果一条子表达式改变了某个运算对象的值另一条子表达式又要使用该值的话运算对象的求值顺序就很关键了。因为递增运算符和递减运算符会改变运算对象的值所以要提防在复合表达式中错用这两个运算符。为了说明这一问题我们将重写3.4.1节(第97页)的程序该程序使用for循环将输入的第一个单词改成大写形式4.6成员访问运算符
点运算符(参见1.5.2节第21页)和箭头运算符(参见3.4.1节第98页)都可用于访问成员其中点运算符获取类对象的一个成员箭头运算符与点运算符有关表达因为解引用运算符的优先级低于点运算符所以执行解引用运算的子表达式两端必须加上括号。如果没加括号代码的含义就大不相同了/ / 运 行 p 的 size成员然后解引用size的结果*p.size() ; / / 错误p 是一个指针它没有名为size的成员这条表达式试图访问对象P的size成员但是p本身是一个指针且不包含任何成员所以上述语句无法通过编译。箭头运算符作用于一个指针类型的运算对象结果是一个左值。点运算符分成两种情况如果成员所属的对象是左值那么结果是左值反之如果成员所属的对象是右值那么结果是右值。