【C++笔记】05 运算符及其优先级

 

1 表达式及语句

表达式:由操作数和运算符组成。例如:算术表达式A+B是由操作数A、B和二元运算符‘+’组成。

语句:语句通常以分号结尾,从功能上说,语句大体可分为执行性语句说明性语句两大类。执行性语句旨在表述程序的动作,又可分为赋值语句、控制语句和输入输出语句。说明性语句旨在定义各种不同数据类型的变量或运算。

2 赋值语句

在赋值语句A=B中,对赋值运算符“=”右边的B,我们需要的是它的值(称为右值),对左边的A,我们需要的是它所代表的存储单元(地址)(称为左值)。

赋值运算符的左操作数必须是非const的左值。

与其他二元操作符不同,赋值操作具有右结合特性,当表达式含有多个赋值操作时,从右向左结合。多个赋值操作中,各变量必须具有相同的数据类型,或者具有可转换为同一类型的数据类型。

3 自增、自减运算符

自增运算符:++

自减运算符:--

这两种运算符是一元算术运算符。

3.1 前缀运算与后缀运算

自增、自减运算符作用在变量之前时,称为前缀运算,如++i--i。作用在变量之后时,称为后缀运算,如i++i--

注:前缀运算与后缀运算的区别在于:以++操作为例,对于变量a,++a表示取a的地址,增加它的内容,然后把指放在寄存器中;a++表示取a的地址,把它的值装入寄存器中,然后增加内存中a的值。即前缀运算是“先变后用”,后缀运算是“先用后变”

3.2 自增、自减运算符作用的对象

自增、自减运算符只作用于变量,而不能作用于常量表达式。只要是标准类型的变量,不管是整型、实型,还是字符型,枚举型都可以作为运算对象。

如:i+++j++++i+(++j)++a+b++++array[--j]都是合法的,而++6(i+j)++'A'++++i+++j(&p)++这5个表达式则是不合法的。

注:为什么i+++j++合法而++i+++j不合法呢?这是因为C/C++的编译器对程序编译时,从左到右尽可能多地将字符组合成一个运算符或标识符,因此i+++j++等效于(i++)+(j++),这是合法的;而++i+++j相当于++(i++)+j,第1个++作用的对象是表达式i++,是不合法的。

前缀自增和后缀自增有个重要的区别就是:前缀自增能够用做左值表达式,后缀自增只能用于右值表达式。这是因为,前缀自增返回操作数本身,而后缀自增返回一个临时变量

3.3 自增、自减运算符的结合方向

自增、自减运算符及负号运算符的结合方向是从右向左。如k=-i++等效于k=-(i++)

4 关系与逻辑运算符

4.1 关系操作符

关系操作符(<<=>>=)具有左结合的特性。事实上,由于关系操作符返回bool类型的结果,因此很少使用其左结合特性。当出现多个关系操作符串接起来使用时,如:

if (i < j < k){ /***/ }

只要k>1,上述表达式的值就为true。这是因为第二个小于操作符的左操作数是第一个小于操作符的结果:true/false。也就是该条件将k与0/1作比较。因此为了实现我们想要的条件检验,应该为:

if (i < j && j < k){ /***/ }

4.2 逻辑操作符

逻辑操作符&&(与运算)、||(或运算)。

逻辑与和逻辑或操作符总是先计算其左操作数,然后再计算其右操作数。只有在仅靠左操作数数的值无法确定该逻辑表达式的结果时,才会求解其右操作数。这种求值策略称为“短路求值”。

给定以下形式:

expr1 && expr2
expr1 || expr2

仅当expr1无法确定表达式的值时,才会求解expr2

5 位运算符

位操作符使用整型的操作数。将其整型操作数视为二进制位的集合,为每一位提供检验和设置的功能。

操作符 功能 单双目 用法
& 按位与 expr1 & expr2
| 按位或 expr1 | expr2
^ 按位异或 expr1 ^ expr2
~ 取反 ~expr
<< 左移 expr1 << expr2
>> 右移 expr1 >> expr2

5.1 与、或、非及异或运算符

  • 按位与:仅当两位都为1时,结果为1,否则为0。
  • 按位或:仅当两位都是0时,结果为0,否则为1。
  • 取反:将操作数每位取反。
  • 按位异或:仅当两位不相同时,结果为1,否则为0。

判断一个整数n是否位2的正整数次幂:

if(n>1 && ((n & (n-1)) == 0))
    cout << "YES";

两个相同的数异或后结果为0,且满足交换律。

