C++ 学习笔记之(4)-表达式、运算符与类型转换

C++ 学习笔记之(4)-表达式、运算符与类型转换

表达式由一个或多个运算对象组成,对表达式求值将得到一个结果。字面值和变量是最简单的表达式,其结果就是字面值和变量的值,把运算符和运算对象组合可以生成较复杂的表达式

基础

基本概念

  • 对于含有多个运算符的复杂表达式来说,要想理解它的含义必须要理解运算符的优先级、结合律以及运算对象的求职顺序

  • 表达式求值过程中,运算对象通常由一种类型转换成另一张类型。

  • 左值(lvalue):对象的身份,即在内存的位置

    • 赋值运算符需要一个(非常量)左值作为其左侧运算对象,结果也是左值
    • 取地址符作用域一个左值运算对象,返回一个指向该运算对象的指针,这个指针是右值
    • 内置解引用运算符、下标运算符的求值结果都是左值
    • 内置类型和迭代器的递增递减运算符作用域左值运算对象
  • 右值(rvalue):对象的值(内容)

  • 关键字decltype作用于左值,得到的是引用类型

    int a = 0;
    int *p = &a;
    decltype(*p) b;  // 解引用运算符是生成左值,所以 b 结果是 int &, 即引用类型,未初始化
    decltype(&p) c;  // 取地址符生成右值,所以 c 结果是 int **, 指针的指针

求值顺序

优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。

int i = f1() * f2();  // f1 和 f2 会在乘法之前调用,但却不知道 f1 和 f2 的执行顺序
int i = 0;
cout << i << " " << ++i << endl;  // 未定义,可能输出1 1,也可能是0 1

算术运算符

这里写图片描述
* 算数运算符能够作用于任意算数类型,以及任意能转换成算数类型的类型。算术运算符的运算对象和求值结果都是右值

  • 一元负号运算符对运算对象值取负后,返回其(提升后的)副本

    int i = 1024;
    int k = -1;  // k 是 -2014
    bool b = true;
    bool b2 = -b;  // b2 是 true
  • 布尔值不参与运算,如上代码所示,bool类型的运算队形先被提升为int类型1,求负或为-1,不为0,故b2为真

  • 取余:若m % n 不等于0, 则它的负号和m相同。

逻辑和关系运算符

logistic_and_relation_operator

  • 对这两类运算符来说,运算对象和求值结果都是左值
  • 短路求值:即当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值,对逻辑与和逻辑或操作符。
  • 进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值truefalse作为运算对象

赋值运算符

  • 赋值运算符的左侧运算对象必须是一个可修改的左值
  • 对于复合运算符(+=, -= 等)都完全等价于a = a op b; , 唯一的区别是左侧运算对象的求值次数:使用复合运算符只求值一次,使用普通的运算符则求值两次(一次计算,一次赋值)

递增和递减运算符、成员访问运算符、条件运算符

  • 前置版本(++a):将对象本身作为左值返回
  • 后置版本(a++):将对象原始值的副本作为右值返回
auto pbeg = v.begin();
*pbeg++;  // 正确, 返回*pbeg, 然后++pbeg; 因为后置递增运算符优先级高于解引用运算符

string s1 = "a string", *p = &s1;
*p.size();  // 错误: p是一个指针,没有名为size的成员, 因为解引用运算符优先级低于点运算符

string finalgrade = (grade < 60) ? "fail" : "pass";

位运算符

bit_operator

  • 左移:右侧插入0
  • 右移:依赖于其左侧运算对象的类型
    • 无符号类型:左侧插入0
    • 带符号类型:左侧插入符号位的副本或者值0, 如何选择视具体环境而定

注意:位运算是一个很重要的知识点,需要深入学习

sizeof 运算符

sizeof运算符返回一条表达式或一个类型名字所占的字节数,结果为一个size_t类型的常量表达式

  • sizeof运算符的运算对象有两种形式
    • sizeof (type)
    • sizeof expr: 返回表达式结果类型的大小。sizeof并不实际计算其运算对象的值
  • sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有真正使用,sizeof不需要真的解引用指针也能知道它所指对象的类型。
  • sizeof运算符的结果部分地依赖于其作用的类型:
    • charchar类型表达式:1
    • 引用类型:被引用对象所占空间的大小
    • 指针:指针本身所占空间的大小
    • 解引用指针:指针指向的对象所占空间的大小,指针不需要有效
    • 数组:整个数组所占空间的大小,sizeof运算不会把数组转换成指针来处理
    • string对象或者vector对象:该类型福鼎部分的大小,不计算对象中的元素占用了多少空间

类型转换

