c++系列文章(12):表达式

  表达式由一个或多个运算对象组成,对表达式求值将得到一个结果字面值和变量是最简单的表达式,其结果就是字面值和变量的值。把一个运算符和一个或多个运算对象组合起来可以生成较复杂的表达式。

表达式的基本概念

  C++定义了一元运算符和二元运算符,此外还有一个作用于三个运算对象的三元运算符。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。
  在表达式求值的过程中,运算对象常常需要由一种类型转换成另一种类型。例如整数能被转换成浮点数、浮点数也能转换成整数、但指针不能转换成浮点数,此外小整数类型(如bool、char、short等)通常会被提升成较大的整数类型,主要是int。
  C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作当运算符作用于类类型的运算对象时,用户可以自定义其含义,称之为重载操作符。IO库的>>和<<运算符以及string对象、vector对象和迭代器使用的运算符都是重载运算符。当我们使用重载运算符时,其包括的运算对象的类型和返回值的类型,都是由该运算符定义的,但是运算对象的个数、运算符的优先级和结合律都是无法改变的
  C++的表达式要不然是右值,要不然就是左值。这两个名词是从C语言中继承过来,原本的意思是:左值可以位于赋值语句的左侧,右值则不能。但在C++语言中,二者的区别就没那么简单:一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧对象;此外,某些表达式的求值结果是对象,但他们是右值而非左值。做一个简单的归纳:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)
  不同运算符对运算对象的要求各不相同,有的需要左值运对象,有的需要右值运算对象;返回值也有差异,有的得到左值结果,有的得到右值结果。一个重要的原则是:在需要右值的地方可以用左值来代替,但不能把右值当成左值使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)
以下是几种我们熟悉的运算符要用到左值:
  赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也是一个左值;
  取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值;
  内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值;
  内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值;
  使用关键字decltype的时候,左值和右值也有所不同。**如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。*例如,假定p的类型是int,因为解引用运算符生成左值,所以decltype(*p)的结果是int&。另一方面,因为取地址运算符生成右值,所以decltype(&p)的结果是是一个指向整型指针的指针。

优先级和结合律

  复合表达式是指含有两个或多个运算符的表达式。求复合表达式的值需要首先将运算符合运算对象合理地组合在一起,优先级与结合律决定了运算对象组合的方式。**优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值,在大多数情况下,不会明确指定求值的顺序。**对于如下的表达式:

int i = f1() * f2();

  我们知道f1和f2一定会在执行乘法之前被调用,因为毕竟相乘的是这两个函数的返回值,但是我们无法知道到底f1在f2之前调用还是f2在f1之前调用。**对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。**例如<<运算符没有明确规定何时以及如何对运算对象求值,因此下面的输出表达式是未定义的:

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

  编译器可能先求++i的值再求i的值,此时输出的结果是1 1;也有可能先求i的值再求++i的值,输出的结果是0 1;甚至编译器还可能左完全不同的操作。因为此表达式的行为不可预知的。
  在处理符合表达式时,有两条经验准则十分重要:1、拿不准的时候最好用括号;2、如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。

算术运算符

算术运算符
  上述的算术运算符都满足左结合律,既当优先级相同时按照从左到右的顺序进行组合。**除非另做特殊说明,算术运算符都能作用于任意算术类型以及任意能转换为算术类型的类型。**算术运算符的运算对象和求值结果都是右值。在表达式求值之前,小整数类型的运算对象被提升成较大的整数类型,所有运算对象最终会转换成同一类型。对大多数运算符来说,布尔类型的运算对象将被提升为int类型。
  算术表达式可能产生未定义的结果:一部分原因是数学性质本身,例如除数是0的情况;另外一部分则源于计算机的特点,例如溢出,当计算的结果超出该类型所能表示的范围时就会产生溢出。
  假设某个机器的short类型占16位,则最大的short数值是32767,则下面的复合赋值语句将会产生溢出:

short short_value = 32767; //short类型占16位,能表示的最大值时32767
short_value += 1; //该计算导致溢出
cout << short_value << endl;

  给short_value赋值的语句是未定义的,这是因为表示一个带符号32768需要17位,但是short类型只有16位。很多系统在编译和运行时都不报溢出错误,而是产生未定义的结果。例如在我的系统中,输出的结果是-32768,该值发生了“环绕”,符号位本来是0,由于溢出被改写成了1,于是结果变成了一个负值。在别的系统中也许会有其他结果,程序的行为可能不同甚至直接崩溃。
  对于取余运算符%,在C++11新标准中,m%(-n)等于m%n,(-m)%n等于-(m%n),例如-21%-8的结果是-5,21%5的结果是1,既先全部按照正数取余,如果m为正,则余数为正,如果m为负,则余数为负

