进制与位操作符详解

①  预备知识:原码,反码,补码

(1) 进制及其转化:

要搞清楚原码是啥,先得弄清楚二进制这个玩意,那二进制又是啥,不要慌张,不要着急,且听我细细道来--------

当前,我们用来表示数据的形式主要有:十进制,八进制,二进制,十六进制,也就是说这些进制只是一个数字的不同表示形式而已,下面我将一一解读这些进制------

1.十进制

啥叫十进制? 对于不了解进制的同学,还以为这个玩意很陌生呢,其实,我们生活的世界就是一个十进制的世界呀! 比如说,你到超市去买东西,人家商品上标的价钱就是用十进制表示的,比如吧,你买个好一点电动牙刷,假如说是128 元(这里只是举个例子哈,小伙伴们可不敢上纲上线喔),我们一般把他读作一百二十八 那么怎样得知他是128 呢,这时候有的小伙伴就说了,直接看的呗,这还用问嘛? 但是128 其实是这么来的: 从个位开始,依次乘以10^0  10^1  10^2  然后相加,也就是8×10^0+2×10^1+1×10^2==128  这就是十进制128的由来,小伙伴们有没有发现十进制都是由0~9的数字组成的呀.

2.二进制

那啥叫二进制? 二进制是由 0 和 1 组成的,同十进制一样,二进制表示的数也是需要从右往左依次乘以10^0  10^1  10^2 .....  (从10^0开始乘,随着位数的增加,幂指数依次增加,最后把每一位相乘的结果加起来,就把二进制的数转化为了十进制的数(也就是我们日常生活中所见到的数字形式),

譬如: 0b 1010 (这里补充一下,0b 为前缀,表示是二进制的数, 0 为前缀表示是八进制数, 0x 为前缀表示的是十六进制数,十进制无前缀),它所表示的十进制数是  0×2^0+1×2^1+0×2^2+1×2^3==2+8==10  也就是说二进制的1010 表示十进制的10  另外你看,十进制是不是满10进1 ,那二进制就是满2进1 例如:(0b) 1010  + (0b) 1010 ==  (0b) 10100 == 20 各位小伙伴如果不清楚的话,我再列个竖式大家看一下:

3. 八进制

有了前面的理解,想必你已经知道八进制是由哪些数字组成的吧? 不错,八进制就是由 0~7 的数字组成的

例如 0173 == 3×8^0+7×8^1+1×8^3 == 123  那我们再算一下,0173+0173等于多少? 如果各位小伙伴们不太清楚,我就再列个竖式给大家看看:

这时候有的小伙伴们就问了,为啥那个中间的7+7 的下面写一个6 ,请小伙伴们对十进制数字的运算做一个回忆,在十进制中, 我们是满十进一的,

例如:128+24  这时候如果按照正常的竖式进行计算,首先得算8+4==12,这时候我们只会在该位所对应的下面写一个2,想一想为啥? 原本我的8+4等于12呀,咋就只写一个2就完了呢? 原因是:我是不是进了个1出去给下一位了呀?  所以从我此时个位的视角来看,原本是12,我进了个1给下一位,就相当于给自己减去了一个10对不对,这才是个位写2的真正原因:某一位给左边的位进一个1,就相当于给自己这一位原本的结果减去一个10,大家想一想是不是这样的.

如果能理解这个的话,那么 为啥那个中间的7+7 的下面写一个6 也就能理解了,原因是7+7 ==14  而在八进制中满八进一,所以14往它左边一位进了个1 就相当于给自己减了个8 ,那余下的就是6写在下面了,不知道各位小伙伴能否理解~

然后再照葫芦画瓢,把相加后的八进制数依次乘以每一位的权重后相加就转化成十进制的 246 了.

4.十六进制

没错,十六进制是由 0~15 的数字组成的, 与其他进制不同的是:在16进制里,10~15分别是由 a b c d e f (大小写均可) 来表示的,有的小伙伴就问了,为啥要这样搞

好的,我们现在假设不这样规定,假设10~15 就用普通的十进制数字表示,那么我问你哦, 我现在要你用上面介绍的方式来把 0x1015 这个数换成十进制的表示形式,有的小伙伴就说了,这还不简单? 这不就是 5×16^0+1×16^1+0×16^2+1×16^3== 4117 嘛,这还用问嘛? 可问题是,我写的是 

1  0  1  5  单独的四个数字还是只有10 和 15 两个数字? 如果只有 10 和 15 那这个就是 15×16^0+10×16^1== 175  那这两个结果到底谁对谁错? 这是不是就产生歧义了呀?那引入字母之后,如果我想表达的是只有 10 和 15  这两个的话, 就可以直接写成 0xAF 这样是不是就可以有效的避免歧义,而我的 0x1015 就很自然的代表了十进制中的 4117 对不?  而对于十六进制来说,满十六进一,借一当十六的运算法则,这里就不再给小伙伴们列竖式了,有兴趣的小伙伴可以自己出两个十六进制的数算一下,然后用电脑上面计算器的程序员模式验证一下,我这里就偷个懒,直接进入下一部分啦!

⑵ 进入正题,原码,反码,补码解析

I 原码

我们知道一个整形在计算机中占4个字节(Byte),也就是32个比特位(bit),而每个二进制位占一个比特位,也就是说,一个整数在计算机中是以32个二进制位的方式进行存储的.

例如,对于10来说,其二进制表示形式是1010  ,由于它在计算机中需要32个bit位的空间,高位全部由0补充,也就是00000000000000000000000000001010  这就是10 的原码,那-10的原码呢? 对于-10来说,只需要将最高位的 0 换成 1 就好  也就是 10000000000000000000000000001010  

因此对于原码来说,负数原码的最高位为1,正数原码的最高位为0  这两个位的数字代表着符号,并不直接参与进制转化,那么怎样把十进制数写成二进制进而写出它的原码呢? 首先,要判断要表示的数是正数还是负数,是正数最高位首先写个 0 , 是负数最高位首先写个 1  再把这个数转成十进制,再把其转成二进制,转成十进制的方法前面已经教过了,就是每一位乘以所对应的权重相加,在把这个十进制转成二进制,方法就是一直用这个数%2,然后把这个余数记录下来,然后把这个数除二之后的数赋给这个数,直到这个数为 0 为止. 下面是步骤示意图:

翻译成代码就是现在下面这样的:

// 整数n的二进制序列获取方式

while (n)
{
	int m = n % 2;
	n /= 2;
}

这里的m就产生了n的二进制序列中从低位到高位的每一个数,接下来我就来实践一下: 

// 打印 n 的二进制序列

void Print_Binary(int n)
{
	while (n)
	{
		int m = n % 2;
		printf("%d", m);
		n /= 2;
	}
}


int main()
{
	int n = 0;
	scanf("%d", &n);
	Print_Binary(n);
	return 0;
}

大家看这样是不是就打印出了5的二进制序列,只不过它刚刚巧合正反都一样,按道理讲它是把101 倒过来打印了,结合我上面的步骤图,你看明白了嘛,再结合5 是正数,最高位为0 其他位用0 补充 ,所以  5 的原码就是 00000000000000000000000000000101  你看懂了嘛

说了正数负数,再来说说无符号整数,无符号整数顾名思义没有符号位,所有的位都要参与进制转化,例如 10000000000000000000000000000101

按照原码解析原本是-5 如果把它当一个无符号整数,它就是: 1×2^31+1×2^0+1×2^2  这就变成了一个非常大的正数.

II 反码

说完了原码,再来说说反码,反码就是原码的符号位(有符号整数的最高位)不变,其他位按位取反.正数的原码,反码,以及补码都相同,因此符号位不变,其他位按位取反这个运算规则只适用于负数

||| 补码

重头戏便是我们的补码,计算机内储存的是补码,原码只是为了方便人为查看和取出数据,补码是反码+1得来的,你可以发现,原码到补码只需要

取反+1即可,同样,补码到原码也可以取反+1 这个运算法则同样只适用于负数, 正数的原反补码相同,因此,在未来,算出一个补码的最高位为0时,就直接按照整数原码解析,如果最高位为1 则需要取反+1 算出原码,按照负数读出.

② & 操作符

& 叫做按位与, 既然它作为操作符,就必然有它所对应的操作数,而它的操作数就是计算机中的补码,它的运算规则是:有 0 便是 0 ,全为 1 才是 1 

来看下面一段代码:

不知道各位小伙伴们看懂了我的注释没有? 这就是&操作符的运算逻辑,我们来验证一下:

不知道小伙伴们能否理解~

③ | 操作符

介绍完了 & 操作符, 再来说说 | 操作符吧 ! 

| 叫按位或 , 其运算规则是 有 1 为 1 ,全 0 为 0   它的操作数同样是计算机中的补码,下面来看一个例子:

最终结果:-7

不知道小伙伴们看懂了没有~

④  ^ 操作符

说完了按位或,再来说说^吧!

^ 叫按位异或,其运算规则是相同为0  相异为1 

话不多说,我们直接上代码:

那结果是不是 -15 呢,我们来验证一下:

⑤ << 操作符

再来一个, 这个<< 叫左移操作符,它的操作符不再是两个补码,而是一个补码,它的运算规则是左边丢弃,右边补零.

啥意思呢,我们还是直接上代码:

不知道小伙伴们看懂了没~

⑥ >> 操作符

>> 操作符分为两种:逻辑右移和算术右移

算术右移:左边补符号位(正数补0  负数补1)  右边丢弃

逻辑右移: 左边无脑补0  右边丢弃

(注:在大多数编译器下, >> 都是算术右移,只有少数的编译器是逻辑右移)

不知道各位小伙伴看懂了我的注释没~

这也说明了 vs 编译器的 >> 是算术右移. 而不是逻辑右移

⑦  经典例题

求⼀个整数补码中1的个数

思路1: 假设它是一个正数,求补码就是求原码,先把它按原码的二进制写出来,其余高位直接补零. 但是这样的话,那负数咋个办

在这里,我们 首先要理解一点 负数的补码 == 该负数所对应的无符号数的原码

为啥? 我们来看下面一串代码:

#include <stdio.h>

int main()
{
    int a = -1;
    printf("%zd\n",a);
    return 0;
}

看到这串代码,让我们先理解理解它是什么意思,它是不是在说把-1当成一个 unsigned int (无符号整数)打印出来,也就是打印出-1所对应的无符号整数对吧? 这如果是个笔试题,你会怎么写,显然,对于整数来说,不管要打印的是什么,我们都得先把待打印数字的补码写出来,因为计算机中所有的整数以及对整数的所有操作都是在补码上做文章,那么在这里,我也就不跟大家啰嗦了,-1的补码是11111111111111111111111111111111  全 1  (这个要作为结论记住),现在%zd 的意思就是把这一串补码当无符号数打印出来,既然是无符号数,每一位都是有效位,也就是 

2^0+2^1+2^2+2^3+2^4+....+2^31 == 2^32-1 == 4294967294 (等比数列前n项和)  你再回过头来想一想,这不就是把全 1  当成原码在打印嘛!

理解了这一点,下面代码就好写了:

#include <stdio.h>

int counts(unsigned int n)
{
	int count = 0;
	while (n)
	{
		int m = n % 2;
		if (m == 1)
			count++;
		n /= 2;
	}
	return count;
}

int main()
{
	int n = -1;
	int ret = counts(n);
    printf("%d\n",ret);
	return 0;
}

其中你看,counts 函数的参数我变成了 unsigned int  这样传参过去就自动完成了强制类型转换,函数里面就直接按照上面十进制转二进制的方法进行书写从而把n 所对应的原码的有效位都遍历一遍,如果有 1  ,count 就自增 1 最后把count 返回给 ret 再把 ret 打印出来不就行了

思路2: 有没有发现思路1有点难以理解,理解的难点在于涉及到一个补码到原码的转化,那我们再来一个思路,这个思路的好处在于:不管正数负数,直接用其补码进行操作,不涉及转化

在讲这个思路之前,我需要小伙伴们想一个问题:就是 任意一个数 & 1  等于多少,结合&的运算法则好好想一想.

我相信聪明的你一下就想到了,是不是有两个结果:0 或者 1 呀? 这时候有的小伙伴就想考验我了,那你说为啥是这两个结果

首先我们要想清楚, 1 为正数 其补码与原码相同  00000000000000000000000000000001

                                                                         &

而任意一个数的补码也是 32 位的                        ...............................................................

由于 & 的 运算规则是 有 0 为 0, 全 1 为 1,所以这个数前面的31位都被 0 给与掉变为 0 了,  所以结果取决于它的最低位,如果它的最低位是0

那么 & 出来的二进制序列显然是:00000000000000000000000000000000  这显然是 0 

如果它的最低位是 1 那么结果是:00000000000000000000000000000001  这显然是  1  

综上所述,给一个数 & 1  能且只能判断出它的最低位是啥. 顺着这个思路想下去,如果我待检测的数为 n 

那我是不是要让 n 的每一位都来到它的最低位并 & 1

 这样是不是就相当于把它的每一位都进行了判断,然后定义上一个计数器,看这之间有没有结果为1 ,如果有,我的计数器就 ++  最后把我的计数值返回并打印是不是就行了,那这是不是需要一个循环呀,来看下面的代码:

int counts(int n)
{
	int count = 0;
	for (int i = 0; i < 32; i++)
	{
		if ((n >> i) & 1 == 1)
			count++;
	}
	return count;
}


int main()
{
	int n = 0;
	scanf("%d", &n);
	return 0;
}

不知道小伙伴们结合思路2能不能理解我上面的代码~

思路3: n&(n-1)

在这里,我需要小伙伴们明白一个事情,n&(n-1) 所得到的结果,恰好就把n最右边的1给去掉了,不信的话,小伙伴们自己可以试验一下,这个恕我能力有限,只能意会不能言传,所以 在n为非0的情况下,我让 n = n & (n-1),看这个代码执行几次,就去掉了几个1,也就是原本有几个1,不知道这个思路小伙伴们能否理解~

int Num_one(int n)
{
	int count = 0;
	while (n)
	{
		n &= (n - 1);
		count++;
	}
    return count;
}


int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Num_one(n);
	printf("%d\n", ret);
	return 0;
}

