0基础C语言积跬步之操作符详解(上)
首先声明,此博客无照搬抄袭,融入个人理解,我所使用的编译器是VS2022
目录
1.操作符的分类
2.二进制和进制之间的转换
3.原码、反码、补码
4.移位操作符
5.位操作符:&、|、^、~
6.单目操作符
7.逗号表达式
一、操作符的分类
- 算术操作符:
+-*/% - 移位操作符:
<<>> - 位操作符:
&|^~ - 赋值操作符:=、+=、
-=、*=、/=、%=、<<=、>>=、&=、|=、^= - 单目操作符(一元操作符):
!、-、+、&、*、sizeof、++、--、~ - 关系操作符:
>、<、>=、<=、==、!= - 逻辑操作符:
&&|| - 条件操作符(三目运算符):
exp1 ? exp2 : exp3 - 逗号操作符 ,
- 下标引用操作符:
[] - 函数调用操作符:
() - 结构成员访问操作符: .
->
二、二进制和进制之间的转换
2.1 二进制转为十进制
二进制转化为十进制,是有权重存在的,从右往左,分别是,2⁰、2¹、2²、2³、2⁴、2⁵、2⁶、2⁷......
其它进制转化为十进制时,二进制转,权重就是二的n次方;八进制转,权重就是八的n次方;十六进制转,权重就是十六的n次方(n从0开始逐个递增)
比如我们计算1101:从右往左加以权重:1×2⁰ + 0×2¹ + 1×2² + 1×2³ = 1 + 0 + 4 + 8 = 13
再比如计算八进制555:从右往左加以权重:5×8⁰ + 5×8¹ + 5×8² = 5 + 40 + 320 = 365
所以当我们会了二进制转为十进制后,八和十六转十进制,也就会了
总结:其它进制转十进制时,乘权相加
2.2十进制转化为二进制
转二进制就模二取余,如下图计算方法,模二,一直模到商为0,将得出的余数从下往上列出,就是结果
总结:十进制转二进制就模二取余,转八进制就模八取余,转十六进制就模十六取余
2.3二进制转八进制
首先,每个二进制位只能存0或者1,而2*2*2等于8,也就是2³=8,所以三个二进制位就可以表达八种结果,分别是:000、001、010、011、100、101、110、111,也就对应着八进制的八个数字字符:0~7
所以三个二进制位,就可以表达所有的八进制的数字符号了
最后,我们在将二进制转为八进制位时,我们需要从右往左,三个三个分隔出来转,权重为2的n次方
例如二进制110101:分成110和101,分别乘权相加:110:1*2²+1*2¹+0*2º=4+2+0=6;101:1*2²+0*2¹+1*2º=4+0+1=5,所以此八进制结果是65
又例如二进制10110:分成010(最左边不足三位时高位补零)和 110 ,分别乘权相加:010:0*2²+1*2¹+0*2º=0+2+0=2;110:1*2²+1*2¹+0*2º=4+2+0=6,所以此八进制结果是26
2.4二进制转十六进制
知道了二进制转八进制的原理,那我们也就相当知道了二进制转十六进制的原理
因为2⁴=16,所以可知,四个二进制位就可以组合16种结果,刚好对应十六进制的16个数字符号:0~F
计算时,我们就需要,从右往左每四个四个分隔开来计算,权重为2的n次方
例如二进制11011010:分为1101和1010,分别乘权相加:1101:1*2³+1*2²+0*2¹+1*2º=13;1010:1*2³+0*2²+1*2¹+0*2º=10,所以此十六进制的结果是DA
当二进制分隔开最左边不够四位需高位补0,再进行转换,和上面二进制转八进制例子一样,在此不再演示
2.5八进制转二进制
八进制转化为二进制,一位八进制数转化二进制对应三位二进制数,也就是一个八进制数转换成二进制数会变成三个二进制数,可以将八进制的每个数看作十进制数,再利用十进制转二进制的方法,分别模二取余,模到0后,将余数从下往上排列,得到每一位数对应的二进制数(当然你也可以通过查表查询一位八进制位和三位二进制位的对应关系来得出),逐个排列好后,再逐个放入原先八进制时的字符位置就可以了
例如:八进制53转化成二进制
八进制 5 → 101 ;八进制 3 → 011
所以53(O) = 101 011(B)
2.6十六进制转二进制
十六进制转二进制,将十六进制的每一位,分别模二取余,结果将余数从下往上排列
其它的进制转换:
十六进制转八进制,可以先将十六进制转化为十进制(或者二进制),再将十进制(或者二进制)转化为八进制
八进制转十六进制,也可以先将八进制转化为十进制(或者二进制),再将十进制(或者二进制)转化为十六进制
到此,进制转换就讲完了
三、原码、反码、补码
整数在内存中的存储都是由二进制形式来存储的,而二进制的表现方式有三种,分别是原码、反码、补码。一个整型整数对应的内存为4个字节的大小,也就是32个bit位,可以存32的二进制数,其中最高位是符号位,'0'代表正数,'1'代表负数
二进制数中最高位是指最左边所存的数,最右边是最低位,也就是从左往右,分别是从高位到低位
- 原码:直接将整数按照正负数翻译成二进制得到的数
- 反码:原码符号位不变,其余数全部取反
- 补码:反码+1
不过注意:正数的正反补码都是相同的;负数的正反补码表示方式都不一样
计算机当中:整数的存储都是由二进制形式来存储,并且是以补码的形式存在,整数的运算和存储都是以二进制补码的形式进行的
四、移位操作符
注意!!!:移位操作符的操作数只能是整数
而且移动的位数只能是大于等于零的整数,不能是负数或者小数
4.1左移操作符<<(乘2)
移位规则:左边抛弃、右边补0
#include <stdio.h> int main() { int num = 10; int n = num << 1; printf("n= %d\n", n); return 0; }左移操作符演示:
可以看见,一共32个格子,整体向左移一位后,就把最左边多出来的数丢掉,符号位也丢,本来是00001010,左移后变成了00010100,右边补了一个0
运行截图:
可以看到num本来是10,变成了20,由此可以看出:左移操作符<<具有将整数乘2的效果
这是因为二进制转化为十进制是有权重的,从低位到高位分别是2⁰、2¹、2²、2³、2⁴、2⁵、2⁶、2⁷.....,例如上面10,二进制是00001010,为1*2³+1*2¹=8+2=10;我们向左移1位之后,变成了00010100,为(1*2³)*2+(1*2¹)*2=8*2+2*2=(8+2)*2,对比移位之前的,每个数都乘了2,根据结合律,那就是总体乘2
4.2右移操作符>>(除2)
右移操作符,移位规则分两种:
- 逻辑位移:左边补0,右边抛弃
- 算术位移:左边补符号位,右边抛弃
#include <stdio.h> int main() { int num = 10; printf("%d\n", num>>1); return 0; }运行截图:
一般情况下都是算数位移,如果是unsigned int,unsigned char类型的整数,就是逻辑位移
可以看到num本来是10,变成了5,由此可以看出:右移操作符>>具有除2的效果
五、位操作符:&、|、^、~
5.1位操作符详解
用于整数二进制补码的运算
& 按位与 | 按位或 ^ 按位异或 ~ 按位取反&:按位与操作符:两边都是1才为1,有0则为0
|:按位或操作符:有1则为1,两边都是0才为0
^:按位异或操作符:相同为0,相异为1
~:按位取反操作符:全部取反(包括符号位)
注:他们的操作数必须是整数。
#include <stdio.h> int main() { int num1 = -3; int num2 = 5; printf("%d\n", num1 & num2); printf("%d\n", num1 | num2); printf("%d\n", num1 ^ num2); printf("%d\n", ~0); return 0; }运行截图:
我们来分析一下为什么会有这串结果,我们把-3和5用八位数的二进制来做演示
- 5原码、反码、补码:00000101
- -3原码:10000011
- -3反码:11111100
- -3补码:11111101
- 0原码、反码、补码:00000000
-3&5计算方式如下:有0则为0,两边都是1才为1
结果:00000101换算成十进制为:5
-3|5计算方式如下:有1则为1,两边都是0才为0
结果:11111101换算成反码为:先取反:10000010,再加1:10000011,换算成十进制为-3
-3^5计算方式如下:相同为0,相异为1
结果:11111000换算成反码为:先取反:10000111,再加1:10001000,换算成十进制为-8
~0计算方式如下:包括符号位全部取反
结果为11111111换算成反码为:10000000,再加1:10000001,换算成十进制为-1
5.2位操作符编程题
5.2.1题一
(1)不能创建临时变量(第三个变量),实现两个整数的交换
#include <stdio.h> int main() { int a = 10, b = 20; printf("交换前a=%d b=%d\n", a, b); a(新a) = a(原a) ^ b(原b); //()内的内容为批注,请读者自行删除 b = a(新a) ^ b(原b); a = a(新a) ^ b(新b); printf("交换后a=%d b=%d\n", a, b); return 0; }运行截图:
首先我们要知道:一个数a如果异或自身结果会为0,如果异或0结果还是a
- a^a=0
- a^0=a
我们来分析这个代码,首先a=a^b没有问题,接着第二步b=a^b,我们已知a=a^b,把a代入第二步的式子,得b=a^b^b=a^(b^b)=a^0=a,所以第二步式子就能把a的值赋给了b,接着第三步a = a(新a) ^ b(新b),由于b已经改变,新b=a(原a),所以式子也可以写为a=a(新a)^a(原a),所以我们再次将第一步中的a(新a) = a(原a) ^ b(原b)式子代入第三步式子,得a=a(原a值)^b(原b)^a(原a值)=(a(原a值)^a(原a值))^b(原b)=b,所以第三步转化为a=b(原b),至此,完成了整数的交换
5.2.2题二
(2)编写代码实现:求⼀个整数存储在内存中的二进制中1的个数
方法一:
#include <stdio.h> int main() { int num = 10; int count = 0;//计数 while (num) { if (num % 2 == 1) count++; num = num / 2; } printf("%d\n", count); return 0; }这个方法看似没问题,逻辑也正确,算10的二进制中有几个1,count用于计数,通过while循环,如果num%2==1就代表,这个二进制的最右边那位数是1(我们通过联想上我上述写的十进制转二进制的计算过程来理解这句话),然后我们再用num=num/2,相当于>>右移操作符,把二进制数整体往右移,然后再重新进入循环,直到num==0,就说明里面的1,全部挪到了最右方挪完了,每判断一次,如果是二进制位数1,count就自增,最后打印count
但是这个代码有个致命的弱点,他只能判断正数,负数进来没法正确判断,我们来分析一下导致其输入负数会错误的地方,首先,假如输入的是负数,比如-5,那么if(if (num % 2 == 1))这句话会直接失效,因为-5%2,一个负数%2,结果必然是负数不会是1,从这就错了,再而,我们看循环条件num,是每次num=num/2来达到这个循环条件,但是我们发现:-5/2=-2.5,除法规则向0取整,为-2,num此时等于-2,然后-2/2=-1,可-1/2=-0.5,我们向0取整,结果是0,结束循环,循环都结束了count仍然是0
总结上述两点错误带来的后果:负数进去,if判断条件失效,程序结束,count不会自增,count永远为0,所以不管输入什么负数,结果都打印0
方法二:
#include <stdio.h> int Find1(int n) { int count = 0; for (int i = 0; i < 32; i++) { if (((n >> i)&1)==1) { count++; } } return count; } int main() { int n = -1; int ret = Find1(n); printf("%d\n", ret); return 0; }运行截图:
这个代码的原理是,已知整型数占4个字节,32个bit位,有32个二进制位,所以我们把整数n不断右移,右移32次,每次都&上1,如果结果为1,那就说明这个位置二进制数为1,最终用count计数并且打印出来整数在内存中用二进制形式存储的1的个数
方法三(最优方法):
#include <stdio.h> int main() { int num = -1; int count = 0; while (num) { count++; num = num & (num - 1); // 每次消去最右边的1 } printf("二进制中1的个数 = %d\n", count); return 0; }上面的方法2,执行了32次,把二进制的每一位都判断了一遍,而此代码,只判断了有1的部分,大大提升了程序执行的效率。num = num & (num - 1)的作用是:每次消去一个1,二进制减 1,会把最右边的 1 变成 0,把最右边1后面的 0 全变成 1,比如1011&1010=1010(把最右边的1变为0),把1011最右边的1消去了,然后1010&1001=1000(把最右边的1变为0,把最右边1后面的 0 变成了 1),把第二个1消去了,然后1000&0111=0000(把最右边的1变为0,把最右边1后面的 0 全变成 1),所有1消去,退出循环,此时count刚好计数到1的个数
所以我们使用num = num & (num - 1)这串代码,效率就会很高
5.2.3题三
(3)将二进制位的某一指定位置置为0或1
- 13的2进制序列: 00000000000000000000000000001101
- 将第5位置为1后:00000000000000000000000000011101
- 将第5位再置为0:00000000000000000000000000001101
#include <stdio.h> void Mod1(int* n,int k) { *n|= (1 << (k - 1)); //置1 } void Mod0(int* n, int k) { *n &= (~(1 << (k - 1))); //置0 } int main() { int n = 13; int k = 0; scanf("%d", &k); Mod1(&n,k); printf("第%d位改成1后的数:%d\n", k, n); Mod0(&n,k); printf("第%d位改成0后的数:%d\n", k, n); return 0; }运行截图:
关键操作:
- 置 1:
*n |= (1 << (k-1)) - 置 0:
*n &= ~(1 << (k-1))
六、单目操作符
&(取地址操作符):取出某个变量在内存中的地址
*(解引用操作符):搭配地址使用,通过地址找到该地址所指向的内存单元里所存放的数据
七、逗号表达式
逗号表达式,就是用逗号隔开的多个表达式
逗号表达式:从左到右依次计算,整个表达式的最终结果是最后一个表达式算出来的结果
逗号表达式: exp1,exp2,exp3,......expN
我们来看几串代码:
(1)代码一:
int a = 1; int b = 2; int c = (a>b, a=b+10, a, b=a+1);//逗号表达式 c是多少?我们来分析,从左到右计算,a>b也就是1>2,为假,表达式结果为0;接着a=b+10,b=2,所以a=12;第三个a=12;第四个表达式b=a+1,b=13,逗号表达式的结果是最后一个表达式算出来的结果,所以最终c等于13
(2)代码二:
if (a = b + 1, c = a / 2, d > 0)这个if语句中的判断条件用了一个逗号表达式,其中逗号表达式的执行流程是从左到右依次执行,a=b+1会执行,c=a/2会执行,但最终决定if判断条件的,就只有逗号表达式的最终结果:d>0来决定
(3)代码三:
// 第一步:先获取一个值 a = get_val(); // 第二步:对这个值做统计/记录 count_val(a); // 只要 a > 0 就进循环 while (a > 0) { //业务处理 //... // 再重新获取新值 a = get_val(); // 再统计新值 count_val(a); }其实就是我们先调用get_val()获取a的值,然后调用了一遍count_val(a),对a进行统计操作,其实这两个具体是什么操作不重要,重要的是我们在开头就调用了这两个函数,接着如果a>0,我们就进入循环,进行一些列代码处理后,又再此执行a = get_val(),count_val(a)这两个函数
这样写可以,但是代码就显得有些冗余了,因为这两个函数在循环外调用了一次,在循环内又继续调用,为了让代码更清晰简明,我们可以这样改进:
(4)代码四:
while (a = get_val(), count_val(a), a > 0) { //业务处理 }用逗号表达式来改进,这串代码的意思和代码三的意思一模一样,都会先执行a = get_val()和 count_val(a),然后最终这个逗号表达式的结果是a>0,也就是会最终还是由a>0这个表达式来决定是否进入循环,也就对应着代码三while循环的判断条件。
这样写代码不冗余,逻辑更集中,可读性更强