逻辑和关系运算符

  关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。值为0的运算对象(算术类型或指针类型)表示假,否则表示真。逻辑和关系运算符,运算对象和求值结果都是右值。
逻辑运算符和关系运算符
  如果要想测试一个算术对象或指针对象的真值,最直接的方法就是将其作为if语句的条件,编译器会自动将其转换成布尔值:

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

  有时视图将上面的真值测试写成如下形式:

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

  但这种写法存在两个问题:首先这种写法较长且不太直接;其次,更重要的是,如果val不是布尔值,这样的比较就失去了原来的意义,因为当val不是布尔值时,编译器在进行比较之前会先把true转换成val的类型,当布尔值转换成其他算术类型时,false转换成0而true转换成1。如果val不是布尔值,则代码会改成如下的形式:

if(val == 1){ /*......*/}

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

赋值运算符

  **赋值运算符的左侧运算对象必须是一个可修改的左值。赋值运算的结果是它的左侧运算对象,并且是一个左值。**相应的,结果的类型就是左侧运算对象的类型。如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。
  C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象

int k;
k = 3.14; //结果:类型是int,值是3
k = {3.14}; //错误:花括号形式不能进行窄化转换
vector<int> vi;
vi = {0,1,2,3,4,5}; //vi现在含有6个元素

  如果左侧运算对象是内置类型,那么初始值列表最多只能包含一个值,而且该值即使转换的话其所占空间也不应该大于目标类型的空间对于类类型而言,赋值运算的细节由类本身决定,例如对于vector来说,vector模板重载了赋值运算符并且可以接收初始值列表,当赋值发生时用右侧运算对象的元素替换左侧运算对象的元素。无论左侧运算对象的类型是什么,初始值列表都可以为空,此时编译器创建一个值初始化的临时量并将其赋给左侧运算对象
  赋值运算符满足右结合律,所以靠右的赋值运算符jval = 0作为靠左的赋值运算符的右侧运算对象。又因为赋值运算符返回的是其左侧运算对象,所以靠右的赋值运算的结果(即jval)被赋给了ival。

int ival,jval;
ival = jval = 0; //等价于(ival = (jval = 0))

递增递减运算符

  递增和递减运算符有两种形式:前置版本和后置版本。前置版本首先将运算对象加1(或减1),然后将改变后的对象作为求值结果;后置版本也会将运算对象加1(或减1),但是求值结果是运算对象改变之前那个值的副本。这两种运算符都必须作用于左值运算对象,前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
  建议:除非必须,否则不用递增递减运算符的后置版本。因为前置版本的递增递减运算符避免了不必要的工作,它把值加1(或减1)后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便返回这个未修改的内容,如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。对于整数和指针类型来说,这种额外的工作消耗不大,编译器也可能进行一定的优化,但是对于相对复杂的迭代器,这种额外的工作就消耗巨大了。
  如果我们想在一条复合表达式中既将变量加1(或减1),又能使用它原来的值,这时就可以使用递增或递减运算符的后置版本。

auto pbeg = v.begin();
while(pbeg != v.end())
	cout << *pbeg++ << endl;

  对于刚接触C++的程序员来说,*pbeg++可能不太容易理解,但这种写法非常普遍。后置递增运算符非优先级高于解引用运算符,pbeg++把pbeg的值加1,然后返回pbeg的初始值的副本作为其求值结果,此时解引用运算符的运算对象时pbeg未增加之前的值。

成员访问运算符

  点运算符和箭头运算符都可用于访问成员,其中,点运算符获取类对象的一个成员;箭头运算符与点运算符有关,表达式ptr->mem等价于*(ptr).mem箭头运算符作用于一个指针类型的运算对象,结果是一个左值点运算符分成两种情况:如果成员所属的对象时左值,那么结果是左值;反之,如果成员所属的对象时右值,那么结果是右值。
  因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号:*(p).size()

条件运算符

  条件运算符(?:)按照如下形式使用:cond ? expr1 : expr2;,其中cond是判断条件的表达式,而expr1和expr2是两个类型相同或可能转换为某个公共类型的表达式。条件运算符的执行过程是:首先求cond的值,如果条件为真对expr1求值并返回该值,否则对expr2求值并返回该值。当条件运算符的两个表达式都是左值或者都能转换成同一种左值类型时,运算的结果是左值;否则运算的结果是右值。
  条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要加上括号。