异或的使用

  • 寻找数成对出现时缺失的某一个数:

    对一组数A、B、C、D、A、C、B,则A^B^C^D^A^C^B=D,可以快速找到D只出现了1次。

  • 不使用第三方变量交换两个变量的值:

    a=a^b;
    b=a^b;
    a=a^b;
    
  • 不用算数运算符实现两个数的加法:

    int add_no_arithm(int a, int b)
    {
        if(b == 0) 
            return a;
          
        int sum = a ^ b;
        int carry = a & b;
        return add_no_arithm(sum, carry);
    }
    

5.2 移位运算符

<<>>分别为左移和右移操作符,移位时的补位规则为:

类型 左移 右移
int 低位补0 高位补符号位
unsigned int 低位补0 高位补0

注:负数左移时有可能会变成正数。

5.3 返回n转化为二进制后包含1的数量

  • 方法一:

    int fun(unsigned int n)
    {
        int count = 0;
        while(n > 0)
        {
            n &= (n-1);
            count++;
        }
        return count; 
    }
    //或
    int fun(int n)
    {
        int count = 0;
        while(n != 0)//考虑负数
        {
            n &= (n-1);
            count++;
        }
        return count; 
    }
    
  • 方法二:

     int fun(unsigned int n)
     {
         int count = 0;
         while(n)
         {
             if(n & 1)//判断最低位是不是为1
                 count++;
             n >>= 1;//每次右移一位
         }
         return count; 
     }
    
  • 方法三:

      int fun(int n)
      {
          int count = 0;
          unsigned int flag = 1;
          while(flag)
          {
              if(n & flag)
                  count++;
              flag <<= 1;
          }
          return count; 
      }
    

5.4 优先级

~运算符 > 移位运算符 > 与、或、异或运算符

6 类型转换

6.1 赋值转换

赋值转换是指将一种类型的值赋给另一种类型的变量时,值将会转换为接收变量的类型。如:

int ival = 3.14;//3.14将被截断为3
int* ip=0;//0将转换为空指针

6.2 表达式的转换

当同一个表达式中出现不同类型的量时,C++会根据不同的情况对操作数进行自动转换,分为“整型提升”和“运算时的转换”两种。

  • 整型提升:在表达式计算中,C++会将boolcharunsigned charsigned charshortsigned short型值都会自动转换成int型。

    注:unsigned shortint比较特殊:如果系统中int占4字节,unsigned short会转换成int,如果系统中int占2字节,则unsigned short会转换成unsigned int,从而避免了数据的丢失。

  • 运算时的转换:当运算涉及两种类型时,较小的类型将会被转换成较大的类型,即表达力低的类型将会被转换成表达力高的类型。各类型表达能力由低到高为:int(等价于signed int)→unsigned intlong(等价于signed long)→unsigned longfloatdoublelong double

6.3 显式转换

C++基本类型的指针之间不含有隐式转换(void*除外、const的某些用法为了兼容C语言也可隐式转换),需要显式转换。

显式转换也称为强制类型转换,在引入命名的强制类型转换操作符之前,显式强制转换用圆括号将类型括起来实现:

int* ip;
char* pc = (char*)ip;

C++引进了如下4个强制类型转换操作符用于显式转换:static_castdynamic_castconst_castreinterpret_cast

命名的强制类型转换符号的一般形式为:

cast_name<type>(expression)

其中cast_name为static_castdynamic_castconst_castreinterpret_cast之一,type为转换的目标类型,expression则是被强制转换的表达式。

6.3.1 static_cast

编译器隐式执行的任何类型转换都可以由static_cast显式完成。

double d = 97.0;
int i = static_cast<int>(d);
//等效于
double d = 97.0;
int i = d;

仅当类型之间可隐式转换时(除类层次间的下行转换以外),static_cast的转换才是合法的,否则将存在问题。

类层次间的下行转换不能通过隐式转换完成:

class base{};
class child: public base{};

base* b;
child* c;
c = static_cast<child*>(b);//下行转换,编译正确,但由于没有动态类型检查,因此不安全。
c=b;//编译不正确

6.3.2 dynamic_cast

dynamic_cast的转换类型必须类的指针类的引用或者void*。如果转换类型是指针类型,那么表达式也必须是一个指针;如果转换类型是一个引用,那么表达式也必须是一个引用。

注:与其他强制类型转换不同,dynamic_cast涉及运行时类型检查dynamic_cast运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表中,只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的,对于没有虚函数表的类使用会导致dynamic_cast编译错误

如果绑定到引用或指针的对象的类型不是目标类型,则dynamic_cast失败。如果转换到指针类型的dynamic_cast失败,则dynamic_cast的结果为0值;如果转换到引用类型的dynamic_cast失败,则抛出一个bad_cast类型的异常。

dynamic_cast操作符执行两个操作:首先验证被请求的转换是否有效,只有转换有效,操作符才是进行转换。

