目录
1. 计算机常用进制类别
2. 10进制转各种进制(取余倒置法)
3. 二进制转8进制、16进制
3.1 二进制转8进制
3.2 二进制转16进制
4. 原码、反码、补码
5. 整型提升 与 算术转换
5.1 整型提升
5.2 算术转换
6. 移位操作符
6.1 左移操作符( << )
6.2 右移操作符( >> )
6.3 左右移的算术式
7. 位运算符
1. 按位与( & )
2. 按位或 ( | )
3. 按位异或( ^ )
4. 按位取反( ~ )
* 练习
1. 计算机常用进制类别
在计算机领域中,最常见的几种进制类别分别是二进制、八进制、十进制、十六进制:
- 二进制:是一种只使用0和1两个数码来表示数值的进位计数制,它的基数为2,进位规则是逢二进一
- 八进制:是一种使用0、1、2、3、4、5、6、7共八个数码来表示数值的进位计数制,基数为8,运算规则为逢八进一。
- 十进制:是一种使用0至9共十个数码来表示数值的进位计数制,基数为10,运算规则为逢十进一。
- 十六进制:是一种使用0到9和A到F共16个字符来表示数值的进位计数制,基数为16,运算规则为逢十六进一。(其中a~f 按顺序来就是10~15,大小写意义不变)
8进制小知识:
一、在变量赋值或初始化时,如果第一个数字是0,后面没有数字大于等于8,系统默认你输入的是8进制的数值。
例如:
int main()
{
int a = 0;
a = 012; //此时的012等于八进制的12
printf("a的值为:%d", a);
return 0;
}
以10进制打印的结果为:
二、八进制的输入输出格式是%o;无论输入时的第一个数字是不是零,输入的结果都是8进制值。
int main()
{
int a, b;
scanf("%o %o", &a, &b); //以8进制的形式输入
printf("十进制下,a的值为%d,b的值为%d\n", a, b);//以10进制的形式输出
return 0;
}
可以看到,当输入形式是%o(八进制)时,输入12和012都是一样的。
16进制小知识:
一、在变量赋值或初始化时,如果前面含有0x(或者0X),后面没有数字大于 f 时,系统默认你输入的是16进制的数值。
例如:
int main()
{
int a = 0;
a = 0x12; //此时的0x12是十六进制的12
printf("a的值为:%d", a);
return 0;
}
二、十六进制的输入输出格式是%x(或者%X);无论输入时的前面有没有0x,输入的结果都是16进制值。
int main()
{
int a, b;
scanf("%x %X", &a, &b); //以16进制的形式输入
printf("十进制下,a的值为%d,b的值为%d\n", a, b);//以10进制的形式输出
return 0;
}
可以看到,当输入形式是%x或%X(十六进制)时,输入12、0x12 (或是0X12) 都是一样的。
2. 10进制转各种进制(取余倒置法)
补充:其实10进制对于我们人类来说,可以算是一种 “中心进制”。人类对于10进制最为敏感,因为10进制与人类有10跟手指密切相关。其他进制都是由10进制演化而来。
先思考一个问题:怎样获得十进制数的每一位数字?
假如现在有个十进制数123,先取余十得到个位数3;再对123除以十得到商12,再对12取余十得到十位数2;对12除以十得到商1,再对1取余十得到百位数1。
我们按顺序得到了个位3,十位2和百位1,只需要按得到的顺序倒过来排列,就得到了123。
对于其他进制也是这样的。比如2进制,先对123取余得到二进制下的第一位数字1,再除以2得到商61;再对61取余二得到二进制下的第二位数字1……以此类推,把所有的余数按倒置排列,就得到了十进制数123的二进制数
总结:10进制转各种进制的步骤
1. 对该数(或商)取余对应进制类别(如2,8,16)得到低位数。
2. 对该数(或商)除以对应进制类别(如2,8,16)得到新的商。
3. 依次重复步骤12,当新的商小于对应进制类别时停止。
4. 停止后,最后的商就是最高位数,按倒置排序就是对应的进制数。
3. 二进制转8进制、16进制
3.1 二进制转8进制
从2进制序列中右边低位开始向左,每3个二进制位会换算⼀个八进制位,剩余不够3个2进制位的直接换算。(每3个二进制位都是独立的,范围都是000 ~ 111,即0 ~ 7)
如:2进制的01101011,换成8进制:0153。
这样做是有一定原理的,我用一张演变图展示给大家看:
3.2 二进制转16进制
从2进制序列中右边低位开始向左,每4个2进制位会换算⼀个十六进制位,剩余不够4个⼆进制位的直接换算。
如:2进制数01101011,换成16进制数0x6b。(16进制表示的时候前⾯加0x)
这个原理跟2进制转8进制是一样的,这里不重复讲解。
4. 原码、反码、补码
- 无符号整数不存在符号位,因此无符号整数没有补码和反码
- 有符号整数的2进制表式⽅法有三种,即原码、反码和补码
- 有符号整数的三种表示⽅法均有符号位,最⾼位被当做符号位,剩余的都是数值位。
- 符号位都是⽤0表示“正”,⽤1表示“负”。
正整数的表示方法:
正整数的原码、反码、补码都相同,最高位是0,数值位上也都一样。
负整数的表示⽅法:
负整数的三种表式⽅法各不相同,但最高位都是1。
- 原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
- 反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
- 补码:反码+1就得到补码。
注意:计算机在存储正数和负数时,无论是原码,反码,还是补码,都是以补码的形式存储的。
以char型数据来举例:(char的大小是1个字节,等于8个比特位)
1. 原码:
- +3的原码:0000 0011
- -3 的原码:1000 0011
2. 反码:
- +3的反码:0000 0011
- -3 的反码:1111 1100
3. 补码:
- +3的补码:0000 0011
- -3 的补码:1111 1101
5. 整型提升 与 算术转换
5.1 整型提升
数据传输和处理的基本单位是字节(8位)。然而,为了提高数据处理效率,计算机通常以更高的粒度进行操作。
在32位处理器中,可以一次性处理4个字节;在在64位处理器中,可以一次性处理8个字节。
对于char型和short型(包括无符号的情况)来说,他们只有1个字节或2个字节,都小于4个字节(或8个字节)。所以在计算之前,它们会被转换为普通整型,这种转换称为整型提升。
整型提升的规则:
1.无符号整型:
- ⽆符号整数提升,⾼位补0。
2. 有符号整型:
- 正整数:高位补0。(正数的原、反、补都一样,正数对原码整型提升)
- 负整数:高位补1。(负数对补码整型提升)
详细举例:
//正数的整形提升
char c2 = 3;
变量c2的⼆进制位(补码)中只有8个⽐特位: 00000011
因为char为有符号整型,所以整形提升的时候,⾼位补充符号位“0”,提升之后的结果是: 00000000 00000000 00000000 00000011
//负数的整形提升
char c1 = -2;
变量c1的⼆进制位(补码)中只有8个⽐特位: 1111110
因为 char 为有符号整型,所以整形提升的时候,⾼位补充符号位“1”,提升之后的结果是: 11111111 11111111 11111111 11111110
数据截断:
两个整数相加时,对于比较小的整型(在32位中小于int的整型,在64位中小于long long的整型),如果最后的数值大于对应存储整型的范围,加法器会先计算出正确的数据,再对多出的字长进行数据截断。【截断的是高位2进制数据】
比如:
char a = -2, b = 3;
char c = a+b;
那么a+b的结果就是:
11111111 11111111 11111111 11111110 + 00000000 00000000 00000000 00000011
等于 10000000 00000000 00000000 00000001
因为char的大小是1个字节,所以截断高位地址的3个字节,剩下:
00000001
所以c的结果就是1。
其实从这里可以看出,符号位也是真实参与二进制运算的。数据截断后,符号位还剩0就是正数,符号位还剩1就是负数。
数据溢出:
两个整数相加时,对于最大的整型(32位中最大是int型,64位中最大是long long型),如果结果超过了能表示的最大值或最小值,加法器会在达到非符号最高位时停止计算,这意味着结果没算完就赋值给接收的变量了。
这是因为计算机中的算术逻辑单元(ALU)在执行加法操作时,会检查是否发生了溢出。一旦检测到溢出,它就会停止进一步的计算并产生一个溢出标志。
5.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除⾮其中⼀个操作数的转换为另⼀个操作数的类型,否则操作就⽆法进⾏。下⾯的层次体系称为算术转换。
(整型提升是针对相同类型的计算时,类型字长不够4或8个字节的情况)
如果某个操作数的类型在该列表中排名靠后,那么⾸先要转换为另外⼀个操作数的类型后执行运算:
1. long double
2. double
3. float
4. unsigned long int
5. long int
6. unsigned int
7. int(char和short会主动转化为int再计算)
[ 整型变成浮点型,其实算是一种隐式类型转换,也会存在类型截断;整型与浮点型的之间的转换还存在一种显式类型转换,详细请看《数学计算类操作符 和 算术类型转换》 ]
6. 移位操作符
注:移位操作符的操作数只能是整数。
6.1 左移操作符( << )
移位规则:
- 左边抛弃、右边补0。(有符号数 和 无符号数都一样)
注意:计算机存储数据用的是补码,无论是正数还是负数,都是对补码左移。
举例说明:
int main()
{
char num = -113;
char n = num << 2;
printf("num = %d\n", num);
printf("n = %d\n", n);
return 0;
}
结果:
num初始化为十进制数-113,而-113的二进制补码是:
1000 1111
左移两位后,结果为:
0011 1100(此时变成了正数,是+60的二进制数值)
所以n接收到的值是60。
[ 从这里也可以看出,“n = num << 2”执行后,num的值是不会被改变的 ]
6.2 右移操作符( >> )
移位规则:
右移运算分两种:
1. 逻辑右移:左边用0填充,右边丢弃。【针对无符号整数】
2. 算术右移:左边⽤原该值的符号位填充,右边丢弃。【针对有符号整数】
- 正数:左边用0填充,右边丢弃。
- 负数:左边用1填充,右边丢弃。
注意:计算机存储数据用的是补码,无论是正数还是负数,都是对补码右移。
逻辑右移的例子:
int main()
{
unsigned char num = 255;
unsigned char n = num >> 1;
printf("n= %d\n", n);
printf("num= %d\n", num);
return 0;
}
结果:
num初始化为十进制数255,而255的二进制数是:
1111 1111。
此时num是无符号整型,右移采用的是逻辑右移,左边补0。右移一位后的结果是:
0111 1111(也就是十进制数127)
[ 从这里也可以看出,“n = num >> 1”执行后,num的值也是不会被改变的 ]
算术右移的例子:
int main()
{
char num = -125;
char n = num >> 1;
printf("num = %d\n", num);
printf("n = %d\n", n);
return 0;
}
结果:
num初始化为十进制数-255,而-255的二进制补码是:
1000 0011 (也是 -3 的原码)
此时num是有符号数,采用算术右移,右移后对补码左边补1。右移一位的结果为:
1100 0001(这是 -63 的补码,也是 -65 的原码)
因为计算机以补码的形式存放,所以n接收的值是-63。
6.3 左右移的算术式
(1)正数 / 2 == 正数 >> 1
(2)正数 * 2 == 正数 << 1
注意:这个式子只适合正数,不适合负数。
类比理解:
在10进制中,假如有个数是123,如果我们对123除以10,则计算机的结果是12;如果123乘上10,则计算机的结果是1230。虽然没有针对10进制的左移右移操作符,但这里看起来是不是就像是把123左右移动?
没错,2进制也是如此。除以10就少一个十进制位,除以2就多一个二进制位;乘10就多一个十进制位,且该位是0,乘2就多一个二进制位,且改位也是0。
7. 位运算符
C语言中一共有4种位运算符,分别是按位与、按位或、按位异或、按位取反。
1. 按位与( & )
作用:比较两个数值中的每一个比特位,并返回比较后的二进制结果。
&的比较规则:
(1)当上下两个比特位都是1时,该二进制位的结果是1。(都是1,才是1)
(2)当上下两个比特位含有0时,该二进制位的结果是0。(存在0,就是0)
注意:比较的是两个数的补码。
代码举例:
int main()
{
char num1 = -3;
char num2 = 5;
printf("num1 & num2 = %d\n", num1 & num2);
return 0;
}
结果:
程序分析:
num1是-3,-3的补码是1111 1101;num2是5,5的补码是0000 0101
// 1111 1101
// 0000 0101
上下都是1可以得到一个1,上下有0会得到一个0,最终每个二进制的值是:
0000 0101(结果是5的二进制数)
所以num1 & num2的结果是5。
按位与& 和 逻辑与&& 的区别在于:
(1)比较的对象不同:逻辑与&&比较的是2个数;按位与&比较的是2个数中的每一个比特位上的值
(2)返回的结果不同:逻辑与&&返回的是10进制中的0和1;按位与&返回的是一个2进制数。
2. 按位或 ( | )
作用:比较两个数值中的每一个比特位,并返回比较后的二进制结果。
| 的比较规则:
(1)当上下两个比特位含有1时,该二进制位的结果是1。(存在1,就是1)
(2)当上下两个比特位都是0时,该二进制位的结果是0。(都是0,才是0)
注意:比较的是两个数的补码。
代码举例:
int main()
{
char num1 = -3;
char num2 = 5;
printf("num1 | num2 = %d\n", num1 | num2);
return 0;
}
程序分析:
num1是-3,-3的补码是1111 1101;num2是5,5的补码是0000 0101
// 1111 1101
// 0000 0101
上下都是0可以得到一个0,上下有1会得到一个1,最终每个二进制的值是:
1111 1101(结果是-3的补码)
所以num1 & num2的结果是-3。
按位或| 和 逻辑或|| 的区别在于:
(1)比较的对象不同:逻辑或|| 比较的是2个数;按位或 | 比较的是2个数中的每一个比特位上的值
(2)返回的结果不同:逻辑或|| 返回的是10进制中的0和1;按位或 | 返回的是一个2进制数。
3. 按位异或( ^ )
作用:比较两个数值中的每一个比特位,并返回比较后的二进制结果。
^ 的比较规则:
(1)当上下两个比特位同时含有1和0时,该二进制位的结果是1。(不同为1)
(2)当上下两个比特位都是0或者都是1时,该二进制位的结果是0。(相同为0)
注意:比较的是两个数的补码。
代码举例:
int main()
{
char num1 = -3;
char num2 = 5;
printf("num1 ^ num2 = %d\n", num1 ^ num2);
return 0;
}
程序分析:
num1是-3,-3的补码是1111 1101;num2是5,5的补码是0000 0101
// 1111 1101
// 0000 0101
上下相同为0,不同为1,最终每个二进制的值是:
1111 1000(结果是-8的补码)
所以num1 & num2的结果是-8。
按位异或的算术式:假设有一个变量a,则有:
(1)a ^ a == 0
(2)a ^ 0 == a
(其实无论是按位与、或、异或,他们都满足交换律和结合律,所以就有下面的推广)
假设有另一个变量b,则:
a ^ b ^ b == b ^ a ^ b == b ^ b ^ a == a
4. 按位取反( ~ )
作用:这也是一种二进制运算,它对一个数的每一位进行逻辑非操作。(0变1,1变0)
注意:是对补码取反,而且符号位也会取反。(所有的位运算,符号位也都会被操作)
代码举例:
int main()
{
printf("~3 = %d\n", ~3);
return 0;
}
程序分析:
3的二进制码是0000 0011。
对-3取反,每一个二进制位都会取反:
变成1111 1100(这是-4的补码)
* 练习
题目要求:
编写代码实现:求⼀个整数存储在内存中的⼆进制中1的个数。
思路1:
在前面讲取余倒置法的时候说过,要想把2进制转换为10进制,就需要重复“对商取余,除数求新商”。每求出一个余数,就多一个2进制位;如果余数是1,那么该二进制位上就是1。所以我们可以仿照这样,来求得⼀个整数存储在内存中的⼆进制中1的个数。
根据思路1,我们可以写成下面的代码:
方案1:
int main()
{
int num = 10;
int count = 0;//计数
while (num)
{
if (num % 2 == 1)//取余,余数是1说明该二进制位是1
count++;
num = num / 2; //除以进制2,求得新的商
}
printf("⼆进制中1的个数 = %d\n", count);
return 0;
}
当我们刚刚思考时没有注意到一个问题:计算机存储的是补码,方案1无法计算整数为负数的情况。
思路2:
我们刚刚学完位运算,能不能通过位运算来解决呢?估计敏感的同学以及想到了,任意一个数和1进行 按位与 运算,如果这个数的最低位是0,那么按位与运算的结果是0,否则就是1。我们再把这个数进行右移,比较它的下一位,这我们不就能实现了吗?
以数值6举例:(假设用变量a存储)
// 0000 0110 (第一次)
// 0000 0001
第一次a & 1的结果是0,计数器不用加1,我们右移继续比较:
// 0000 0011 (第2次)
// 0000 0001
第2次a & 1 的结果是1,计算机加加,以此类推……
我们重复比较完符号位才停止,总共32次(我们的变量是int型,共4个字节,32个比特位)。由此写成下面的代码:
方案2:
int main()
{
int num = -1;
int i = 0;
int count = 0;
for (i = 0; i < 32; i++)//4个字节,32个比特
{
if ((num >> 1) & 1) //其实也可以写成 num & (1 << i)
count++; //上面的右移与下面的左移,它们的相对变化是一致的
}
printf("该数字在二进制中1的个数 = %d\n", count);
return 0;
}
思路3:
为了优化固定次数,最终会写成这样:(这个思路难想)
方案3:
int main()
{
int num = 0;
scanf("%d", &num);
int count = 0;//计数
while (num)
{
count++;
num = num & (num - 1);
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
我的分享完毕,谢谢大家Thanks♪(・ω・)ノ