位运算符

  位运算符作用于整数类型的运算对象,并把运算对象看成二进制位的集合。
位运算符
  一般来说,如果运算对象时“小整数”,则它的值会被自动提升成较大的整数类型。运算对象可以是带符号的,也可以是无符号的。如果运算对象是带符号的且它的值为负,那么位运算符如何处理运算对象的“符号位”依赖于机器。而且,此时的左移操作可能会改变符号位的值,因此是一种未定义的行为,所以强烈建议仅将位运算符用于处理无符号类型。

移位运算符:

  <<运算符合>>运算符的内置含义是对其运算对象执行基于二进制位的移动操作,首先令左侧运算对象的内容按照右侧运算对象的要求移动指定位数,然后将经过移动的(可能还进行了提升)左侧运算对象的拷贝作为求值结果。其中,右侧的运算对象一定不能为负,而且值必须严格小于结果的位数,否则就会产生未定义的行为。二进制位或者向左移(<<)或者向右移(>>),移出边界之外的位就被舍弃掉了。
移位运算符
  左移运算符(<<)在右侧插入值为0的二进制位,右移运算符(>>)的行为则依赖于其左侧运算对象的类型如果该运算对象时无符号类型,在左侧插入值为0的二进制位如果该运算对象时带符号类型,在左侧插入符号位的副本或值为0的二进制位,如何选择要视具体环境决定。
  移位运算符满足左结合律,例如表达式cout << “hi” << " there" << endl;等价于((cout << “hi”) << " there" ) << endl;。移位运算符的优先级不高不低,低于算术运算符,高于关系运算符、赋值运算符和条件运算符。

sizeof运算符

  sizeof运算符返回一条表达式或一个类型名字所占的字节数。sizeof运算符满足右结合律,其所得的值是一个size_t类型的常量表达式。sizeof运算符的运算对象有两种形式:sizeof(expr)sizeof expr,在第二种形式中,sizeof返回的是表达式结果类型的大小。
  此外sizeof并不实际计算其运算对象的值,所以即使sizeof的运算对象是一个无效的指针也不会有什么影响。在sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并不会被真正使用,sizeof不需要真的解引用指针也能知道它所指对象的类型。
  C++11新标准允许我们使用作用域运算符来获取类成员的大小。通常情况下只有通过类的对象才能访问到类的成员,但是sizeof运算符无须我们提供一个具体的对象,因为要想直到类成员的大小无须真的获取该成员。
sizeof运算符的结果部分地依赖于其作用的类型:
  对char类型或者类型为char的表达式执行sizeof运算,结果得1
  对引用类型执行sizeof运算得到被引用对象所占空间的大小
  对指针执行sizeof运算得到指针本身所占空间的大小
  对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需要有效
  对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会将数组转换成指针来处理;因为执行sizeof运算能得到整个数组的大小,所以可以用数组的大小除以单个元素的大小得到数组中元素的个数:sizeof(arr)/siezof(*arr)。同时,sizeof的返回值是一个常量表达式,所以可以用sizeof的结果声明数组的维度
    对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。

逗号运算符

逗号运算符含有两个运算对象,按照从左到右的顺序依次求值。对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉,真正的结果是右侧表达式的值。如果右侧运算对象时左值,那么最终的求值结果也是左值。

运算对象可按任意顺序求值

  大多数运算符都没有规定运算对象的求值顺序,这在一般情况下不会有什么影响。然而,如果一条子表达式改变了某个运算对象的值,另一条子表达式又要使用该值的话,运算对象的求值顺序就很关键了。因为递增递减运算符会改变运算对象的值,所以要堤防在复合表达式中错用这两个运算符。

// 将输入的第一个单词改写成大写形式
for(auto it = s.begin();it != s.end();++it)
	*it = toupper(*it)
// 看似等价的while循环
while(beg != s.end())
	*beg = toupper(*beg++); //错误,该赋值语句未定义

  问题在于:赋值运算符左右两端的运算对象都用到了beg,并且右侧的运算对象还改变了beg的值,所以该赋值语句是未定义的。编译器可能按照下面的任意一种思路处理该表达式,也可能采取别的什么方式处理它:

*beg = toupper(*beg); // 如果先求左侧的值
*(beg+1) = toupper(*beg); // 如果先求右侧的值

类型转换

  C++语言不会直接将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象的类型统一后再求值,这些类型转换是自动执行的,无需程序员接入,被称作“隐式转换”。
  算术类型之间的隐式转换被设计得尽可能避免损失精度。如果表达式中既有整数类型的运算对象,又有浮点数类型的运算对象,整数会被转换成浮点数。而在初始化过程中,因为被初始化的对象的类型无法改变,因此初始值被转换成该对象的类型。

