第4章 表达式

第4章 表达式

4.1 基础

4.1.1 基本概念

左值和右值

C++的表达式要不然是右值,要不然是左值,这两个名词是从C语言继承过来的,左值可以位于赋值语句的左侧,右值则不能。

在C++语言中,二者的区别就没那么简单了。一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。简单归纳:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值时,用的是对象的身份(在内存中的位置)。

不同运算符对运算对象的要求各不相同,有的需要左值运算对象、有的需要右值运算对象;返回值也有差异,有的得到右值结果。一个重要的原则是在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是在内存中的位置)。当一个左值被当成右值使用时,实际使用的是它的内容。

运算符需要的运算对象返回结果
赋值运算符左值作为其左侧运算对象左值
取地址符左值运算对象返回一个指向该运算对象的指针,该指针为右值
内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符左值左值
内置类型和迭代器的递增递减运算符作用于左值运算对象左值

接下来介绍运算符时,会注明该运算符的运算对象是否必须是左值以及求值结果是否是左值。

使用关键字decltype的时候,左值和右值有所不同。如表达式的求值结果是左值,decltype作用与该表达式(不是变量)得到一个引用类型。例如,假设p的类型是int *,因为解引用运算符生成左值,所以decltype(*p)的结果是int &;另一方面,因为取址运算符生成右值,所以decltype(&p)的结果是int **,也就是说,结果是一个指向整形指针的指针。

4.1.3 求值顺序

优先级规定了运算对象的组合方法,但是没有说明运算对象按照什么顺序求值。在大多数情况下,不会明确指定求值的顺序。举个简单的例子,<<运算符没有规定何时以及如何对运算对象求值,因此下面输出表达式是未定义的:

int i=0;
cout<<i<<""<<++i<<endl;

因为程序是未定义的,所有我们无法推断它的行为。编译器可能先求++i的值再求i的值,此时输出结果未1 1;也有可能先求i的值在求++i的值,输出结果为 0 1;甚至编译器还可能做完全不同的操作。因为此表达式的行为不可预知,因此不论编译器生成什么样的代码程序都是错误的。

求值顺序、优先级、结合律

运算对象的求值顺序与优先级和结合律无关,在一条形如f()+g()*h()+j()的表达式中对其中的函数调用顺序没有明确规定。若f、g、h和j是无关函数,它们既不会改变同一对象的状态也不执行IO任务,那么函数的调用顺序不受限制;反之,若其中某几个函数影响同一对象,则它是一条错误的表达式,将产生未定义的行为。

4.2 算术运算符

在表达式求值前,小整数类型的运算对象会被提升为较大的整数类型,所有运算对象最终会被转换成同一类型。

对大多数运算符来说,布尔类型的运算对象被提升为int类型。如布尔变量b为真,参与运算时将被提升为整数值1,对它求负后的结果为-1。将-1转换成布尔值并将其作为b2的初始值,显然这个初始值不为0,转换成布尔值后应该为1。所以,b2的值是真!

bool b=true;
bool b2=-b;

!!!注:溢出和其他算术运算异常:

 //溢出 short类型占16位 最大值为32767 最小值为-32768
    short short_value=32767;
    short_value+=1; //值发生“环绕”,符号位本来为0,由于溢出变为1,于是结果变为一个负值
    cout<<short_value<<endl;

C++新标准中除了-m导致溢出的特殊情况,其他时候**(-m)/n和m/(-n)都等于-(m/n)**,m%(-n)等于m%n,(-m)%n等于-(m%n)。

4.4 赋值运算符

赋值运算符的左侧对象必须是一个可修改的左值

赋值运算的结果是它的左侧运算对象,并且是一个左值,相应的,结果的类型就是左侧运算对象的类型。若赋值运算符的左右两个运算对象类型不同,则右侧运算对象会将转换成左侧运算对象的类型。例:

int k=0;
k=0;//结果:类型是int,值为0
k=3.14159;//结果:类型是int,值为3

C++11标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。**若左侧运算对象是内置类型,那么初始值列表最多只能包含一个值,而且该值即使转换的话其所占的空间也不应该大于目标类型的空间。**例:

k={3.14};//错误:double型转成int  窄化转换
赋值运算符满足右结合律
int ival,jval;
ival=jval=0;

