表达式由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果(result)。字面值和变量是最简单的表达式(expression),其结果就是字面值和变量的值。把一个运算符(operator)和一个或多个运算对象组合起来可以生成较复杂的表达式。
基础
概念
一元运算符:作用于一个运算对象的运算符是一元运算符,如取地址符(&)和解引用符(*);
二元运算符:作用于两个运算对象的运算符是二元运算符,如相等运算符(==)和乘法运算符(*)。
也有三元运算符,此外,函数调用也是一种特殊的运算符,
重载运算符:运算符作用于类类型的对象时,用户可以自行定义含义,为已存在的运算符赋予另一种含义。
运算对象的个数、运算符的优先级和结合律都是无法改变的。
左值:当一个对象被用作左值时,用的是对象的身份(在内存中的位置)。
右值:当一个对象被用作右值时,用的是对象的值(内容)。
在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。
使用关键字decltype时,如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。
优先级与结合律
算术运算符满足左结合律:如果运算符优先级相同,按照从左向右的顺序组合运算对象。
括号无视优先级与结合律。
求值顺序
对于没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。
int i = 0;
cout << i << ++i << end;//未定义的
因为程序是未定义的,所以无法推断它的行为。可能输出 1 1 ,也有可能输出 0 1。
明确规定了求值顺序的运算对象:&& || ?: ,
处理复合表达式:
1.拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求。
2.如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。
算数运算符
**一元运算符的优先级最高,接下来是乘法和除法,优先级最低的是加法和减法。**上述都满足做结合律。
算术运算符都能作用于任意算术类型。算术运算符的运算对象和求值结果都是右值。
算术表达式有可能产生未定义的结果的原因:
1.数学性质本身:例如除数是0的情况;
2.源于计算机的特点:例如溢出,当计算的结果超出该类型所能表示的范围时就会产生溢出。
逻辑和关系运算符
关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。
赋值运算符
赋值满足右结合定律。
赋值运算优先级较低,低于关系运算符的优先级。
C++语言允许用赋值运算作为条件:if(i = j)
,这并不会出错,因此在写if(i == 1)
时,尽量写为 if(1 == i)
,这样可以避免少写 =
而编译器不会报错。
复合赋值符与普通运算符唯一的区别是:使用复合运算符只求值一次,使用普通的运算符则求值两次。
这两次包括:一次是作为右边子表达式的一部分求值,另一次是作为赋值运算的左侧运算对象求值。
递增和递减运算符
递增和递减运算符有两种形式:前置版本和后置版本。
int i=0,j=0;
j = ++i;//j=1, i=1:前置版本得到递增之后的值
j = i++;//1 j= l, i=2:后置版本得到递增之前的值
这两种运算符必须作用于左值运算对象。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。
auto pbeg = v.begin();
//输出元素直至遇到第一个负值为止
while (pbeg != v.end() & & *beg >=0)cout<< *pbeg++ <<endl;//输出当前值并将pbeg向前移动一个元素//等价于
while (pbeg != v.end() & & *beg >=0){cout<<*pbeg<<endl;++pbeg;}
后置递增运算符的优先级高于解引用运算符,因此*pbeg++
等价于* (pbeg++)
。pbeg++
把pbeg
的值加1
,然后返回pbeg
的初始值的副本作为其求值结果,此时解引用运算符的运算对象是pbeg
未增加之前的值。最终,这条语句输出 pbeg
开始时指向的那个元素,并将指针向前移动一个位置。
成员访问运算符
点运算符和箭头运算符都可以用于访问成员,其中,点运算符获取类对象的一个成员;箭头运算符与点运算符有关,表达式ptr->mem
等价于(*ptr).mem
。
箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值;反之,如果成员所属的对象是右值,那么结果是右值。
条件运算符
cond ? expr1 : expr2;
条件运算符的执行过程是:首先求cond
的值,如果条件为真对exprl
求值并返回该值,否则对expr2
求值并返回该值。
条件运算符的优先级非常低,通常都要加括号。
位运算符
位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。
一般来说,如果运算对象是“小整型”,则它的值会被自动提升成较大的整数类型。运算对象可以是带符号的,也可以是无符号的。
移位运算符
左移运算符(<<)在右侧插入值为0的二进制位。右移运算符(>>)的行为则依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在左侧插入值为0的二进制位;如果该运算对象是带符号类型,在左侧插入符号位的副本或值为0的二进制位。左移操作处理带符号值是一种未定义的行为。
sizeof 运算符
sizeof
运算符返回一条表达式或一个类型名字所占的字节数。通常有两种形式:sizeof (type)
和 sizeof expr
。在第二种形式中,sizeof
返回的是表达式结果类型的大小。
- 对引用类型执行sizeof运算得到被引用对象所占空间的大小。
- 对指针执行sizeof运算得到指针本身所占空间的大小。
- 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需有效。
- 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会把数组转换成指针来处理。
- 对string对象或vector对象执行sizeof运算*只返回该类型固定部分的大小,*不会计算对象中的元素占用了多少空间。
逗号运算符
逗号运算符(comma operator)含有两个运算对象,按照从左向右的顺序依次求值。
对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。
类型转换
隐式转换:自动执行,无须程序员介入。算术类型之间的隐式转换被设计得尽可能避免损失精度。
何时发生隐式类型转换:
- 在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型。
- 在条件中,非布尔值转换成布尔类型。
- 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
- 函数调用时也会发生类型转换。
算数转换
**算术转换(arithmetic conversion)**的含义是把一种算术类型转换成另外一种算术类型。
如果某个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型。
当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型。
如果一个运算对象是无符号类型、另外一个运算对象是带符号类型:
1.其中的无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的;
2.其中带符号类型大于无符号类型,此时转换的结果依赖于机器。
其他隐式类型转换
数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针。
int ia[10];//含有10个整数的数组
int* ip = ia;//ia转换成指向数组首元素的指针
指针的转换: 0 或 nullptr 都能转换成任意指针类型。指向非常量的指针能转换成 void*。指向所有对象的指针都能转换成 const void*。
转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制。
char *cp =get_string O);
if (cp)/* ...*/ //如果指针cp不是0,条件为真
while (*cp)/*...*/ //如果*cp不是空字符,条件为真
转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针。(引用也一样)
int i;
const int &j = i;//非常量转换成const int的引用
const int *p = &i;//非常量的地址转换成const的地址
int &r=j, *g = p;//错误:不允许const转换成非常量
类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。
string s, t= "a value";//字符串字面值转换成string类型
while (cin >>s)//while的条件部分把cin转换成布尔值
显示转换
强制类型转换:
cast-name<type>(expression);
其中,type
是转换的目标类型而expression
是要转换的值。如果type
是引用类型,则结果是左值。cast-name指定了执行的是哪种转换。
cast-name是 static _cast 、 dynamic_cast , const_cast 和 reinterpret_cast 中的一种。
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static cast。
double slope = static_cast<double>(j)/i; //将 j 转换成 double 以便执行浮点数除法
当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。此时,强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。
如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息;但是当我们执行了显式的类型转换后,警告信息就会被关闭了。
const_cast
const cast只能改变运算对象的底层const。
只有 const_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只改变常量属性
reinterpret_cast
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。风险高,尽量不用。
旧式的强制类型转换
type (expr);//函数形式的强制类型转换
(type) expr;//C语言风格的强制类型转换
与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不那么清晰明了,容易被看漏,所以一旦转换过程出现问题,追踪起来也更加困难。
重要术语
- 强制类型转换(cast) 一种显式的类型转换。
- 整型提升(integral promotion) 把一种较小的整数类型转换成与之最接近的较大整数类型的过程。不论是否真的需要,小整数类型(即 short、 char等)总是会得到提升。
- 短路求值(short-circuit evaluation) 是一个专有名词,描述逻辑与运算符和逻辑或运算符的执行过程。如果根据运算符的第一个运算对象就能确定整个表达式的结果,求值终止,此时第二个运算对象将不会被求值。