一般而言,引用或指针所绑定的对象的类型在编译时是未知的,基类的指针可以赋值为指向派生类对象,同样,基类的引用也可以用派生类对象初始化。因此,dynamic_cast操作符执行的验证必须在运行时进行。

dynamic_cast主要用于类层次间的上行转换和下行转换

dynamic_cast运算符可以在执行期决定真正的类型。如果下行转换是安全的(也就是说,如果基类指针或者引确实指向一个派生类对象),这个运算符会传回转型过的指针。如果downcast不安全,这个运算符会传回空指针。在上行转换时,dynamic_caststatic_cast效果是一样的,但dynamic_cast具有类型检查的功能,static_cast更安全

6.3.3 const_cast

const_cast可以将表达式的const性质转换掉。也只有const_cast才能将const性质转换掉。用const_cast来执行其他任何类型转换,都会引起编译错误。

const double value = 0.0;
double* ptr = nullptr;
//可以使用const_cast让ptr指向value
ptr = const_cast<double*>(value);

6.3.4 reinterpret_cast

reinterpret_cast与使用圆括号将类型括起来的显式类型转换操作功能相同。

int* ip;
char* pc = (char*)ip;
//等效于
int* ip;
char* pc = reinterpret_cast<char*>(ip);

7 运算符优先级表

优先级 运算符 名称或含义 使用形式 结合方向 说明
1 [] 数组下标 数组名[常量表达式] 左到右  
  () 圆括号 (表达式)/函数名(形参表) 左到右  
  . 成员选择(对象) 对象.成员名 左到右  
  -> 成员选择(指针) 对象指针->成员名 左到右  
2 - 负号运算符 -表达式 右到左 单目运算符
  (类型) 强制类型转换 (数据类型)表达式 右到左  
  ++ 自增运算符 ++变量名/变量名++ 右到左 单目运算符
  -- 自减运算符 --变量名/变量名-- 右到左 单目运算符
  * 取值运算符 *指针变量 右到左 单目运算符
  & 取地址运算符 &变量名 右到左 单目运算符
  ! 逻辑非运算符 !表达式 右到左 单目运算符
  ~ 按位取反运算符 ~表达式 右到左 单目运算符
  sizeof 长度运算符 sizeof(表达式) 右到左  
3 / 表达式/表达式 左到右 双目运算符
  * 表达式*表达式 左到右 双目运算符
  % 余数(取模) 整型表达式%整型表达式 左到右 双目运算符
4 + 表达式+表达式 左到右 双目运算符
  - 表达式-表达式 左到右 双目运算符
5 << 左移 变量<<表达式 左到右 双目运算符
  >> 右移 变量>>表达式 左到右 双目运算符
6 > 大于 表达式>表达式 左到右 双目运算符
  >= 大于等于 表达式>=表达式 左到右 双目运算符
  < 小于 表达式<表达式 左到右 双目运算符
  <= 小于等于 表达式<=表达式 左到右 双目运算符
7 == 等于 表达式==表达式 左到右 双目运算符
  != 不等于 表达式!=表达式 左到右 双目运算符
8 & 按位与 表达式&表达式 左到右 双目运算符
9 ^ 按位异或 表达式^表达式 左到右 双目运算符
10 | 按位或 表达式|表达式 左到右 双目运算符
11 && 逻辑与 表达式&&表达式 左到右 双目运算符
12 || 逻辑或 表达式||表达式 左到右 双目运算符
13 ?: 条件运算符 表达式1?表达式2:表达式3 右到左 三目运算符
14 = 赋值运算符 变量=表达式 右到左  
  /= 除后赋值 变量/=表达式 右到左  
  *= 乘后赋值 变量*=表达式 右到左  
  %= 取模后赋值 变量%=表达式 右到左  
  += 加后赋值 变量+=表达式 右到左  
  -= 减后赋值 变量-=表达式 右到左  
  <<= 左移后赋值 变量<<=表达式 右到左  
  >>= 右移后赋值 变量>>=表达式 右到左  
  &= 按位与后赋值 变量&=表达式 右到左  
  ^= 按位异或后赋值 变量^=表达式 右到左  
  |= 按位或后赋值 变量|=表达式 右到左  
15 , 逗号运算符 表达式,表达式,... 左到右 从左向右顺序运算

运算符优先级有几个简单的规则

1)()[]->.最高;

2)单目的比双目的高;算术双目比其他双目的高;

3)移位运算高于关系运算;关系运算高于按位运算(与,或,异或);按位运算高于逻辑运算;

4)三目的只有一个条件运算,低于逻辑运算。

5)赋值运算仅比,高,且所有的赋值运算符优先级相同,结合访问位从右向左。