💻前言
🍁本篇汇总归纳各种操作符,对操作符的使用进行详细的分析,总结操作符的优先级和结合性。
🍁一些基础的操作符在我的另一篇博客中有详细介绍到,这些基础操作符我也会归纳到这里,相应位置会有另一篇博客的跳转链接 !
🍁我们编写代码时往往会将表达式应用到各种环境下,那么这些表达式在计算机当中是如运算的,在这里会介绍到。
💻一.操作符归纳
1.算数操作符
+ - * / %
这几个个操作符较为基础,
在入门篇c语言中有讲到C语言入门——带你从0开始
2.移位操作符
<< 左移操作符
>> 右移操作符注:移位操作符的操作数只能是整数,同时不可以移动负数位,这个是标准未定义的
移位操作符移动的是这个数在计算机中的二进制位,整数的二进制表示有三种,原码、反码和补码,计算机当中数据以补码的形式存储,正数的原码反码补码都相同,而负数的原反补需要通过计算得出,
这里的转换可以看另一篇博客数制间的转换,原反补!
所以移位操作符移动的是整数补码的二进制位
🌸<< 左移操作符
移位规则:
左边抛弃、右边补0
- 我们通过程序的打印结果可以发现,移位操作符对一个整数进行移位,得到一个新的值但其本身并不会发生改变。
- 其实这里我们多换几个数便可以得出一个规律,将一个数向左移动一位,得到的值是没有移动前的2倍。
🌸>> 右移操作符
移位规则:>
右移运算分两种:
- 逻辑移位
左边用0填充,右边丢弃- 算术移位
左边用原该值的符号位填充,右边丢弃
图中运行结果是在vs2119编译环境下得到的,所以在这个环境下,右移采用的是算数右移,其实在绝大多数编译环境下,右移都采用的是算数右移,这样就显的很合理,我们可以想一下,如果采用逻辑右移,由-1便会产生一个很大的正数,这样的运算是不是显得有些不合理呢。。。
3.位操作符
操作数必须是整数
&
按(2进制)位与 , 对应俩个二进制位都为1则为1,否则为0
|
按(2进制)位或 - 有1就为1,都为0才是0
^
按(2进制)位位异或 - 相同为0,相异为1
小技巧:
a ^ a = 0
0 ^ a = a
可以自己写出二进制序列看一下进行理解。
知道这个小技巧后下面这道变态得笔试题就没那么变态了!
不能创建临时变量(第三个变量),实现两个数的交换。
#include<stdio.h> //方法一: int main() { int a = 3; int b = 5; printf("交换前:a=%d b=%d\n", a, b); a = a ^ b;//a=3^5 b = a ^ b;//3^5^5 --> b=3 a = a ^ b;//3^5^3 --> a=5 printf("交换后:a=%d b=%d\n", a, b); return 0; } //方法二: int main() { int a = 3; int b = 5; //这种方法会有溢出的问题 printf("交换前:a=%d b=%d\n", a, b); a = a + b;//俩个很大得数相加可能会栈溢出 b = a - b; a = a - b; printf("交换后:a=%d b=%d\n", a, b); return 0; }
4.逻辑操作符
&& 逻辑与
|| 逻辑或区分逻辑与和按位与
区分逻辑或和按位或1 & 2----->0
1 && 2---->1
1 | 2----->3
1 || 2---->1
5. 赋值操作符
= 赋值操作符是一个很棒的操作符,他可以让你改到一个你之前不满意的值;也就是你可以给自己重新赋值。
int weight = 120;//体重 weight = 89;//不满意就赋值 double salary = 10000.0; salary = 20000.0;//使用赋值操作符赋值。 //赋值操作符可以连续使用,比如: int a = 10; int x = 0; int y = 20; a = x = y+1;//连续赋值 //这样的代码感觉怎么样? //那同样的语义,你看看: x = y+1; a = x; //这样的写法是不是更加清晰爽朗而且易于调试。
复合赋值符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
这些运算符都可以写成复合的效果。
比如:
int x = 10; x = x+10; x += 10;//复合赋值 //其他运算符一样的道理。这样写更加简洁。
6. 单目操作符
7. 关系操作符
注意区分==和=的使用的区别
8. 条件操作符
exp1 ? exp2 : exp3
点这里找到对应操作符部分
9. 逗号表达式
exp1, exp2, exp3, …expN
点这里找到对应操作符部分
10. 下标引用、函数调用和结构成员
操作数:一个数组名 + 一个索引值
int arr[10];//创建数组 arr[9] = 10;//实用下标引用操作符。 [ ]的两个操作数是arr和9。
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
11. 相关练习题
统计二进制中1的个数
/* 方法一: 思路: 我们想要得到一个十进制数的每一位时,对这个数进行%10运算便可; 对于二进制数同样适用,做法如下: 循环进行以下操作,直到n被缩减为0: 1. 用该数据模2,检测其是否能够被2整除 2. 可以:则该数据对应二进制比特位的最低位一定是0, 否则是1,如果是1给计数加1 3. 如果n不等于0时,继续1 */ int count_one_bit(int n) { int count = 0; while(n) { if(n%2==1) count++; n = n/2; } return count; } /* 上述方法缺陷:进行了大量的取模以及除法运算, 取模和除法运算的效率本来就比较低。 方法二思路: 一个int类型的数据,对应的二进制一共有32个比特位, 可以采用位运算的方式一位一位的检测,具体如下 */ int count_one_bit(unsigned int n) { int count = 0; int i = 0; for(i=0; i<32; i++) { if(((n>>i)&1) == 1) count++; } return count; } /* 方法二优点:用位操作代替取模和除法运算,效率稍微比较高 缺陷:不论是什么数据,循环都要执行32次 方法三: 思路:采用相邻的两个数据进行按位与运算 举例: 9999:10 0111 0000 1111 第一次循环:n=9999 n=n&(n-1)=9999&9998= 9998 第二次循环:n=9998 n=n&(n-1)=9998&9997= 9996 第三次循环:n=9996 n=n&(n-1)=9996&9995= 9992 第四次循环:n=9992 n=n&(n-1)=9992&9991= 9984 第五次循环:n=9984 n=n&(n-1)=9984&9983= 9728 第六次循环:n=9728 n=n&(n-1)=9728&9727= 9216 第七次循环:n=9216 n=n&(n-1)=9216&9215= 8192 第八次循环:n=8192 n=n&(n-1)=8192&8191= 0 可以观察下:此种方式,数据的二进制比特位中有几个1, 循环就循环几次,而且中间采用了位运算,处理起来比较高效 */ int count_one_bit(int n) { int count = 0; while(n) { n = n&(n-1); count++; } return count; } int main() { int n = 0; printf("输入一个整数:>"); scanf("%d", &n); int ret = count_one_bit(n); printf("二进制中 1 的个数:>%d\n", ret); return 0; }
求两个数二进制中不同位的个数
/* 方法一: 1. 先将m和n进行按位异或,此时m和n相同的二进制比特位清零, 不同的二进制比特位为1 2. 统计异或完成后结果的二进制比特位中有多少个1即可 */ #include <stdio.h> int calc_diff_bit(int m, int n) { int tmp = m^n; int count = 0; while(tmp) { tmp = tmp&(tmp-1); count++; } return count; } int main() { int m,n; while(scanf("%d %d", &m, &n) == 2) { printf("%d\n", calc_diff_bit(m, n)); } return 0; } /*方法二:*/ #include<stdio.h> int main() { int a = 0; int b = 0; int count = 0; printf("请输入俩个整数:>"); scanf("%d%d", &a, &b); int i = 0; for (i = 0; i < 32; i++) { if ((a >> i & 1) != (b >> i & 1)) { count++; } } printf("%d\n", count); return 0; }
打印整数二进制的奇数位和偶数位
/* 思路: 1. 提取所有的奇数位,如果该位是1,输出1,是0则输出0 2. 以同样的方式提取偶数位置 检测num中某一位是0还是1的方式: 1. 将num向右移动i位 2. 将移完位之后的结果与1按位与,如果: 结果是0,则第i个比特位是0 结果是非0,则第i个比特位是1 */ void Printbit(int num) { for(int i=31; i>=1; i-=2) { printf("%d ", (num>>i)&1); } printf("\n"); for(int i=30; i>=0; i-=2) { printf("%d ", (num>>i)&1); } printf("\n"); } int main() { int num = 0; printf("输入一个整数:>"); scanf("%d",&num); Printbit(num); return 0; }
💻二.表达式运算原理
1. 隐式类型转换
C的整型算术运算总是至少以缺省(默认)整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
整形提升是按照变量的数据类型的符号位来提升的
//负数的整形提升 char c1 = -1; 变量c1的二进制位(补码)中只有8个比特位: 1111111 因为 char 为有符号的 char 所以整形提升的时候,高位补充符号位,即为1 提升之后的结果是: 11111111111111111111111111111111 //正数的整形提升 char c2 = 1; 变量c2的二进制位(补码)中只有8个比特位: 00000001 因为 char 为有符号的 char 所以整形提升的时候,高位补充符号位,即为0 提升之后的结果是: 00000000000000000000000000000001 //无符号整形提升,高位补0 //int 4byte - 32bit //char 1byte- 8bit int main() { char a = 5; //00000000000000000000000000000101 //00000101 a char b = 126; //00000000000000000000000001111110 //01111110 b char c = a + b; //00000000000000000000000000000101 a运算时整形提升 //00000000000000000000000001111110 b运算时整形提升 //00000000000000000000000010000011 a+b //10000011 c //11111111111111111111111110000011 - 补码 //11111111111111111111111110000010 //10000000000000000000000001111101 -125 // printf("%d\n", c);//-125 return 0; } int main() { char a = 0xb6; short b = 0xb600; int c = 0xb6000000; if (a == 0xb6) printf("a"); /*a, b要进行整形提升, 但是c不需要整形提升 a, b整形提升之后, 变成了负数, 所以表达式 a == 0xb6, b == 0xb600 的结果是假, 但是c不发生整形提升, 则表达式 c == 0xb6000000 的结果是真. 所以程序输出的结果是: c */ if (b == 0xb600) printf("b"); if (c == 0xb6000000) printf("c"); return 0; } int main() { char c = 1; printf("%u\n", sizeof(c));//1 printf("%u\n", sizeof(+c));//4 printf("%u\n", sizeof(-c));//4 /* c只要参与表达式运算, 就会发生整形提升, 表达式 + c, 就会发生提升, 所以 sizeof(+c) 是4个字节. 表达式 - c 也会发生整形提升, 所以 sizeof(-c) 是4个字节, 但是 sizeof(c), 就是1个字节. */ return 0; }
2.算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行,下面的层次体系称为寻常算术转换。
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14; int num = f; //隐式转换,默认进行,会有精度丢失
3.操作符的属性
复杂表达式的求值有三个影响的因素。
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
绝大多数操作符是不控制求值顺序的,但碰到下面几个需要注意!
这里的逗号表达式和条件表达式的求值顺序好理解,我们重点理解&&和||构成的表达式!
4.规避问题表达式
有了上面的基础,我们可以总结表达式求值的顺序一部分是由操作符的优先级和结合性决定;同时,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
我们学习了这些有助于我们在代码中写出一个正确的表达式,了解了这些原理,可以让我们在写代码过程中避免很多的错误和问题,但掌握了这些并不意味着我们在使用表达时可以随心所欲!
比如下面这个表达式就存在一些问题!
//表达式的求值部分由操作符的优先级决定。 a*b + c*d + e*f
这个表达式在计算的时候,由于 * 比 + 的优先级高,只能保证,* 的计算是比+早,但是优先级并不能决定第三个 * 比第一个+早执行。
所以表达式的计算机顺序就可能是:
a * b
c * d
a * b + c * d
e * f
a * b + c * d + e * f
或者:
a * b
c * d
e * f
a * b + c * d
a * b + c * d + e * f
所以学习这些表达式的运算原理是为了让我们能够识别表达式是否存在问题,表达式的计算并不由我们想的所决定,我们需要去判我们写出来的表达式是否可以得出正确的结果,学习了这些让我们有能力去写出一个正确的代码,避免写出一些问题表达式!
小建议:写出来的表达式可以多使用()避免问题的出现,同时不建议写出过于复杂的表达式,一样的效果可以多用几层if等语句来实现!