一个爱历史的程序猿

我知道这个世界很大。但只有疯狂到相信自己能改变世界的人,才能改变世界。

导航

负数位运算的右移操作-C语言基础

Posted on 2018-12-23 01:08  梁小满  阅读(4502)  评论(0编辑  收藏  举报

 

在这里插入图片描述
这一篇探讨的是“负数位运算的右移操作”,涉及到数据的源码、反码、补码的转换操作。属于C语言基础篇


先看例子

#include <stdio.h>
int main(void) {
	//正数的位右移
	//补码0000 0101
	int x = +5;
	//正数补码右移两位后
	//补码0000 0001
	printf("+5>>2 = %d\n", x>>2); //+5>>2 = 1
	
	//负数的位右移
	//补码1111   1011
	int y = -5;
	//负数补码右移两位后
	//补码1111   1110
	printf("-5>>2 = %d\n", y>>2); //-5>>2 = -2
}

输出结果。
在这里插入图片描述


好了,现在来解释一下这个输出结果是怎么来的。

在讨论负数的右移之前,我们先要了解一下什么是原码、什么是反码、什么是补码

任何一个数据都有其唯一对应的原码、反码以及补码。计算机对于数据的处理都是以补码形式来进行的(至于为什么要这样就又可以展开一整篇文章来说明了,这里就不深入讨论了)。而且正数和负数的原反补码的转换规则是不一样的。

对于正数来说,其原、反、补码都是相同的,都是该数据的二进制形式。例如+5的原、反补码均为0000……0101(其中0的个数由该数据的类型以及计算机操作系统的位数决定)。其最高位为0表示正数。

对于一个有符号数(既不加unsigned修饰的数据类型)来说,其最高位便是它的符号位。由于有符号数的最高位为符号位,这就使得char类型有符号数的取值范围为-128~+127而不是0~256。

对于负数来说,其原码是在其正数原码的基础上,最高位改为1以表示负数。所以-5的原码为1000……0101。负数的反码是在保持源码符号位不变的情况下其余位取反。而负数的补码是其反码加1。


现在,我们知道了原码、反码、补码都是些什么了,那么这个例子的结果就很好分析了。

首先是 +5>>2 = 1

+5
原码 0000 …… 0101
补码 0000 …… 0101
>>2(正数右移高位补0)
补码 0000 …… 0001
原码 0000 …… 0001
= 1

然后是 -5>>2 = -2

-5
原码 1000 …… 0101
反码 1111 …… 1010 负数的反码是保留符号位不变源码取反
补码 1111 …… 1011 补码是反码加1
>>2 (负数右移高位补1)
补码 1111 …… 1110
反码 1111 …… 1101 补码转反码减1
源码 1000 … 0010 负数反码转源码保留符号位不变取反
= -2


以上就是有符号数的右移操作了,随便说一下“有符号数的左移”以及“负数的无符号右移”(没有“无符号左移”这个说法,因为左移是在低位补0,而符号位在高位,左移之后补的数据不能影响最终的符号)

有符号数的左移

无论是有符号的正数还是负数,其左移都是在其补码的基础上面左移,而且低位都是补0。看到这里,你是否就意识到了一个问题,“如果正数的补码在左移的过程中,刚好有一个1移到了最高位,那么是否就会变成了负数呢?”嗯,这种情况确实是会发生的。负数左移到一定值的时候也会变成正。

无符号右移

注意:在C语言中是没有“无符号右移”运算符的,在Java中用“>>>”表示,C语言中可以利用“((unsigned int)(-5))>>n”来实现

无论是正数还是负数,其无符号右移都是在其补码的基础上右移,高位补0。
例如

-5
原码 1000 …… 0101
反码 1111 …… 1010 负数的反码是保留符号位不变源码取反
补码 1111 …… 1011 补码是反码加1
>>>2(无符号右移,高位补0)
补码 0011 …… 1110
反码 (此时符号位已经变为0了,系统会当成正数来处理,原反补码均一样)
原码 0011 …… 1110
= 这个不好说,得看操作系统的位数(在我这里int为32位,
结果为:107374182==>00111111 11111111 11111111 11111110)


题外话(纯属瞎扯,感兴趣的可以看下)

为什么我们要讨论数据的移位操作呢?关于这点,我想要说一些不太恰当的题外话。

虽然对于嵌入式的开发,伦理上来说应该会经常涉及对于数据的位操作才对。曾经有个老师是这么对我说的“学单片机其实就是在学位操作”,但是我对于这句话的解读却不是太赞成,我觉得应该这么说才更符合现在的开发环境,“学单片机其实就是在学习控制位操作。”为什么这么说呢?我不太严谨的说一下自己的看法吧!虽然,移位操作在AT89S51的开发中表现的很明显。但是如今AT89S51的应用场景已经越来越少了。移位操作在STM32的开发中就表现的不太明显,由于在STM32的开发过程中,我们大多数时候都是用库函数来完成,利用了别人已经封装好的函数来开发。而且这些库函数大多数情况下还不仅仅是封装一层。这虽然大大提高了程序员开发时的方便性,不过也造成了初学者对于自己写的程序是如何指导芯片正常工作的中间过程模糊不清。如果你不用库函数来操作,效率又太低,所以大多数开发人员都还是用现成的库函数来开发。移位操作用的较少。

移位操作在底层的开发中(特别是汇编语言)用的很多,但在在应用层上面应用的比较少了。要求的掌握程度几乎处于“知道有这么一回事,接触到的时候,能想的起来知道它干了什么就行” 的地步。很多开发人员就只在刚刚开始学习编程的时候以及去面试做笔试的时候接触过数据的移位操作,之后就再也用不上了。但是对于类似数据移位的这种应用范围比较窄的知识点,我想要说的是,希望大家在学习的过程中多留一个心眼,不要觉得不重要不常用就不去重视。因为这些知识点往往能在某些特定场景下面有奇效。就例如移位操作的妙用。


在介绍这个妙用之前,我们需要知道一个前提。

计算机在处理数据的时候,处理加、减、乘、除所需要的时间是不一样的,其中加减所需要的时间和移位操作几乎是一样的可以忽略不计,但是乘法需要的时间却是加法的十到二十倍。而除法所需要的时间几乎是加法的二十到三十倍。具体是多少,这个不好说,在不同的机器上面是不一样的。但可以确定的是,乘除所需要的时间总是比加减移位多。特别是除法。

在刚刚开始接触数据的移位操作这个知识点的时候,老师就已经和我们说过移位操作的结果和原数据之间的关系。

其关系大概如下:

对于正数在不溢出的情况下的左移和右移(由于负数的左移和右移后得到的数据和原数据之间的关系不明显,所以仅仅讨论正数)

左移:
左移n位后的数据 = 原数据乘2n
右移:
右移n位后的数据 = 原数据除2n

一开始了解到这个知识点的时候,我对此是很不屑的,因为实在是太局限了,即便移位操作比乘除快很多,但是这个乘的数值或除的数值必须是2^n也太鸡肋了吧。而且如今计算机的运算速度这么快,这点运输速度之间的差异实在不算什么。

但是在后来我看的一个例子中,利用移位操作却有奇效。在这个例子中,有两个关键因素使得移位操作比乘除运算好。一个是对于运算结果精度要求不高,另外一个是运算的数据量巨大。这个例子展开来说,又是一整篇文章了。感兴趣的话可以看一下这篇文章。虽然在这个例子中,移位操作并不能真正解决最终问题。但是,却可以给我们一个启发“在某些场景下,一些平时不受待见的冷门知识点,却出奇的会很好用”。


原博客始发于CSDN,在如今博客界的转载抄袭泛滥的环境下,原创不易,点个赞再走呗。以下是博客首页的链接。


零BUG是原则性问题。