int ival = 3.14 + 3; 
//首先进行加法运算,int类型的3被转换成double型的3.0,得到double型的6.14;接下来6.14被用来初始化int型的变量,由double向int转换时忽略掉小数部分。

何时发生隐式类型转换

在以下这些情况中,编译器会自动转换运算对象的类型:
  比int类型小的整数型值首先提升为较大的整数类型(通常为int)
  在条件中,非布尔值转换成布尔值
  初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型
  如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型;
  此外函数调用时也会发生类型转换;

算术转换

  算术转换的含义是把一种算术类型转换成另一种算术类型。算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型,整数值将转换成相应的浮点类型
  整数提升负责把小整数类型转换成较大的整数类型。对于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中最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值,而不仅仅是当前的值。
  如果某个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型,但是如果某个运算对象的类型是无符号类型,那么转换的结果就要依赖机器中各个整数类型的相对大小了
    首先执行整型提升,如果结果的类型匹配,则无需进一步的转换;
    如果两个提升后的运算对象的类型要么都是带符号的、要么都是无符号的,则小类型的运算对象转换成较大的类型
    如果一个运算对象是无符号类型、另外一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号类型的运算对象转换成无符号类型的。例如两个类型分贝是unsigned int和int,则int类型的运算对象转换成unsigned int类型,需要注意的是如果int型的值恰好为负值,其结果将被强行转换,由此带来相关的副作用;
    如果带符号类型大于无符号类型,此时的转换结果依赖于机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型,如果不能,那么带符号类型的运算对象转换成无符号类型。例如,如果两个运算对象的类型分别是long和unsigned int,并且int和long的大小相同,则long类型的运算对象转换成unsigned int类型,如果long类型占用的空间比int更多,则unsigned int类型的运算对象转换成long类型。
  要想理解算术转换,最好的办法就是研究大量的例子:
算术转换
算术转换

其他隐式类型转换

除了算术转换之外还有几种隐式类型转换:
  数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针,但当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeof以及typeid等运算符的运算对象时,上述转换不会发生。同样,如果用一个引用来初始化数组,上述转换也不会发生
  指针的转换:C++规定了几种其他的指针转换方式,包括常量整数值0或者字面值nullptr能转换成任意指针类型指向任意非常量的指针能转换成void*;指向任意对象的指针能转换成const void*。此外,有继承关系的类型间还有一种指针转换的方式。
  转换成布尔类型:如果指针或算术类型的值为0,转换结果是false;否则转换结果是true。
  转换成常量允许将指正非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。但相反的转换并不存在,因为它视图删除掉底层const。
  类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次最多只能执行一种类类型的转换

显式转换

  有时我们希望显式地将对象强制转换成另外一种类型。虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。一个命名的强制类型转换具有如下形式:cast-name(expression);,其中,type是转换的目标类型而expression是要转换的值。如果type是引用类型,则结果是左值。cast-name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。

static_cast

  任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。当需要把一个较大的算术类型赋值给较小的类型是,static_cas非常有用。此时强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。一般来说,如果编译器发现一个较大的算术类型视图赋值给较小的类型,就会给出警告信息;但是当我们执行了显式的类型转换后,警告信息就会被关闭。

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

  static_cast对于编译器无法自动执行的类型转换也非常有用,例如可以用static_cast找回存在于void指针中的值。当我们把指针存放在void中,并且使用static_cast将其强制转换会原来的类型时,应该确保指针的值保持不变,也就是说我们必须确保转换后所得的类型就是指针所指的类型。

double d;
void* p = &d;
double *dp = static_cast<double*>(p);

const_cast

  const_cast只能改变运算对象的底层const,也只有const_cast能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样,也不能用const_cast改变表达式的类型

const char *pc; //指向字符常量的指针(底层const)
char *p = const_cast<char*>(pc); //正确,但是通过p写值是未定义的行为
char *q = static_cast<char*>(pc); //错误,static_cast不能转换掉const性质
static_cast<string>(pc); //正确,字符串字面值转换成string类型
const_cast<string>(pc); //错误,const_cast只改变常量属性

reinterpret_cast

  reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释

int *ip;
char *pc = reinterpret_cast<char*>(ip)

  我们必须牢记pc所指的真实对象时一个int而非字符,如果把pc当成普通的字符指针使用就可能在运行时发生错误。

发布了19 篇原创文章 · 获赞 6 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/hjc132/article/details/104587334
今日推荐