因为赋值运算符满足右结合律,所以靠右的赋值运算jval=0作为靠左的赋值运算符的右侧运算对象,又因为赋值运算返回的是其左侧运算对象,所以靠右的赋值运算的结果被赋给ival。

对于多重赋值语句中的每一个对象,它的类型或者与右边对象的类型相同、或者可由右边对象的类型转换得到。

int ival2,*p;
ival2=p=0;//错误:不能把指针的值赋给int
string s1,s2;
s1=s2="OK";
赋值运算符的优先级较低

注:赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。

4.5 递增和递减运算符

递增和递减运算符有两种形式:前置版本和后置版本 。前置版本形式的运算符首先将运算对象加1(或减1),然后将改变后的对象作为求值结果;后置版本形式的运算符也会将运算对象加1(或减1),但是求值结果是运算对象改变之前那个值的副本

 int ival3=0,j=0;
 j=++ival3;
 cout<<j<<ival3<<endl;//j=1,ival3=1:前置版本得到递增之后的值
 j=ival3++;
 cout<<j<<ival3<<endl;//j=1,ival=2:后置版本得到递增之前的值

这两种运算符必须作用于左值运算对象。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。

**!!建议:除非必须,否则不用后置版本。**因为前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回改变了的运算对象,与之相比,后置版本需要将原始值存储下来以便返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。

在一条语句中混用解引用和递增运算符

若我们想在一条复合表达式中既将变量加1或减1又能使用它原来的值,这时就可以使用递增和递减运算符的后置版本。例如,使用后置的递增运算符来控制循环输出一个vector对象内容直至遇到(但不包括)第一个负值为止:

auto pbeg=v.begin();
while(pbeg!=v.end()&&*pbeg>=0){
    cout<<*pbeg++<<endl;//输出当前值并将pbeg向前移动一个元素
}

后置递增运算符的优先级高于解引用运算符,因此pbeg++等价于(pbeg++)。pbeg++把pbeg的值加1,然后返回pbeg的初始值的副本作为求值结果,此时解引用运算符的对象是pbeg未增加前的值。最终输出pbeg开始时指向的那个元素,并将指针向前移动一个位置。

4.6 成员访问运算符

点运算符和箭头运算符都可用于访问成员,其中点运算符获取类对象的一个成员;箭头运算符与点运算符有关,表达式ptr->mem等价于(*ptr).mem;

解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。箭头运算符作用于一个指针类型的运算对象,结果是一个左值;点运算符根据成员所属的对象产生结果,若对象是左值,结果是左值,反之,为右值。

练习:假设iter是vector::iterator类型,说明下表中的表达式是否合法。

表达式是否合法
*iter++;合法。后置递增运算符的优先级比解引用优先级高,指向原始vector位置,然后迭代器加1。
(*iter)++;不合法。string对象没有重载++运算符
*iter.empty()不合法。点运算符优先级高于解引用运算符,iter是一个指针,指针没有empty成员函数。
iter->empty()合法。iter->empty()等价于(*iter).empty(),iter解引用是string对象,有成员函数。
++*iter不合法。
iter+±>empty()合法。箭头运算符优先级高于后置递增运算符,等于iter->empty(),iter++;
 vector<string>s={"a","b","c"};
    vector<string>::iterator iter=s.begin();
    *iter++;
    //(*iter)++;不合法
    //*iter.empty();不合法
    iter->empty();
    //++*iter;不合法
    iter++->empty();  // iter->empty(),iter++;

4.7 条件运算符

条件运算符(?:)允许把简单的if-else嵌入到单个表达式中,条件运算符形式如下:

cond?expr1:expr2;

其中cond是判断条件的表达式,expr1和expr2是两个类型相同或可能转换为某个公共类型的表达式。条件运算符的执行过程是:首先求cond的值,如果条件为真对expr1求值并返回该值,否则对expr2求值并返回该值。

int grade=80;
string finalGrade=(grade<60)?"fail":"pass";
cout<<finalGrade<<endl; //输出pass
在输出表达式中使用条件运算符

条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。如:

cout<<((grade<60)?"fail":"pass");//输出pass或fail
cout<<(grade<60)?"fail":"pass";//输出1或0
cout<<grade<60?"fail":"pass"; //错误:试图比较cout和60 

