进制数基础知识 与 位运算(基础版)

目录

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. 有符号整型:

  1. 正整数:高位补0。(正数的原、反、补都一样,正数对原码整型提升)
  2. 负整数:高位补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♪(・ω・)ノ

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值