目录
前言
一、操作符的分类
二、⼆进制和进制转换
1. 二进制转10进制
2. 10进制转2进制数字
3. 2进制转8进制和16进制
3.1 2进制转8进制
3.2 二进制转16进制
三、原码、反码、补码
四、移位操作符
1. 左移操作符
2. 右移操作符
五、位操作符:&、|、^、~
1.按位与:
2.按位或:
3.异或
4.按位取反
5.趁热打铁
5.1⼀道变态的⾯试题:
5.2 练习一
方案一:
方案二
方案三
总结
前言
这期我们总结一下C语言操作符,有我们前面学过的,也有没学过的,但是后面我们也会详细讲解;
正文开始
一、操作符的分类
- 算术操作符: + 、- 、* 、/ 、%
- 移位操作符: << >>
- 位操作符: & | ^ `
- 赋值操作符: = 、+= 、 -= 、 *= 、 /= 、%= 、<<= 、>>= 、&= 、|= 、^=
- 单⽬操作符: !、++、--、&、*、+、-、~ 、sizeof、(类型)
- 关系操作符: > 、>= 、< 、<= 、 == 、 !=
- 逻辑操作符: && 、||
- 条件操作符: ? :
- 逗号表达式: ,
- 下标引⽤: []
- 函数调⽤: ()
- 结构成员访问: . 、->
上述的操作符,我们已经讲过算术操作符、赋值操作符、逻辑操作符、条件操作符和部分的单⽬操作符,今天继续介绍⼀部分,操作符中有⼀些操作符和⼆进制有关系,我们先铺垫⼀下⼆进制的和进制转换的知识。
二、⼆进制和进制转换
其实我们经常能听到2进制、8进制、10进制、16进制这样的讲法,那是什么意思呢?其实2进制、8进制、10进制、16进制是数值的不同表⽰形式⽽已。
比如:数值15的各种进制的表⽰形式:
15的2进制:1111
15的8进制:17
15的10进制:15
15的16进制:F
我们重点介绍⼀下⼆进制:
⾸先我们还是得从10进制讲起,其实10进制是我们⽣活中经常使⽤的,我们已经形成了很多尝试:
• 10进制中满10进1
• 10进制的数字每⼀位都是0~9的数字组成
其实⼆进制也是⼀样的
• 2进制中满2进1
• 2进制的数字每⼀位都是0~1的数字组成
那么 1101 就是⼆进制的数字了。
1. 二进制转10进制
其实10进制的123表⽰的值是⼀百⼆⼗三,为什么是这个值呢?其实10进制的每⼀位是权重的,10进制的数字从右向左是个位、⼗位、百位....,分别每⼀位的权重是10零次方 , 10一次方 , 10二次方 ...
如下图:
2进制和10进制是类似的,只不过2进制的每⼀位的权重,从右向左如下图所示:
如果是2进制的1101,该怎么理解呢?
2. 10进制转2进制数字
3. 2进制转8进制和16进制
3.1 2进制转8进制
8进制的数字每⼀位是0~7的,0~7的数字,各⾃写成2进制,最多有3个2进制位就⾜够了,⽐如7的⼆进制是111,所以在2进制转8进制数的时候,从2进制序列中右边低位开始向左每3个2进制位会换算⼀个8进制位,剩余不够3个2进制位的直接换算。
如:2进制的01101011,换成8进制:0153,0开头的数字,会被当做8进制。
int main()
{
printf("%d\n", 123);
printf("%d\n", 0123); //1 * 8^2 + 2 * 8^1 + 3 * 8^0 = 83;
return 0;
}
运行结果:
3.2 二进制转16进制
16进制的数字每⼀位是0~9,a ~f 的,0~9,a ~f的数字,各⾃写成2进制,最多有4个2进制位就⾜够了,比如 f 的⼆进制是1111,所以在2进制转16进制数的时候,从2进制序列中右边低位开始向左每4个2进制位会换算⼀个16进制位,剩余不够4个⼆进制位的直接换算。
进制的01101011,换成16进制:0x6b,16进制表⽰的时候前⾯加0x
int main()
{
printf("%d\n", 345);
printf("%d\n", 0x345); //3 * 16^2 + 4 * 16^1 + 5 * 16^0 = 837
return 0;
}
运行结果:
三、原码、反码、补码
整数的2进制表⽰⽅法有三种,即原码、反码和补码
有符号整数的三种表⽰⽅法均有符号位和数值位两部分,2进制序列中,最⾼位的1位是被当做符号
位,剩余的都是数值位。
符号位都是⽤0表⽰“正”,⽤1表⽰“负”。
正整数的原、反、补码都相同。
负整数的三种表⽰⽅法各不相同。
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码。
反码得到原码也是可以使⽤:取反,+1的操作
int main()
{
int a = -10;
//-10是存在a中,a是整形变量,是4字节, 32bit
//10000000000000000000000000001010 - 原码
//11111111111111111111111111110101 = 反码
//11111111111111111111111111110111 - 补码 反码+1就得到补码。
int b = 10;
//10000000000000000000000000001010 - 原码
//10000000000000000000000000001010 - 反码
//10000000000000000000000000001010 - 补码
return 0;
}
对于整形来说:数据存放内存中其实存放的是补码。
为什么呢?
在计算机系统中,数值⼀律⽤补码来表⽰和存储。原因在于,使⽤补码,可以将符号位和数值域统⼀处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
//1-1
//1+(-1)
//原码计算:
//00000000000000000000000000000001
//10000000000000000000000000000001
//10000000000000000000000000000010 结果不对
//用补码计算:
//00000000000000000000000000000001 1的补码
//11111111111111111111111111111110 -1的反码
//11111111111111111111111111111111 -1的补码
//00000000000000000000000000000000 最高位舍去(结果)
四、移位操作符
<< 左移操作符
>> 右移操作符
1. 左移操作符
移位规则:左边抛弃、右边补0
#include <stdio.h>
int main()
{
int num = 10;
int n = num<<1;
printf("n= %d\n", n);
printf("num= %d\n", num);
return 0;
}
注意我们左移的是数的补码,不是原码,上面由于是正数,正数的原码和补码一样,如果我们换成负数的例子:
int main()
{
int a = -6;
//10000000000000000000000000000110 原码
//11111111111111111111111111111001 反码
//11111111111111111111111111111010 补码
int b = (a << 1);
//11111111111111111111111111110100 左移后的补码
//10000000000000000000000000001011 取反
//10000000000000000000000000001100 加一(得到左移后的原码)-12
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
运行结果:
2. 右移操作符
移位规则:⾸先右移运算分两种:
1. 逻辑右移:左边⽤0填充,右边丢弃
2. 算术右移:左边⽤原该值的符号位填充,右边丢弃
右移到底是算术右移,还是逻辑右移是取决于编译器的实现,常见的是编译器都是算术右移(我使用的VS2022是算术右移)
int main()
{
int num = -1;
//10000000000000000000000000000001
//11111111111111111111111111111110
//11111111111111111111111111111111
int b = num >> 1;
//11111111111111111111111111111111 算术右移不变
printf("%d\n", b);
printf("%d\n", num);
return 0;
}
运行结果:
算术右移示意图:
因为小编使用的是VS2022是使用的算术右移,所以无法演示逻辑右移,在这里我们也画一下示意图:
警告⚠️:对于移位运算符,不要移动负数位,这个是标准未定义的。
例如:
int num = 10;
num>>-1;//error
其实这个操作符也可想+-一样赋值简写:
a >>= 1;
a = a >> 1; //两者表达意思一致
五、位操作符:&、|、^、~
位操作符有:
& //按位与
| //按位或
^ //按位异或
~ //按位取反
注:他们的操作数必须是整数。
我们看到这个是不是有点熟悉呢:
&(按位与)-->&&(逻辑与)
|(按位或)--> ||(逻辑或)
按位与,按(2进制):
话不多说,直接上代码:
1.按位与:
int main()
{
int a = 3;
int b = -5;
int c = a & b;
//00000000000000000000000000000011 3的补码
//10000000000000000000000000000101 -5的补码
//11111111111111111111111111111010 -5的反码
//11111111111111111111111111111011 -5的补码
// 将两个数的补码按位与:
//00000000000000000000000000000011 3的补码
//11111111111111111111111111111011 -5的补码
//00000000000000000000000000000011 3&-5(补码)
//00000000000000000000000000000011 正数原反补码相同
printf("%d\n", c); //3
return 0;
}
运行结果:
2.按位或:
int main()
{
int a = 3;
int b = -5;
int c = a | b;
//将两个数的补码按位或:
//00000000000000000000000000000011 3的补码
//11111111111111111111111111111011 -5的补码
//11111111111111111111111111111011 3 | -5(补)
//10000000000000000000000000000101 原码(-5)
printf("%d\n", c);
return 0;
}
运行结果:
3.异或
异或规则:相同为0,不同为1
//异或
//异或规则:相同为0,不同为1
int main()
{
int a = 3;
int b = -5;
int c = a ^ b;
//将两个数的补码按位异或:
//00000000000000000000000000000011 3的补码
//11111111111111111111111111111011 -5的补码
//11111111111111111111111111111000 3 ^ -5(补)
//10000000000000000000000000001000 原码(-8)
printf("%d\n", c);
return 0;
}
运行结果:
4.按位取反
我们是不是想起了“!”,我们来想想他两个的区别是:
//按位取反~
int main()
{
int a = 0;
int b = ~a;
//00000000000000000000000000000000 0的原码也就是补码
//11111111111111111111111111111111 补码
//10000000000000000000000000000001 ~a的原码(-1)
printf("%d\n", b);
return 0;
}
运行结果:
5.趁热打铁
5.1⼀道变态的⾯试题:
不能创建临时变量(第三个变量),实现两个数的交换。
其实这个问题解决方法不止一种:
方案一:
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;
}
运行结果:
这个方案看似已经解决了我们的需求,但是存在一个潜在的问题:
如果a,b很大,但没超过整型,a+b超过整型的最大值,所以这种方法不可取;
我们来看另一种解决方法;
方案二:
我们直接来看代码:
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;
}
我们先来看看结果和我们需要的一不一样:
结果如我们所见,和我们要求一样,但是why?
在讲这个问题前我们先来将问题具象化:
假设 a = 3,也就是0011;那么a^a是什么?
不难想出,都一样那么结果就是0;
那a^0等于多少?
0011
0000 -->0011(结果不变还是3)
好,上面的两个小问题想明白了,我们接着看这个代码:
a = a ^ b;-----------1
b = a ^ b;
a = a ^ b;
第二步的b = a ^ b中的a已经经过第一步的赋值变成了a ^ b的结果,代入1式子,也就是b = a ^ b ^ b,刚刚在上面讲过了,两个相同的数异或等于0;而一个数与0异或等与本身;
所以也就是 b = a;------------------------------------2
此时已经将a赋值给b了,接着执行第三步,a = a^b,代入1,2,也就是a = a ^ b ^ a;同理也就是a = b;也就完成了交换;
因为异或不存在有进位的运算,所以也就不可能存在溢出,所以这个程序也就完美的规避了方案一的缺陷;
5.2 练习一
编写代码实现:求⼀个整数存储在内存中的⼆进制中1的个数
这个问题也可用多种方法解决,我们来分析一下各类方法的优缺点:
方案一:
我们想要二进制中的1的个数,我们可以利用获取最后的一位数,用if判断是否为1,如果为1,那就加一,再将最后一位去除,形成一个循环:
代码展示:
int main()
{
int num = 0;
scanf("%d", &num);
int count = 0;
while (num)
{
if (num % 2 == 1)
count++;
num /= 2;
}
printf("num的二进制中1的个数是%d\n", count);
return 0;
}
我们运行一下看看结果:
但是这个程序有个漏洞,它不能统计负数中的1的个数,比如:
输入-1
-1%2 !=0,-1/2 = 0;
这就无法得出正确答案
其实我们也可以在这个代码上直接进行补救:
我们将输入的数直接看成无符号数:
int main()
{
unsigned int num = 0;
scanf("%d", &num);
int count = 0;
while (num)
{
if (num % 2 == 1)
count++;
num /= 2;
}
printf("num的二进制中1的个数是%d\n", count);
return 0;
}
也可得出正确答案:
方案二
我们还可以利用上面讲的按位与和右移操作符进行确定最低位和往高位推进:
代码如下:
int main()
{
int n = 0;
int count = 0;
scanf("%d", &n);
int i = 0;
for (i = 0; i < 32; i++)
{
if ((n >> i) & 1 == 1)
count++;
}
printf("%d\n", count);
return 0;
}
这个思路就不会因为你输入的是负数而出错:
方案三
其实还有一种不容易想到的算法:
我们利用这种算法来对1进行计数,去除一个1就记一次数:
int main()
{
int n = 0;
scanf("%d", &n);
int count = 0;
while (n)
{
n = n & (n - 1);
count++;
}
printf("%d\n", count);
return 0;
}
运行结果:
5.3 练习三
我们前面学了很多符号,其实每种符号都有用:
例:⼆进制位置0或者置1
编写代码将13⼆进制序列的第5位修改为1,然后再改回0
13的2进制序列: 00000000000000000000000000001101
将第5位置为1后:00000000000000000000000000011101
将第5位再置为0:00000000000000000000000000001101
int main()
{
int n = 13;
//把第5为改为1
n |= (1 << 4);
printf("%d\n", n);
//把第5位改成0
n &= (~(1 << 4));
printf("%d\n", n);
return 0;
}
运行结果
一个问题:如何判断一个数是否是2的n次方
利用上面的知识想想,why,不会私信,评论都ok
总结
我们在这期主要总结了一下C语言中的操作符,但是由于篇幅的问题,还没有总结完成,下期见!26考研加油!