2. 将13⼆进制序列的(从右向左)第5位修改为1,然后再改回0

我们知道,  13  的原码和补码是:  00000000000000000000000000001101

像这种题目,我给大家分享一下我的思路吧,你就先按照题目要求,通过位操作符改掉指定位置,然后再在此基础上保持其他不变.

比如,它说要你把第五位修改为1,你就先满足这个需求,第五位本来是0,怎样把它修改为1 ,先不管其他位,是不是直接在这一位的下面 |  (按位或)上一个 1  就好了,因为 | 的运算规则是有 1 为 1   那其他位要保持不变,而且还要保证操作符是按位或,那其他位置按位或上一个 0 不就行了,想一想,其他位如果有 1  或上一个 0  之后还是 1  ,其它位如果为 0  或上一个 0  之后还是为 0   这是不是就对上了?

也就是  13:    00000000000000000000000000001101

|                    00000000000000000000000000010000

那这个是啥?  当然,你可以直接说它是 16  

但我想说它是  1<<4   都对   那结果就是 13+16  == 29  

分析完思路,我们直接上代码:

int main()
{
	int a = 13;
	int ret = a | (1 << 4);
	printf("%d\n", ret);
	return 0;
}

验证一下结果:

改完了还得还原回去,  现在是:  29:   00000000000000000000000000011101

还是一样,先不管其他位,

先把第五位 & 0 再说  其他位&1:          11111111111111111111111111101111                      

这个是啥?  1 太多了看不出来,先取个反: 00000000000000000000000000010000

这不取不知道, 一取吓一跳,这不就是 1<<4  嘛   然后逆向写代码就好啦~  

也就是:

int main()
{
	int a = 13;
	int ret = a | (1 << 4);

	ret &= ~(1 << 4);
	printf("%d\n", ret);
	return 0;
}

检验一下结果:

好啦~  这一篇就到这里,我们下一篇再见!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值