Expressions

基础

基本概念

  • 当我们使用重载运算符时,操作数的类型以及运算的结果都取决于运算的定义;然而,操作数的个数以及运算符的优先级和结合性都是无法改变的。
  • C++中所有的表达式都是右值(rvalue)或是左值(lvalue),这两个概念继承自C语言,原本是为了帮助记忆:左值可以放在赋值语句左侧,而右值不能(在C++中有所区别)。
  • 通常,我们使用一个右值时,使用的是对象的值(内容);我们使用一个左值时,使用的是对象的身份(在内存中的位置)。
  • 一个重要的原则是(在13.6节中有一个例外),当需要使用右值时可以使用左值代替,但不能把右值当成左值使用。
  • 内置类型以及迭代器的递增递减运算作用于左值运算对象,其前置版本返回结果也是左值。
  • decltype应用于表达式(而非变量)时,若表达式产生左值,结果将是引用类型。

求值顺序

  • 对于不指定求值顺序的运算符,在一个表达式中引用并改变相同的对象是错误的。

    1
    2
    int i = 0;
    cout << i << " " << ++i << endl; // undefined
  • 有4个运算符保证了运算对象的求值顺序:逻辑与运算符(&&),逻辑或运算符(||),条件运算符(?:)和逗号运算符(,)。

算术运算符

  • 以下代码中,对short_value的赋值是未定义的。

    1
    2
    3
    short short_value = 32767;    // max value of shorts are 16 bits
    short_value += 1; // this calculation overflows
    cout << "short_value: " << short_value << endl;
  • (-m) / nm / (-n)的值与-(m / n)相等,m % (-n)m % n相等,(-m) % n-(m % n)相等。

赋值运算符

  • 如果左侧运算对象是内置类型,那么初始化列表中只能包含一个值,并且在强制转化中不能丢失信息。
  • 无论左侧运算对象的类型是什么,初始化列表都可以为空;此时编译器会创建一个值初始化的临时量并将其赋给左侧运算对象。
  • 使用复合赋值运算符对左侧运算对象只求值一次,普通赋值运算符则求值两次。

递增和递减运算符

  • 递增和递减运算符作用于左值运算对象,前置版本将对象本身作为左值返回,后置版本将对象初始值的副本作为右值返回。
  • 以下代码是未定义行为:
    1
    2
    while (beg != s.end() && !isspace(*beg))
    *beg = toupper(*beg++); // error: this assignment is undefined

问题在于:=的两侧都使用了beg并且右侧运算对象改变了beg

成员访问运算符

  • 解引用运算符的优先级低于点运算符。

条件运算符

  • 若条件运算符的两个表达式都是左值或者都能转换成同一种左值类型时,运算的结果是左值,否则结果为右值。
  • 条件运算符是右结合的:
    1
    2
    finalgrade = (grade > 90) ? "high pass" 
    : (grade < 60) ? "fail" : "pass";

位运算符

  • 通常,如果运算对象是一个“小整型”,则它的值首先会自动转换为较大的整数类型。
  • 左移运算符(<<)在右侧插入0二进制位。右移运算符(>>)的行为取决于左侧运算对象的类型:若该运算对象为unsigned类型,则在左侧插入0二进制位;若该运算对象为signed类型,则在左侧插入符号位的拷贝或者0,取决于具体实现。
  • 位移运算符具有中等的优先级:低于算术运算符,但高于关系、赋值和条件运算符。

sizeof运算符

  • sizeof返回的结果为常量表达式,其类型为size_t
  • sizeof不对其运算对象进行求值。

逗号运算符

  • 与逻辑与、逻辑或和条件运算符相同,逗号运算符保证了运算对象求值的顺序。
  • 逗号运算符左侧的表达式在求值后被舍弃。逗号运算符的结果是右侧表达式的值,若右侧运算对象为左值,那么最终结果也是左值。

类型转换

  • 如果两种类型可以相互转换(conversion),那么它们就是相关联的。
  • 大多数情况下,如果表达式中既有整数类型运算对象也有浮点数类型的运算对象,整型会被转换为浮点型。
  • 在以下情况下,编译器会自动转换运算对象的类型:
    • 在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型。
    • 在条件中,非布尔值转换为布尔值。
    • 初始化过程中,初始值转化为变量的类型;在赋值语句中,右侧运算对象转换为左侧运算对象的类型。
    • 若果算术运算或关系运算的运算对象有多种类型,需要转换为同一种类型。
    • 函数调用时也会发生类型转换。

算术转换

  • 算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。

其他隐式类型转换

  • 数组转化为指针:在大多数用到数组的表达式中,数组自动转换为指向数组首元素的指针:

    1
    2
    int ia[10];    // array of ten ints
    int* ip = ia; // convert ia to a pointer to the first element

    当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeoftypeid运算符的运算对象时,上述转换不会发生。

  • 指针转换:常量整数值0及字面值nullptr能转换为任意指针类型;指向任意非常量的指针能转换成void*;指向任意对象的指针可转换为const void*

显式转换

  • 命名的强制类型转换形式如下:

    1
    cast-name<type> (expression);

    type是引用类型,则结果是左值。

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

    1
    double slope = static_cast<double>(j) / i;
  • const_cast只能改变运算对象的底层const

    1
    2
    const char *pc;
    char *p = const_cast<char*>(pc); // ok: but writing through p is undefined

只有const_cast能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译时错误。同样的,不能使用const_cast改变表达式的类型。const_cast常用于有函数重载的上下文中。

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

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!