type
status
date
slug
summary
tags
category
icon
password
Property
左值和右值
C++
中左值和右值概念是从C
中继承过来的,C
定义的很简单,在赋值语句左边的是左值,右边的就是右值。C++
中的比较复杂,C++
的左值表达式的求值结果是对象或函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对像。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。通常来说,右值使用的是对象的值(内容),左值使用的对象地址(内存中的位置)。不同运算符对运算对象的要求各不相同,有的要求左值、有的要求右值;返回值也有差异,有的作为左值返回,有的作为右值返回。一个重要的原则是:在需要右值的地方可以用左值来代替,但是不能把右值当成左值(位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。
一般下列运算符需要用到左值:
- 赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值
&
取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值
- 内置
*
解引用和[]
下标操作符都将产生左值
- 自增操作符和自减操作符都需要左值作为操作数,前置形式返回左值,后置形式返回右值;
将
decltype
运用与产生左值的表达式时,返回的是结果类型的引用。使用关键字
decltype
的时候,左值和右值也有所不同。如果表达式的求值结果是左值,decltype
作用于该表达式(不是变量)得到一个引用类型。假定p的类型是int*
,因为解引用运算符生成左值,所以decltype (*p)
的结果是int&
。另一方面,因为取地址运算符生成右值,所以decltype (&p)
的结果是int**
,结果是一个指向整型指针的指针。算术运算符
所有算数运算符都是左结合的,操作数和结果都是右值。一元加号返回操作数的一个值副本,一元负号返回负的值副本。对于绝大部分操作符,
bool
值会被提升为int
,注意整数与布尔值进行相等比较时,会将布尔值转为整数进行比较。算数运算符需要注意的是值溢出,分为上溢和下溢。特别是乘法与除法结合时,虽然最后结果在范围内,但是乘法先计算是有可能导致溢出。此时可以先计算除法再计算乘法来避免。现代计算机的整数表示都是补码,补码溢出时会回绕,上溢之后值变成最小值,下溢则变成最大。
除法运算:
- 整数相除(
/
)结果还是整数,即直接弃除商的小数部分
- 取余或取模运算符(
%
)计算整数相除的余数
在除法运算中,
C++
的早期版本允许结果为负数的商向上或向下取整,C++11
则规定商一律向0取整(去除小数部分):逻辑和关系运算符
关系运算符作用于算术类型和指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。
逻辑和关系操作符的操作数是右值,结果是右值。所有这些操作符都返回
bool
值,算术值和指针值中的0被认为是false
,其它值被认为是true
。逻辑与和逻辑或执行短路求值,意味着前面如果能决定结果其后面的操作数不再计算。
&&
只有在左操作数的求值为真时,才会对右操作数求值。|| 只有在左操作数为假的情况下,才会对右操作数求值。因此,可以让左操作数作为右操作数的守卫,使得右操作数的计算是安全的:关系操作符需要注意的是不要将多个关系符号串在一起:
相等测试与 bool 字面量
测试算术或指针对象的最好方式是直接在条件语句中测试,而不要让它们与
bool
值进行比较:第三个语句中,如果
val
不是布尔值,true
会被整型提升为val
类型,例如true
转为1,从而变成if(val == 1)
。在C++
中bool
其实是一种小整型。bool
字面量须与bool
类型值比较才是正确的做法。进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值true和false作为运算对象。
赋值运算符
赋值操作符的左操作数必须是可修改的左值,运算结果是其左操作数,并且是一个左值。当左右类型不一样时,右操作数类型转为左操作数类型。在新标准下,可以提供一个大括弧初始值列表,如果提供一个空列表,左操作数将被赋值一个值初始化的值。列表中的值不能执行精度变小的转换。
赋值是右结合的。赋值语句的结果是最右端的操作数的值,整个链上的值都是一样的。多赋值表达式的类型必须与其右边的操作数一致,或者可以互相转换。
赋值操作的优先级通常较低,比关系操作优先级还低,所以经常需要用括号将赋值操作括起来。注意不要将赋值操作和等于操作混淆。
复合赋值运算符
复合赋值运算就是将左边操作数与右边操作数进行运算然后将结果值赋值到左操作数中,如:
+=
-=
*=
/=
%=
>>=
<<=
&=
^=
|=
递增和递减运算符
递增(
++
)和递减( --
)运算符是为对象加1或减1的简洁书写形式。这两个运算符还可应用于迭代器,因为很多迭代器本身不支持算术运算。递增和递减运算符分为前置版本和后置版本:
- 前置版本:首先将运算对象加1(或减1),然后将改变后的对象作为求值结果
- 后置版本:也会将运算对象加1(或减1),但求值结果是运算对象改变前的值的副本
除非必须,否则不应该使用递增或递减的后置版本。因为后置版本需要将原始值存储下来以便于返回修改前的内容,如果不需要这个值,那么后置版本的操作就是一种浪费。
如果想在一条复合表达式中既将变量加1或减1又能使用它原来的值,这时就可以使用递增和递减运算符的后置版本:
成员访问运算符
点运算符
.
和箭头运算符->
都可以用来访问成员。点运算符获取类对象的一个成员;箭头运算符与点运算符有关,表达式 ptr->mem
等价于 (*ptr).mem
因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。如果没如括号,代码的含义就大不相同了:
条件运算符
条件运算符(
? :
)的使用形式如下:条件操作符的优先级非常低,并且求值顺序是有要求的,当条件为真时,? 后的 expr1 求值,否则对 expr2 求值。要求 expr1 和 expr2 的结果是相同类型或者可以相互转换。如果两个表达式都是左值,则整个条件操作的结果是左值,否则将是右值。
条件操作可以嵌套,但最好不要嵌套多于两层。条件操作是右结合的,意味着多个条件操作符嵌套时将从右边开始分析。
由于条件操作符的优先级非常低,所以很多时候必须将条件操作符用括弧括起来。
位运算符
在位运算中符号位如何处理并没有明确的规定,所以强烈建议仅将位运算符用于无符号类型的处理
移位运算符的优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。
sizeof运算符
sizeof
操作符返回表达式结果类型或类型名所表示类型的内存尺寸,返回的大小是固定尺寸,不会包含动态分配的内存。因而,对于 vector
类型不会包含分配的元素的内存大小。sizeof
是右结合的,其所得的值是一个size_t
类型的一个常量表达式。运算符的运算对象有两种形式:第二种形式中,
sizeof
并不会对表达式进行求值,而是推断其结果类型,进而计算所需的内存大小。甚至不理会表达式本身是否求值出来是未定义的,如:解引用一个未初始化的指针。 sizeof
求值是安全的,因为sizeof
根本不会真正对指针进行解引用,只要它的返回值类型。在新标准中,可以使用
::
作用域操作符来获取类的成员的大小。如:sizeof Sales_data::revenue
。需要注意的是当对数组进行sizeof
操作时返回的是数组占的内存大小,此时数组不会转为指针。对指针进行sizeof
得到的就是指针本身所占内存大小,在64位机器上是8字节。对string
或vector
进行sizeof
操作只返回对象所占的固定部分,不会返回为元素分配的动态内存。sizeof
运算符的结果部分依赖于其作用的类型:- 对
char
或者类型为char
的表达式执行sizeof
运算,返回值为1
- 对引用类型执行
sizeof
运算得到被引用对象所占空间的大小
- 对指针执行
sizeof
运算得到指针本身所占空间的大小
- 对解引用指针执行
sizeof
运算得到指针指向的对象所占空间的大小,指针不需要有效
- 对数组执行
sizeof
运算得到整个数组所占空间的大小
- 对
string
或vector
对象执行sizeof
运算只返回该类型固定部分的大小,不会计算对象中元素所占空间的大小
逗号运算符
逗号运算符
,
含有两个运算对象,按照从左向右的顺序依次求值,最后返回右侧表达式的值。逗号运算符经常用在 for
循环中:优先级和求值顺序
表达式中有两个或多个操作符是一个复合表达式,优先级和结合律决定了复合表达式中的操作符与操作数怎样进行组合,决定了哪些部分先进行求值。括号无视优先级与结合律,建议:当对操作符的优先级有怀疑时使用括号进行标明。优先级高的操作符比低的先求值,结合律决定了相同优先级的操作符谁先进行求值。
大部分的操作符不会要求操作数的求值顺序,可以以任何顺序对操作数进行求值。如:
int i = f1()*f2();
就能以任何顺序对 f1 和 f2 函数进行调用。对于不强制要求操作数顺序的操作符,如果对一个操作数又改变又取值就是错误,通常导致未定义行为:有4 种运算符明确规定了运算对象的求值顺序:
- 逻辑与 (
&&
) 运算符,规定先求左侧运算对象的值,只有当左侧运算对象的值为真才继续求右侧运算对象的值/
- 逻辑或 (
||
) 运算符
- 条件 (
? :
) 运算符
- 逗号 (
,
) 运算符
操作数的求值顺序与优先级和结合性是完全独立的。