表达式细节问题


基本概念

  1、运算符: c++除了一些基本运算符,函数调用也是一种特殊的运算符 ,它对运算符对象的数量没有限制。

  2、运算对象转换: 一般的二元运算符都要求两个运算对象的类型相同,但是很多时候即使运算对象的类型不相同也没关系(存在隐式转换),只要它们能被转换成同一种类型即可。小整数类型(如bool、char、short等)通常会被提升成较大的整数类型,主要是int。

  3、左值和右值: 当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。赋值运算符的左侧运算对象必须是左值,右侧运算对象可以是左值也可以是右值。

  4、优先级与结合律: 每个运算符都有其对应的优先级和结合律,优先级规定了符合表达式中运算符组合的方式,结合律则说明当运算符的优先级一样时应该如何组合(右结合律:按照从右向左的顺序组合;左结合律:按照从左到右的顺序组合)。

  5、求值顺序: 对于那些没有定义执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为,比如:

// <<运算符没有明确规定何时以及如何对运算对象求值,因此下面的输出表达式是未定义的:
int i=0;
cout<<i<<" "<<++i<<endl;    // 未定义的

  运算对象的求值顺序与优先级和结合律无关,在一条形如f()+g()*h()+j()的表达式中:

  • 优先级规定,g()的返回值和h()的返回值相乘。

  • 结合律规定,f()的返回值先与g()和h()的乘积相加,所得结果再与j()的返回值相加。

  • 对于这些函数的调用顺序并没有明确的规定。

    注意: 如果f、g、h和j是无关函数,它们既不会改变同一对象的状态也不执行IO任务,那么函数的调用顺序不受限制。反之,如果其中某几个函数影响同一对象,则它就是一条错误的表达式,将产生未定义的行为。

    总结: c++语言没有明确规定大多数运算符的求值顺序,给编译器优化留下了余地,并且对于同一个表达式中的函数,它们的调用顺序也没有明确定义。

算术运算符

  1、运算符优先级分组: 一元运算符的优先级最高,接下来是乘法和除法,优先级最低的是加法和减法。

  2、算术运算符的运算对象和求值结果都是右值

  3、取整问题: c++语言的早期版本允许结果为负值的商向上或向下取整,c++11新标准 则规定商一律向0取整(即直接切除小数部分)。根据取余运算的定义,如果m和n是整数且n非0,则表达式(m/n)*n+m%n的求值结果与m相等。隐含的意思是,如果m%n不等于0,则它的符号和m相同,除了-m导致溢出的特殊情况,其他时候(-m)/n和m/(-n)都等与-(m/n),m%(-n)等于m%n,(-m)%n等于-(m%n)。如果取余的两个运算对象的符号不同,则负号所在的位置不同运算结果也不同,取余结果的符号和m的符号一致!

  4、常见优先级结合律例子:

  • 后置递增运算符的优先级高于解引用运算符,因此* pbeg++等价于* (pbeg++)。
  • 解引用运算符的优先级低于点运算符,所以执行解引用的子表达式两段必须加上括号。
*p.size();    // 错误:p是一个指针,它没有名为size的成员。
(*p).size();  // 正确
  • c++规定<、<=、>、>=的优先级高于==和!=,因此if(i!=j<k)等价于if(i!=(j<k))。

逻辑和关系运算符

  1、对于逻辑和关系运算符来说,它们的运算对象和求值结果都是右值

  2、短路求值: 逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值,这种策略称为求值短路

  3、相等性测试与布尔字面值: 如果想测试一个算术对象或指针对象的真值,最直接的方法就是将其作为if语句的条件:

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

//有时会试图将上面的真值测试写成如下形式:
if(val==true){....}   //注意:只有当val等于1时条件才为真!

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

赋值运算符

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

  2、赋值运算的结果是它的左侧运算对象,并且是一个左值 ,如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。

  3、赋值运算满足右结合律,这一点与其他二元运算符不太一样:

int val,jval;
ival=jval=0;  //正确:都被赋值为0

  4、复合赋值运算符:

算术运算符 += -= *= / = &=
位运算符 <<= > > = &= ^= |=

  任何一种复合运算符都完全等价于:a= a op b;

  唯一的区别是左侧运算对象的求值次数:使用复合运算符只求值一次,使用普通的运算符则求值两次。这两次包括:一次是作为右边子表达式的一部分求值,另一次是作为赋值运算的左侧运算对象求值。其中在很多地方,这种区别除了对程序性能有些许影响外几乎可以忽略不计。

  5、递增和递减运算符: 递增运算符(++)和递减运算符(--)为对象的加1和减1操作提供了一种简洁的书写形式。这两个运算符还可以应用于迭代器 ,因为很多迭代器本身不支持算术运算,所以此时递增和递减运算符除了书写简洁外还是必须的!

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

  注意: 因为递增运算符和递减运算符会改变运算对象的值,所以要提防在复合表达式中错用了这两个运算符:

//该循环的行为是未定义的
while(beg!=s.send() && !isspace(*beg))
  *beg=toupper(*beg++);         //错误:该赋值语句未定义
/*问题在于:赋值运算符左右两端的运算对象都用到了beg,并且右侧的运算对象还改变了beg的值,所以该语句是未定义的。*/ 

命名的强制类型转换

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

cast-name<type>(expression):

type是转换的目标类型而expression是要转换的值,如果type是引用类型,则结果是左值。
cast-name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种,其中dynamic_cast支持运行时类型识别。

  2、static_cast: 任何具有明确定义的类型转换,只要不包含底层const ,都可以使用static_cast。当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用,而且对于编译器无法自动执行的类型转换也非常有用,比如我们可以用static_cast找回存在于void*指针中的值:

void* p=&d;
double *dp=static_cast<double*>(p);   // 正确:将void*转换回初始的指针类型

  3、const_cast: 只能改变运算对象的底层const ,比如,

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

  对于将常量对象转换成非常量对象的行为,我们一般称其为去掉const性质 。一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权利是合法的行为。然而如果对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。

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

原文地址:https://www.cnblogs.com/xipuhu/p/7456666.html