C语言

5. C语言--操作符

Posted on 2022-01-30,17 min read

5.操作符

5.1 算术操作符

+ - * / %
  • 除%操作符之外,其他几个操作符可以作用于整数和浮点数。

  • 对于/操作符如果两个操作符都为整数,执行整数除法。只要有一个浮点数,则执行浮点数除法。

    float num = 1 / 2; // num=0.000000; 
    
    float num = 1 / 2.0; // num=0.500000;
    // 这里的2.0 是double类型 而num为float类型
    // 可以做如下调整
    
    float num = 1f / 2.0f;
    double num = 1 / 2.0;
    
    
  • %操作符两端必须都是整数,可以是负数,但不能为0。

5.2 移位操作符

>> <<

5.2.1 非负整数的二进制表示形式

  • 原码:直接根据数值写出二进制
  • 反码:原码符号位不变,其他位按位取反
  • 补码:反码加1
// 非负整数在内存中存放的是原码(非负整数的原码、反码、补码相同)
// 负整数在内存中存放的是补码

5.2.2 左移

算数左移和逻辑左移相同,都是左边抛弃,右边补0。

5.2.3 右移

  • 算术右移(通常采用)

    向右移动,首位补原二进制位的第一位

  • 逻辑右移

    向右移动,首位补0

// 只能移动非负整数位

int main()
{
  int ret = -1 >> 1;
  printf("%d\n", ret);
  return 0;
}


5.3 位操作符

& | ^ // 操作数必须为整数

5.3.1 按位与

int main()
{
  int ret = 3 & 4; 
  // 00000000000000000000000000000011 
  //                                & 
  // 00000000000000000000000000000100
  printf("%d\n", ret);
  return 0;
}


5.3.2 按位或

// 按位或
int main()
{
  int ret = 3 | 4; 
  // 00000000000000000000000000000011 
  //                                |
  // 00000000000000000000000000000100
  printf("%d\n", ret);
  return 0;
}


5.3.3 按位异或

相同为0,相异为1

int main()
{
  int ret = 3 ^ 4; 
  // 00000000000000000000000000000011 
  //                                ^
  // 00000000000000000000000000000100
  printf("%d\n", ret);
  return 0;
}
// 奇数^奇数=偶数
// 奇数^偶数=奇数
// 偶数^偶数=偶数


5.3.4 题目

交换两个int变量的值,不使用第三个变量

  • 方法一

    int main()
    {
      int a = 3;
      int b = 4;
      a = a + b;
      b = a - b;
      a = a - b;
      printf("a=%d, b=%d\n", a, b);
      return 0;
    }
    // 问题:数字过大,内存溢出
    


  • 方法二

    int main()
    {
      int a = 3;
      int b = 4;
      a = a ^ b;
      b = a ^ b;
      a = a ^ b;
      printf("a=%d, b=%d\n", a, b);
      return 0;
    }
    // 没有进位,不可能发生溢出
    // 异或特点:
    //      1.任何数和他本身异或,结果为0
    //      2.任何数和0异或,结果为他本身
    //  底层逻辑:
    //      b = a ^ b ^ b = a (前两行代码)
    //      a = a ^ b ^ b ^ a ^ b = b (第三行代码)
    


5.3.5 练习

编写代码实现:求一个整数存储在内存中的二进制中1的个数

  • 方法一

    int main()
    {
      int num = 5;
      int count = 0;
      for (int i = 0; i < 32; i++)
      {
        if (num % 2 == 1)
        {
          count++;
        }
        num = num >> 1;
      }
      printf("count=%d\n", count);
      return 0;
    }
    
    


    缺点:不能统计负数的二进制中1的个数。

  • 方法二

    int main()
    {
        int num = -5;
        int count = 0;
        for (int i = 0; i < 32; i++)
        {
            if ((num >> i) & 1)
            {
                count++;
            }
        }
        printf("count=%d\n", count);
        return 0;
    }
    // -5 
    // 原码 10000000000000000000000000000101
    // 补码 11111111111111111111111111111011
    // 右移0位
    // 11111111111111111111111111111011 & 00000000000000000000000000000001
    // 00000000000000000000000000000001 -> 1
    // 右移一位
    // 01111111111111111111111111111101 & 00000000000000000000000000000001
    // 00000000000000000000000000000001 -> 1
    // 右移二位
    // 00111111111111111111111111111110 & 00000000000000000000000000000001
    // 00000000000000000000000000000000 -> 0
    // ...
    // 右移31位
    // 00000000000000000000000000000001 & 00000000000000000000000000000001
    // 00000000000000000000000000000001 -> 1
    
    
  • 方法三

    int main()
    {
        int num = -5;
        int count = 0;
        while (num)
        {
            num = num & (num - 1);
            count++;
        }
        printf("count=%d\n", count);
        return 0;
    }
    // num & (num - 1)
    // 11111111111111111111111111111011 => num
    // 11111111111111111111111111111011 & (11111111111111111111111111111011 - 1)
    // 11111111111111111111111111111011 & 11111111111111111111111111111010
    // 11111111111111111111111111111010 => num
    // 11111111111111111111111111111010 & (11111111111111111111111111111010 - 1)
    // 11111111111111111111111111111010 & 11111111111111111111111111111001
    // 11111111111111111111111111111000 => num
    // ...
    // 每次把num&(num-1)的结果再赋值给num
    // 每num&(num-1)运算一次,就会将num中的1变成0
    // 通过n次num&(num-1)运算使得num=0
    // 此时n为num的二进制中1的个数
    


