• 【C语言从青铜到王者】第四篇·详解操作符


    本篇前言

    C语言的各种操作符让我们对数据的算数操作有了可能,也为人类利用计算机实现各种算法提供了强力的工具。今天,就让我们深入的了解一下C语言中所有的操作符和它们的使用规则与注意事项,掌握它们是我们提升算法能力的基础。

    各种操作符的介绍


    算数操作符

    符号名称表达式结果(a=6,b=5)
    +a+b11
    -a-b1
    *a*b30
    /a/b1
    %取模a%b1

    要点一:除号操作符的结果是否取整问题

    看下面这段程序

    #include
    int main()
    {
    	int a = 6 / 5;
    	printf("a = %d\n", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    结果是

    image.png

    如果把a的数据类型改成浮点数

    #include
    int main()
    {
    	float a = 6 / 5;
    	printf("a = %f\n", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    结果是

    image.png

    这是为什么呢?

    因为6/5这个表达式的结果本来就是1,把1放入什么类型的变量中结果都是1

    那如果想真的想得到小数应该怎么办呢?

    #include
    int main()
    {
    	float a = 6.0 / 5;
    	printf("a = %f\n", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    image.png

    我们发现:只要除号的两端至少有一个数字是浮点数,执行的结果就是浮点数

    如果仔细看会发现编译器会警告,原因是直接写出的小数,编译器默认是double类型的,double是双精度浮点数,精度比float高,如果这样赋值可能会丢失精度,所以我们最好用double类型接收。所以要不然就用float类型的数字写表达式,要不然就用double变量接收。

    以下两种写法都是可以的:

    #include
    int main()
    {
    	float a = 6.0f / 5.0f;
    	printf("a = %f\n", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    #include
    int main()
    {
    	double a = 6.0 / 5.0;
    	printf("a = %lf\n", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    要点二:取模操作符的操作数必须为整数

    只有整数相除有余数

    如果用小数取模就会报错:

    #include
    int main()
    {
    	double a = 6 % 5.0;
    	printf("a = %lf\n", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    image.png


    移位操作符

    符号名称表达式含义
    <<左移操作符a << 1把a的二进制位向左移动一位
    >>右移操作符a >> 1把a的二进制位向右移动一位

    左移:左边丢弃,右边补0

    #include
    int main()
    {
    	int a = 2;
    	int b = a << 1;
    	printf("b = %d\n", b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    image.png

    • 详解:
      a是int型,储存4字节32位有符号二进制数:
      00000000 00000000 00000000 00000010
      把a的二进制位向左移动一位,空位补0
      00000000 00000000 00000000 00000100
      得到的二进制输的十进制值为4

    右移

    算数右移:右边丢弃,左边补原符号位

    逻辑右移:右边丢弃,左边补0

    #include
    int main()
    {
    	int a = 10;
    	int b = a >> 1;
    	printf("b = %d\n", b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 详解
      原本a的二进制形式:
      00000000 00000000 00000000 00001010
      右移后(两种右移结果一致):
      00000000 00000000 00000000 00000101
      结果为5

    要点一:整数在内存中的存储形式

    码制正整数负整数
    原码根据十进制值写出的二进制序列二进制序列首位为符号位1
    反码等于原码原码符号位不变,其他位按位取反
    补码等于原码反码+1

    整数在内存中存放的是二进制的补码,各种操作二进制序列的操作符操作的也是补码

    整数最终被打印出来的是二进制的原码转换成十进制的值

    下面把a改成-1测试一下我的编译器是什么右移

    #include
    int main()
    {
    	int a = -1;
    	int b = a >> 1;
    	printf("b = %d\n", b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 详解:
      对于-1来说:
      原码 10000000 00000000 00000000 00000001
      反码 11111111 11111111 11111111 11111110
      补码 11111111 11111111 11111111 11111111
      假设为算数右移,补符号位1,右移一位结果为:
      补码 11111111 11111111 11111111 11111111
      反码 11111111 11111111 11111111 11111110
      原码 10000000 00000000 00000000 00000001
      结果应当仍未-1

    实际结果:
    image.png

    测试得知我的编译器为算术右移

    要点二:移位操作符不会改变被操作变量本身的值

    也就是说 a >> 1 并不会改变 a 的值

    要点三:移位的数字要为正整数

    移位的数字表示移动的位数,当然得是正整数


    位操作符

    符号名称含义规则
    &按位与对应二进制位相与有假为假 同真为真
    |按位或对应二进制位相或有真为真 同假为假
    ^按位异或对应二进制位相异或相同为假 相异为真

    这里的假就是0,真就是1

    #include
    int main()
    {
    	int a = 1 & 0;
    	int b = 1 | 0;
    	int c = 1 ^ 0;
    	int d = 1 ^ 1;
    	printf("a=%d\nb=%d\nc=%d\nd=%d\n", a, b, c, d);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    1的二进制补码是
    00000000 00000000 00000000 00000001
    0的二进制补码是
    00000000 00000000 00000000 00000000
    1&0前面所有位0和0相与都是0,最后一位1和0相与也是0,所以结果是0
    1|0前面所有位0和0相或都是0,最后一位1和0相或是1,所以结果是1
    1^0前面所有位0和0相异或都是0,最后一位1和0相异或是1,所以结果是1
    1^1前面所有位0和0相与都是0,最后一位1和1相与也是0,所以结果是0


    赋值操作符

    符号名称含义
    =直接赋值符把 = 右边的值赋给右边的变量
    +=、-=、*=、<<=、…复合赋值符a += b 就是 a = a + b ,以此类推
    #include
    int main()
    {
    	int a = 0;
    	a = a + 1;
    	int b = 0;
    	b += 1;
    	printf("a=%d\nb=%d", a, b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述


    单目操作符

    符号名称含义
    逻辑反操作假→真,真→假
    -负号操作符符号位取反
    &取地址操作符取出变量地址
    sizeof类型长度以字节为单位计算类型长度
    ~按位取反对一个数的内存二进制序列按位取反
    前置–,后置–自减1
    ++前置++,后置++自加1
    *解引用操作符通过地址找到变量所在的内存
    ( 数据类型 )强制类型转换把某个变量强制转换成括号内的类型

    要点一:单目操作符就是只有一个操作数的操作符

    要点二:详解sizeof

    sizeof操作符可以丈量变量所占空间的大小,单位是字节,结果是无符号整数,严格来说要用%u打印,也可以用%d打印

    #include
    int main()
    {
    	int a = 10;
    	char arr[10] = { 0 };
    	printf("%d\n", sizeof(a));
    	printf("%d\n", sizeof(int));
    	printf("%d\n", sizeof a);//证明sizeof是一个操作符,而不是函数
    	printf("%d\n", sizeof arr);//sizeof也可以计算数组大小
    	printf("%d\n", sizeof(int [10]));//去掉数组名arr的东西是数组类型
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image.png

    要点三:sizeof内部的表达式不参与运算

    #include
    int main()
    {
    	short s = 5;
    	int a = 10;
    	printf("%d\n", sizeof(s = a + 2));
    	printf("%d\n", s);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    image.png

    为什么s = a + 2 没有参与运算呢?

    因为sizeof()在编译期间就完成计算了,而s = a + 2这句话如果要执行也得到运行期间执行

    image.png

    要点四:数组传参时sizeof数组名的含义

    #include
    void test(int arr[])
    {
    	printf("%d\n", sizeof(arr));
    	return;
    }
    int main()
    {
    	int arr[10] = { 0 };
    	test(arr);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image.png

    我在之前讲数组的文章中说过,数组名除了两个特殊情况下表示的是整个数组,其余情况下都是数组首元素的地址。所以当我们把数组名作为参数传递给函数时,本质上是传递了数组首元素的地址,函数用指针类型的形式参数接收,所以sizeof这个形式参数得到的是指针变量的大小,32位平台的结果是4,64位平台的结果是8

    要点五:~按位取反操作的是补码

    #include
    int main()
    {
    	int a = -1;
    	int b = ~a;
    	printf("%d", b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    image.png

    -1补码:

    11111111 11111111 11111111 11111111

    按位取反:

    00000000 00000000 00000000 00000000

    取反后原码:

    00000000 00000000 00000000 00000000

    结果是0

    要点六:前置++与后置++的区别(–同理)

    后置++:先使用,后++

    前置++:先++,后使用

    #include
    int main()
    {
    	int a = 10;
    	int b = a++;
    	printf("%d\n", a);
    	printf("%d\n", b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    a先使用(赋值给b),再++:

    image.png

    要点七:取地址操作符与解引用操作符

    #include
    int main()
    {
    	int a = 10;
    	printf("%p\n", &a);
    	int* pa = &a;
    	printf("%d\n", *pa);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    地址以32位/64位的二进制序列来标定内存空间,但是是以16进制的数字展示给编码者,所以用%p接收

    image.png

    要点八:强制类型转换
    通过(数据类型)可以进行将浮点型直接转换成整型的操作

    #include
    int main()
    {
    	int a = (int)3.14;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    关系操作符

    符号含义
    >判断左操作数是否大于右操作数 是结果为1 否结果为0
    >=判断左操作数是否大于等于右操作数 是结果为1 否结果为0
    <判断左操作数是否小于右操作数 是结果为1 否结果为0
    <=判断左操作数是否小于等于右操作数 是结果为1 否结果为0
    ==判断左操作数是否等于右操作数 是结果为1 否结果为0
    !=判断左操作数是否不等于于右操作数 是结果为1 否结果为0

    逻辑操作符

    符号名称含义
    &&逻辑与一假则假 全真才真
    ||逻辑或一真为真 全假才假

    要点一:&&操作符只要遇到0就会停止运算 结果为0

    要点二:||操作符只要遇到1就会停止运算 结果为1

    #include
    int main()
    {
    	int i = 0, a = 0, b = 2, c = 3, d = 4;
    	i = a++ && ++b && d++;
    	printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    image.png

    a为0,i必为0,后面的++bd++不再运算


    条件操作符

    符号含义
    a ? b : ca为真结果为b,a为假结果为c

    条件操作符又叫三目操作符,看下面的“比大小”算法对其的巧妙运用

    #include
    int main()
    {
    	printf("请输入三个数:\n");
    	int a, b, c;
    	scanf("%d%d%d", &a, &b, &c);
    	printf("三个数中最大数为:%d\n", a > b ? (a > c ? a : c) : (b > c ? b : c));
    	printf("三个数中最小数为:%d\n", a < b ? (a < c ? a : c) : (b < c ? b : c));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    image.png


    逗号表达式

    符号含义计算方法
    ,隔开的一串表达式从左向右依次计算每一个表达式,整个表达式的结果是最后一个表达式的值
    #include
    int main()
    {
    	int a = 3;
    	int b = 5;
    	int c = 0;
    	int d = (c = 5, a = a + c, b = a * b, c = c + b);
    	printf("%d\n", d);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    image.png


    下标引用、函数调用、访问结构体成员

    符号名称含义
    数组名[数组元素下标]下标引用操作符访问数组元素
    函数名实际参数函数调用操作符调用函数
    结构体名.结构体成员访问结构体成员操作符访问结构体成员
    结构体指针->结构体成员访问结构体成员操作符访问结构体成员

    这些符号的用法在数组、函数、结构体中分别有细致的介绍


    优先性与结合性

    优先性

    小学算术教会我们“先乘除,后加减”,这种不同运算符的运算优先等级就是操作符的优先性。
    不同操作符在同一表达式中,按照优先级顺序进行计算。

    结合性

    运算是从左往右计算,这种计算的方向性就是操作符的结合性。
    同一优先级的操作符在同一表达式中,按照结合性顺序进行计算

    具体顺序请直接查表

    C语言操作符优先性与结合性表格


    隐式类型转换

    学会了各种操作符的用法后,我们仍需知道;两个重要的“潜规则”

    整型提升与截断(重点)

    short a, b, c;
    	a = b + c;
    
    • 1
    • 2

    b和c的数据类型大小(short型2字节)不到一个普通整型的大小(4字节),所以b + c这个式子在运算时会先将b和c的值提升为普通整型,然后再进行加法运算。b和c进行提升后相加结果是4字节int类型,存入short类型的a中会“放不下”,所以会发生截断现象
    所以“潜规则一”就是精度不到整型的变量参与运算都会发生整型提升
    整型提升就是按照变量的符号位补相应的数,无符号类型统一补0

    “整型提升”和“截断”发生的过程是什么呢?请看下面的案例

    #include
    int main()
    {
    	char a = 3;
    	char b = 127;
    	char c = a + b;
    	printf("%d\n", c);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    image.png

    • 详解:
      3的二进制位:
      00000000 00000000 00000000 00000011
      但是a只能容纳8位,所以把前面的24位全部截断后再放入a中:
      a现在的补码为00000011
      127的二进制位:
      00000000 00000000 00000000 01111111
      b也只能容纳8位,所以把前面的24位全部截断后放入b中:
      b现在的补码为01111111
      a + b发生整型提升
      a提升后的补码:
      00000000 00000000 00000000 00000011
      b提升后的补码:
      00000000 00000000 00000000 01111111
      则a+b的补码:
      00000000 00000000 00000000 10000010
      然后存入char类型的c中,再次发生截断:
      c的补码为:
      10000010
      然后打印的时候是用%d类型接收的,所以c也要整型提升
      提升后c的补码为:
      11111111 11111111 11111111 10000010
      则c反码为:
      11111111 11111111 11111111 10000001
      则c原码为:
      10000000 00000000 00000000 01111110
      这个二进制数就是c的值
      则c的值为-126

    为什么会发生整型提升呢?

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


    算数转换

    如果同时参与运算的两个或以上个操作数的类型不同,首先得按照下面的顺序进行转换后再执行运算:

    intunsigned intlong intunsigned long intfloatdoublelong double

    其实就是计算机在进行不同精度的数据之间的计算时,默认的将低精度数据向精度更高的类型去转换的过程。


    问题表达式

    值得注意的是,就算我们已经掌握透彻操作符的使用场景、注意事项、优先性与结合性、隐式类型转换,但是仍然会有一些问题表达式的存在,它们的出现是C语言这个语言和编译语言的编译器的固有缺陷。不同的编译器的汇编语言中,执行语句的顺序有差别,这个差别导致了部分代码的移植性极差,也就是问题表达式的“问题”所在。我们在学习一门技术的时候要尊重它的优势,同时也要避免它的劣势,扬长避短,才能成为技术的主人。
    以下三种常见的问题表达式,放出来大家引以为戒。这种模棱两可的写法一定不要出现在我们的程序中

    问题表达式一:

    a*b + c*d + e*f

    三个乘法式子的优先级无法准确判断,不同编译器下的结果不同,也就是说同样的代码拷贝到别人的电脑中可能结果不一样,这是很可怕的。所以这个式子是问题表达式

    问题表达式二:

    c + --c

    查表可知,--+的优先级高,所以应该先算--c,后算c + --c,但是我们无法知道+左边的c是再--c之前准备好的,还是之后准备好的

    c=2的情况下,之前准备好的结果是3,之后准备好的结果是2,不同编译器下结果不同,此式也为问题表达式

    问题表达式三:

    #include
    int fun()
    {
    	static int count = 1;
    	return ++count;
    }
    int main()
    {
    	int a = 0;
    	a = fun() - fun() * fun();
    	printf("%d\n", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    image.png

    这个就有很明显的困惑了。三个fun函数不同的执行顺序都会导致不同的结果。如果先算后面的结果就是-2,先算前面的结果就是-10。
    所以启示就是:不要写运算顺序模棱两可的代码,这种代码看起来简洁,实际上健壮性和可读性都很差。


    至此文毕,与诸君共勉。

  • 相关阅读:
    python基础(五)----时间模块
    Keil4打开单片机工程一片空白,cpu100%程序卡死的问题解决
    持续部署的得力助手:探索LangChain支持的CD工具全景
    Nlp项目实战自定义模板框架
    基于GA遗传算法的异构网络垂直切换优化算法的matlab仿真
    微软正在研究使 Linux 脚本更安全
    MSTP&VRRP协议
    RibbitMQ学习笔记之MQ练习
    【svn使用教程】
    C#异步有多少种实现方式?
  • 原文地址:https://blog.csdn.net/qq_51379868/article/details/119185972