C++语言不会直接操作两个不同类型的值,而是先根据类型转换规则设法将运算对象的类型统一后再运算,由于是自动执行,故被称作隐式转换

何时发生隐式类型转换

  • 在大多是表达式中,比int类型小的整形值首先提升为较大的整数类型
  • 在条件中,非布尔值转换成布尔类型
  • 初始化过程中,初始值转换成变量的类型;赋值语句中,右侧运算对象转换成左侧运算对象的类型
  • 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
  • 函数调用也会发生类型转换(后面学习)

算数转换

算数转换是把一种算数类型转换成另一种算数类型

  • 运算符的运算对象转换成最宽的类型,比如一个运算对象为long double, 则另一个运算对象必会转为long double
  • 表达式既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型
  • 整形提升:小整数类型转换成较大的整数类型
    • bool、char、signed char、unsigned char、short、unsigned short,如果int放得下,就提升为int,否则提升为unsigned int类型
    • 较大的char类型(wchar_t, char16_t, char32__t)提升为int,unsigned int, long, `…..等中最小的类型,前提是放得下
  • 某运算对象为无符号类型
    • 无符号 >= 带符号类型:带符号的运算对象转换成无符号,如果带符号的值为负值,则结果无法判断
    • 无符号 < 带符号:转换结果依赖于机器,
    • 如果无符号类型的所有值都能存在该带符号类型中,则无符号运算对象转换为带符号类型
    • 若不能,则带符号类型的运算对象转换成无符号类型

其他隐式类型转换

  • 数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针
  • 指针的转换
    • 常量整数值0或者字面值nullptr能转换成任意指针类型
    • 指向任意非常量的指针能转换成void *
    • 指向任意对象的指针能转换成const void *

显示转换

命名的强制类型转换cast-name<type>(expression);, 其中cast-namestatic_cast, dynamic_cast, const_castreinterpret_cast

static_cast

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

  • 当需要把一个较大的算数类型赋值给较小的类型时, static_cast非常有用,强制类型转换表示:我知道并且不在乎精度损失,故编译器不会出现警告信息

  • static_cast可用于编译器无法自动执行的类型转换

    void *p = &d;  // 正确:任何非常量对象的地址都能存入void *
    double *dp = static_cast<double*>(p);  // 正确:将void *转换成初始的指针类型.如果类型不符,则未定义

const_cast

  • 只能改变运算对象的底层const,即可用来将常量对象转换成非常量对象的行为。

    一旦去掉了对象的const性质,编译器就不再阻止我们对该对象进行写操作了。若对象本身不是常量,使用强制类型转换获得写权限是合法的行为。但若对象是一个常量,在使用const_cast执行写操作就会产生未定义的后果。

  • 其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误

  • const_cast常常用于有函数重载的上下文中

reinterpret_cast

通常为运算对象的位模式提供较低层次上的重新解释。reinterpret_cast本质上依赖于机器,非常危险,需要对涉及的类型和编译器实现转换的过程非常了解

int *ip;
char *pc = reinterpret_cast<char*>(ip);  // 等价于 char *pc = (char *) ip;
string str(pc);  // 运行时错误,pc 实质上指的是一个int

dynamic_cast

  • dynamic_cast的使用形式如下所示, type必须是一个类类型,并且通常含有虚函数

    • dynamic_cast<type*>(e):e必须是一个有效的指针
    • dynamic_cast<type&>(e):e必须是一个左值
    • dynamic_cast<type&&>(e): e不能使左值
  • e 的类型必须符合一下三个条件中的任意一个

    • 目标type的共有派生类
    • 目标type的共有基类
    • 目标type的类型
  • 转换失败时

    • 若目标是指针类型:结果为0
    • 若目标是引用类型:抛出bad_cast异常
    // 指针类型的 dynamic_cast
    // bp 指针指向 Base(至少含有一个虚函数), Derived 是 Base 的共有派生类
    if(Derived *dp = dynamic_cast<Derived*>(bp))
    {
      // 转换成功, 使用 dp 指向的 Derived 对象
    }else{  // bp 指向一个 Base 对象
      // 转换失败, 使用 dp 指向的 Base 对象
    }
    
    // 引用类型的dynamic_cast
    // 因为不存在控引用,对于引用失败,应该捕获异常
    void f(const Base &b)
    {
      try{
          const Derived &d = dynamic_cast<const Derived&>(b);
            // 使用 b 引用的 Derived 对象
      }catch(bad_cast){
          // 处理类型转换失败的情况
      }
    }

运算符优先级表

operator_priority_table

结语

  • 对于多运算符的表达式,理解优先级、结合律和求值顺序
  • 类型转换非常重要,要专门深入学习

猜你喜欢

转载自blog.csdn.net/u011221820/article/details/80010154