5.4 赋值操作符

= += -= *= /= %= >>= <<= &= |= ^=

// 连续赋值:
int a = 0;
int b = 0;
int c = 1;
a = b = c + 1; // 从由向左赋值(不推荐)

5.5 单目操作符

只有一个操作数

!      // 逻辑反操作
-      // 负
+      // 正
sizeof // 操作数类型长度
~      // 按位取反
--     // 前置 后置
++     // 前置 后置
*      // 解引用(间接访问)操作符
&      // 取址       
(type) // 强制类型转换

5.5.1 sizeof

sizeof是一个操作符,不是函数。单位:字节

  • 例1

    int main()
    {
      short s = 5;
      int a = 10;
      printf("%d\n", sizeof(s = a + 2)); 
      // sizeof括号中放的表达式,不参与运算
      // 在编译期间处理sizeof(s = a + 2); 而s = a + 2在程序运行时计算
      // 大数据放在小空间里被截断
      printf("%d\n", s); // 5
      return 0;
    }
    


  • 例2

    int main()
    {
      int a = 10;
      printf("%d\n", sizeof(a));
      printf("%d\n", sizeof a);
      printf("%d\n", sizeof(int));
      //printf("%d\n", sizeof int);
    
      char arr[10] = { 0 };
      printf("%d\n", sizeof(arr));
      printf("%d\n", sizeof arr);
      printf("%d\n", sizeof(char [10]));
    }
    


  • 例3

    void foo1(int arr[10])
    {
      printf("%d\n", sizeof(arr));
    }
    void foo2(char arr[10])
    {
      printf("%d\n", sizeof(arr));
    }
    
    int main()
    {
      int arr1[10] = { 0 };
      char arr2[10] = { 0 };
      printf("%d\n", sizeof(arr1)); // 40
      printf("%d\n", sizeof(arr2)); // 10
      foo1(arr1); // 4或8
      foo2(arr2); // 4或8
      return 0;
    }
    // 数组作为函数参数传递时,实际传递的是数组的指针
    


5.5.2 ~

按位取反

int main()
{
  int a = 13;
  // 00000000000000000000000000001101
  // 将a的二进制位第五位置零
  // 00000000000000000000000000001101  ->  13的二进制
  // 00000000000000000000000000010000  ->  1<<4
  //                                |  ->  按位或
  // 00000000000000000000000000011101  ->  结果29
  a = a | (1 << 4);
  printf("%d\n", a);
  
  // 将a的二进制位第五位置一
  // 00000000000000000000000000011101  ->  29的二进制
  // 11111111111111111111111111101111  ->  ~(1<<4)
  //                                &  ->  按位与
  // 00000000000000000000000000001101  ->  结果13
  a = a & (~(1 << 4));
  printf("%d\n", a);
  return 0;
}


5.5.3 ++/—

// 前置:先++,再使用
// 后置:先使用,再++
// (使用包括赋值和函数传参)
  • 后++

  • 先++

  • 垃圾代码

    int main()
    {
      int a = 1;
      int b = (++a) + (++a) + (++a);
      printf("%d\n", b);
      return 0;
    }
    // 运行结果:vs2019中结果是12
    //         linux中结果是10
    // 垃圾代码,不做过多研究 
    


5.5.4 & *

& // 取址
* // 解引用
  • int main()
    {
      int num = 100; // &获取对象所处的内存地址,取址操作符
      int* pn = &num; // 此处*不是操作符,仅说明pn是一个指针变量
      *pn = 200; // 此处*才是解引用操作符,将200赋值给pn所指的对象
      printf("pn=%p, num=%d\n", pn, num);
      return 0;
    }
    


5.5.5 (type)

int main()
{
  int num = (int)3.1315; // 强制类型转换
  printf("%d\n", num);
  return 0;
}


5.6 关系操作符

> >= < <= != ==