在第二条表达式中,grade和60的比较结果是<<运算符的运算对象,<<运算符的返回值是cout,接下来cout作为条件运算符的条件,也就是说,第二条表达式等价于:

cout<<(grade<60);//输出0或1
cout?"fail":"pass";//根据cout的值是true还是false产生对应的字面值

第三条表达式等价于:

cout<<grade;//小于运算符的优先级低于移位运算符,所以先输出grade
cout<60?"fail":"pass";//比较cout和60 错误!!

4.8 位运算符

**一般来说,如果运算对象是“小整型”,则它的值会被自动提升成较大的整数类型,例如char类型对象在操作时会被自动转换成int类型(高位补0)**运算对象可以是带符号的,也可以是无符号的。若运算对象是带符号的且它的值为负,那么位运算符如何处理运算对象的符号位依赖于机器。而且此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。

建议仅将位运算符用于处理无符号类型整数。

移位运算符

移位运算符满足左结合律,它的优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。因此在一次使用多个运算符时,有必要在适当的地方加上括号使其满足我们的要求。

cout<<42+10;//正确:+的优先级更高,因此输出求和结果
cout<<(10<42);//正确:输出1
cout<<10<42;//错误:试图比较cout和42!

4.9 sizeof运算符

sizeof运算符返回一个表达式或一个类型名字所占的字节数,满足右结合律,其所得值是一个size_t类型的常量表达式。

C++11新标准允许我们使用作用域运算符来获取类成员的大小。通常情况下只有通过类的对象才能访问到类的成员,但是sizeof运算符无须我们提供一个具体的对象,因为要想知道类成员的大小无须真的获取该成员。

int x[10];
int *p=x;
cout<<sizeof(x)/sizeof(*x)<<endl;//10
cout<<sizeof(p)<<endl;//8 指针8字节
cout<<sizeof(*p)<<endl;//4
cout<<sizeof(p)/sizeof(*p)<<endl;

4.11 类型转换

int ival=3.541+3;//编译器可能会警告该运算损失了精度

上述的类型转换是自动执行的,无须程序员的介入,它们被成为隐式转换。

4.11.2 其他隐式类型转换

除了算术转换之外还有几种隐式类型转换,如下:

类型简单介绍
数组转换成指针大多数情况下,数组自动转换成指向数组首元素的指针;当数组被用作decltype关键字的参数时及用一个引用来初始化数组时,上述转换不会发生。
指针的转换常量整数值0或者字面值nullptr能转换成任意指针类型;指向非常量的指针能转换成void*;指向任意对象的指针能转换成const void*
转成布尔类型若指针或算术类型的值为0,转换结果为false;否则为true。
转换成常量允许将指向非常量类型的指针转换成指向相应的常量类型的指针。也就是说T是一种类型,可以将指向T的指针或引用分别转换成指向const T的指针和引用,相反的转换不存在。

4.11.3 显式转换

命名的强制类型转换

一个命名的强制类型转换有如下形式:

cast_name<type>(expression)

type是转换的目标类型,expression是要转换的值。若type是引用类型,则结果是左值。cast_name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。cast_name指定了执行哪种转换。

static_cast

任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。

当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。

static_cast对于编译器无法自动执行的类型转换也非常有用。如,我们可以使用它找回存在于void*指针中的值:

 void *p=&d;//正确:任何非常量对象的地址都能存入void *中
double *dp=static_cast<double*>(p)
const_cast

const_cast只能改变运算对象的底层const,即将常量对象转换成非常量对象。若对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为;若对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。

const char *pc;
char *p=const_cast<char *>(pc);//正确:但是通过p写值是未定义的行为

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

const char *cp;
char *cp=static_cast<char*>(cp);//错误:static_cast不能改变运算对象的常量属性
string str=static_cast<string>(cp);//正确:static_cast可以将字面值转换成string类型
string str2=const_cast<string>(cp);//错误:const_cast只改变常量属性   
reinterpret_cast

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

int *ip;
char *pc=reinterpret_cast<char*>(ip);
string str(ip);//错误

因为pc所指的真实对象是一个int而份字符,如果把pc当成普通的字符指针使用就可能在运行时发生错误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值