// = 赋值
// == 判断是否相等

// 比较两个字符串是否相等不能使用==

5.7 逻辑操作符

&& 逻辑与:从前往后找到第一个为假的值返回0,否则返回1
|| 逻辑或:从前往后找到第一个为真的值返回1,否则返回0

5.7.1 习题

如下代码运行结果:

int main()
{
  int i = 0, a = 0, b = 2, c = 3, d = 4;
  i = a++ && ++b && d++;
  //i = a++ || ++b || d++;
  printf("a=%d, b=%d, c=%d, d=%d\n", a, b, c, d);
}

// &&: 左边确定为假,后边不用再进行计算
// ||: 左边确定为真,后边不用再进行计算



5.8 三目操作符

exp1 ? exp2 : exp3;
// 如果exp1为真,则计算exp2,整个表达式的结果为exp2的结果,exp3不计算
// 如果exp2为假,则计算exp3,整个表达式的结果为exp3的结果,exp2不计算
  • int main()
    {
      int a = 3;
      int b = 4;
      int ret = 0;
      ret = a > b ? a++ : b++;
      printf("%d\n", ret);
      printf("a=%d, b=%d\n", a, b);
      return 0;
    }
    


5.8 逗号表达式

exp1, exp2, exp3;
// 从前往后依次执行,整个表达式的结果为最后一个表达式exp3的计算结果

5.9 下标引用

下标引用操作符有两个操作数,数组名和下标。

int main()
{
  int arr[10] = { 0 }; // 此处[]不是操作符,仅用来指定数组的大小
  arr[4] = 100; // 此处[]为操作符,指定数组中索引(或下标)为4的值为100
  printf("arr[4]=%d\n", arr[4]);
  return 0;
}

5.10 函数调用

有一个或多个操作数,当没有参数时,只有函数名一个操作数;当有参数时,操作时个数为参数个数+1。

// 函数调用
int Add(int x, int y) // 此处()不是操作符,仅用来说明形参及其类型
{
  return x + y;
}

int main() {
  int ret = Add(3, 4); // 此处()为函数调用操作符,即使没有参数,也要有() 操作数:Add 3 4共3个
  printf("%d\n", ret);
  return 0;
}


5.11 结构成员访问操作符

. 
-> 

// 使用格式:
结构体变量.结构体成员变量名
结构体指针变量->结构体成员变量名

  • int main()
    {
      struct Person
      {
        char name[10];
        int age;
        float height;
      };
      printf(".操作符\n");
      struct Person p = { "listen", 22, 185.0 };
      printf("name: %s\n", p.name);
      printf("age: %d\n", p.age);
      printf("height: %f\n", p.height);
    
      struct Person* pp = &p;
      printf("name: %s\n", (*pp).name);
      printf("age: %d\n", (*pp).age);
      printf("height: %f\n", (*pp).height);
    
      printf("->操作符\n");
      printf("name: %s\n", pp->name);
      printf("age: %d\n", pp->age);
      printf("height: %f\n", pp->height);
      return 0;
    }
    
    


5.12 表达式求值

表达式求值的顺序一般是由操作符的优先级和结合性决定。同样,有些表达式的操作数再求值的过程中可能需要转换为其他类型。

5.12.1 隐式类型转换

C的整形算数运算总是至少以缺省整形类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整形,这种转换称为整形提升。

包括:

  • 整形提升
  • 算数转换

5.12.2 整型提升

  • 概念

    在表达式计算时,各种整形(只有比int小的类型才会发生整型提升)首先要提升为int类型,如果int类型不足以表示的话,就需要提升为unsigned int类型,然后再执行表达式的运算。

  • 意义

    虽然机器指令中可能有现两个8比特字节这种字节相加指令,但是一般用途的CPU是难以直接实现这样的字节相加运算的。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。而表达式的整型运算要在CPU的相应运算器件内执行。因此,两个char类型的数进行相加运算时,是在CPU中执行,自然而然的需要先转换为CPU内整型操作数的标准长度。

  • 如何提升

    按照最高位进行整型提升

    • 正数整型提升:高位补0
    • 负数整形提升:高位补1
    • 无符号整数整形提升:高位补0
  • 例1

    int main()
    {
      char a = 3;
      char b = 127;
      char c = a + b;
      printf("%d\n", c)
      return 0;
    }
    
    // 原理
    char a = 3; //二进制: 00000011
    char b = 127; //二进制:01111111 
    char c = a + b;
    // + 操作符 整型提升
    //a 提升: 00000000000000000000000000000011
    //b 提升: 00000000000000000000000001111111
    //  相加: 00000000000000000000000010000010
    //c 截断: 10000010
    printf("c=%d\n", c);
    // c是char类型,而使用%d打印,c 整形提升(高位补1)
    // 11111111111111111111111110000010 补码
    // 11111111111111111111111110000001 反码(补码-1)
    // 10000000000000000000000001111110 原码(反码符号位不变,其余位取反)
    
    


  • 例2

    int main()
    {
      char a = 0xFF; // a整形提升之后为11111111111111111111111111111111
      short b = 0xFFFF; // b整形提升之后为11111111111111111111111111111111
      int c = 0xFFFFFFFF;
      if (a == 0xFF) {
        printf("a\n");
      }
      if (b == 0xFFFF) {
        printf("b\n");
      }
      if (c == 0xFFFFFFFF) {
        printf("c\n");
      }
      printf("%d\n", a == b);
      return 0;
    }
    // 其中a, b需要进行整形提升,而c不需要整形提升
    // a, b整形提升之后变成了负数,所以a==0xFF和b == 0xFFFF为假
    


  • 例3

    int main()
    {
      char a = 1;
      printf("%u\n", sizeof a);
      printf("%u\n", sizeof -a); // 类型提升为int类型
      printf("%u\n", sizeof +a);
      printf("%u\n", sizeof !a); // vs2022中为1 gcc中为4
      return 0;
    }
    


5.12.3 算数转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转化为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。

long double
double
float
unsigned long int
long int 
unsigned int 
int

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转化为另一个操作数的类型后执行运算。

  • int main()
    {
      float a = 1.23;
      int b = 5;
      float c = 0;
      c = a + b; 
      // a为float类型,b为int类型,
      // float类型值和int类型值相加时,
      // int类型值被转换为float类型
      printf("%f\n", c);
      return 0;
    }
    


5.13 表达式的属性

  • 值属性(运行后才能确定)
  • 类型属性(可推断,编译时确定)

5.14 操作符的属性

  • 复杂表达式的求值有三个影响的因素:
    • 操作符的优先级
    • 操作符的结合性
    • 是否控制求值顺序(例如&& || ,

5.14.1 优先级

运算符的优先级确定表达式中项的组合。如果优先级相同,则根据结合性确定计算顺序。

类别 运算符 结合性
后缀 () [] -> . ++ - - 从左到右
一元 + - ! ~ ++ - - (type)* & sizeof 从右到左
乘除 * / % 从左到右
加减 + - 从左到右
移位 << >> 从左到右
关系 < <= > >= 从左到右
相等 == != 从左到右
位与 AND & 从左到右
位异或 XOR ^ 从左到右
位或 OR
逻辑与 AND && 从左到右
逻辑或 OR
条件 ?: 从右到左
赋值 = += -= *= /= %=>>= <<= &= ^= =
逗号 , 从左到右

5.14.2 一些问题表达式

  • 表达式1

    a * b + c * d + e * f;
    // 计算原理:
    // 方法1:
    a * b;
    c * d;
    e * f;
    a * b + c * d + e * f
    // 方法2:
    a * b;
    c * d;
    a * b + c * d;
    e * f;
    a * b + c * d + e * f;
    
    
  • 表达式2

    c + --c;
    // 无法确定左操作数是在--c之前还是--c之后确定
    // 计算原理:
    // 方法1:
    --c;
    c;
    c + --c;
    
    // 方法2:
    c;
    --c;
    c + --c;
    
  • 代码3-错误代码

    int main()
    {
      int i = 10;
      i = i-- - --i * (i = -3) * i++ + ++i;
      printf("i = %d\n", i);
      return 0;
    }
    // 输出结果在不同的编译器中结果不同
    
  • 代码4- 错误代码

    int fun()
    {
      static int count = 1;
      return ++count;
    }
    
    int main()
    {
      int answer;
      answer = fun() - fun() * fun(); // 2 - 3 * 4 (vs2022中)
      printf("%d\n", answer); // -10(vs2022中)
      return 0;
    }
    // 结果不可控,fun()函数的调用顺序不同,结果不同
    
  • 代码5-错误代码

    int main()
    {
      int i = 1;
      int ret = (++i) + (++i) + (++i);
      printf("%d\n", ret);
    }
    // VS -> 12
    // 执行原理
    ++i;
    ++i;
    ++i;
    i = 4
    4 + 4 + 4;
    
    // gcc -> 10
    ++i;
    ++i;
    i = 3;
    3 + 3;
    ++i;
    i = 4;
    3 + 3 + 4;
    
    


5.14.3 总结

对于同一个表达式或一段代码,即使有确定的优先级和结合性,仍然有不确定的计算或运行方式,且不同方式计算出来的结果不同,则该表达式或代码是存在问题的,不应该使用这样的表达式和代码。


下一篇: 4. C语言--数组→