补码/反码、零扩展和符号位扩展(Zero extension and Sign extension)

众所周知,每种基本数据类型都有一个固定的位数,比如byte占8位,short占16位,int占32位等。正因如此,当把一个低精度的数据类型转成一个高精度的数据类型时,必然会涉及到如何扩展位数的问题。这里有两种解决方案:
(1)补零扩展:填充一定位数的0。
(2)补符号位扩展:填充一定位数的符号位(非负数填充0,负数填充1)。
对于无符号类型(相当于都是非负数)与有符号类型中的非负数部分,这两种方法没有区别,都是填充0;对于有符号类型中的负数部分,这两种方法就会产生差异了,补零扩展会填充0,而补符号位扩展会填充1。下面将byte类型的-127转为int类型为例,探讨一下这两种方法的区别。

 

首先必须明确一些知识点:

  • 计算机是用补码来存储数字的;
  • 一个数的补码的补码等于原码。

反码、补码

首先,我们从位权的含义说起。例如,十进制39的各个数位的数值,并不是简单的3和9,这点大家都知道,3表示的是3×10,9表示的是9×1。这里和各个数位的数值相乘的10和1,就是位权。数字的位数不同,位权也不同。第一位(最右边的一位)是10的0次幂,第二位是10的1次幂….以此类推。

位权的思考方式同样适用于二进制。即第一位是2的0次幂,第二位是2的1次幂…. “OO的XX次幂”表示位权,其中,十进制数的情况下OO部分为10,二进制的情况为2,这个则称为基数

在日常生活当中,可以看到很多这样的事情:

  1. 把某物体左转 90 度,和右转 270 度,在不考虑圈数的条件下,最终的效果是相同的;
  2. 把分针倒拨 20 分钟,和正拨 40 分钟,在不考虑时针的条件下,效果也是相同的;
  3. 把数字 87,减去 25,和加上 75,在不考虑百位数的条件下,效果也是相同的;
  4. ……。

上述几组数字,有这样的关系:

  •   90 + 270 = 360
  •   20 + 40 = 60
  •   25 + 75 = 100

式中的 360、60 和 100,就是“”。
式中的 90 和 270、20 和 40,以及 25 和 75,就是一对对“互补”的数字。

知道了“模”,求某个数字的“补数”,就是轻而易举的了:
如果模为 365,数字 120 的补数为:365 – 120 = 245。

用补数代替原数,可把减法转变为加法。出现的进位就是模,此时的进位,就应该忽略不计。

接下来我们正式引入补数:

二进制数中表示负数值时,一般把最高位作为符号位来使用,符号位为0时表示正数,为1时表示负数。

那么-1用八位二进制来表示的话是怎么样的呢?可能很多人认为1的二进制’0000 0001’(常规思维),因此-1的二进制就是‘1000 0001’。但这个答案是错位的,正确答案是‘1111 1111’(计算机补码形式)。(PS:有些教程可能写0000 0001,它可能非计算机八位二进制补码形式,而查用我们数学表达?而下面使用符号位不变取反也是这批人。)

而计算机里面,只有加法器,没有减法器,所有的减法运算,都必须用加法进行。计算机再做减法运算时,实际上内部是在做加法运算,在表示负数时就需要使用“二进制的补数”补数就是用正数来表示负数,很不可思议吧。

为了获得补数,我们需要将二进制的各数位的数值全部取反,然后再将结果加一。例如,用八位二进制表示-1时,只需求得1,也就是0000 0001得补数即可。具体来说,就是将各位数得0取反得到1,1取反成0,然后将取反得结果为1,最后就转化为了1111 1111。

(ps:国内还有一种算法就是符号位不变,balala.. 按照上面钟表的例子,其实也行的通,但是其似乎偏离了设计者得本意与它得本质)

《补码/反码、零扩展和符号位扩展(Zero extension and Sign extension)》

补码的思考方式,虽然直观上不易理解,但逻辑上非常严谨,例如1-1也就是1+(-1)这一运算,我们都知道答案为0。首先,让我们将-1表示为1000 0001(错误方式,原码)来运算,看看结果如何,0000 0001 + 1000 0001 = 1000 0010,很显然结果不是0。

接下来,我们把-1表示为1111 1111 (补码)来进行运算。 0000 0001 + 1111 1111 = 1 0000 0000 。最高位溢出,对于溢出位,计算机回自动忽略掉。在八位这个范围内计算,1 0000 0000 这个 九位二进制会被认为是 0000 0000 这一八位二进制数。

请牢记“将二进制数的值取反后加一的结果,和原来的值相加,结果为零”这一法则。

那么 1111 1110 表示的负数是多少大家知道吗?这时,我们可以利用负负得正的性质,假若1111 1110是负,那么1111 1110的补数就是正。通过求解补数的补数,就可知道该值的绝对值。1111 1110的补数,取反加1后为0000 0010。这个是2的十进制数,因此1111 1110表示的就是-2。

另外,关于下面网上说法,我不知道其观点的具体含义,我在中文维基百科看到有这样描述,但在英文版似乎没有发现。 这里也不是理解正数的补码与负数的补码这种说法。

  • 正数的补码等于原码; 
  • 负数的补码等于反码+1;

我认为,补码不过是表示负数的一种方式,补码不是相对正数的吗?”正数的补码”有何含义吗?希望有人给我解答其具体含义?

《补码/反码、零扩展和符号位扩展(Zero extension and Sign extension)》

高级程序设计语言允许程序员使用包含不同字节大小整数的对象表达式。

那么,当一个表达式的两个操作数大小不同时,有些语言会报错,有些语言则会自动将操作数转换成一个统一的格式。这种转换是有代价的,因此如果你不希望编译器在你不知情的情况下自动加入各种转换到原本非常完美的代码中,你就需要掌握编译器如何处理这些表达式。

零扩展

在移动或转换操作中,零扩展是指将目标的高位设置为零,而不是将其设置为源的最高有效位的副本。 如果操作的源是无符号数字,则零扩展通常是在保留其数值的同时将其移至更大字段的正确方法,而符号扩展对于有符号数字是正确的。

高位直接补0的扩展,如1111变成00001111,补0并不影响计算结果,这个很好理解,但如果二进制数带了符号,就不一样了,因为最高位是符号位,所以1111就从一个负数,变成了一个正数00001111,由此,产生了符号扩展。

在x86和x64指令集中,movzx指令(“零扩展移动”)执行此功能。 例如,movzx ebx,al将一个字节从al寄存器复制到ebx的低位字节,然后用零填充ebx的其余字节。

在x64上,大多数写入任何通用寄存器的低32位的指令都会将目标寄存器的高一半置零。 例如,指令mov eax,1234将清除rax寄存器的高32位。

符号扩展

符号扩展是计算机算术中在保留数字的符号(正/负)和值的同时增加二进制数的位数的操作。 这是通过根据所使用的特定带符号的数字表示的过程,将数字附加到数字的最高有效位来完成的。

例如,如果使用六位表示数字“ 00 1010”(十进制正数10),并且符号扩展操作将字长增加到16位,则新的表示形式就是“ 0000 0000 0000 1010”。因此,既保持了价值,又保持了价值为正的事实。

如果用10位表示用二进制补码值“1111110001”(十进制负15),并且将其符号扩展为16位,则新表示为“1111 1111 1111 0001 ”。因此,通过在左侧填充ones,可以保持负号和原始编号的值。 1111 1111 1111 0001

例如,在Intel x86指令集中,有两种方式进行符号扩展:

  • 使用指令cbw,cwd,cwde和cdq:分别将字节转换为字,将字转换为双字,将字转换为扩展双字和将双字转换为四字(在x86上下文中,一个字节有8位,一个字有16位,一个双字和扩展的双字32位和四字64位);
  • 使用由movsx(“带符号扩展的移动”)指令系列完成的符号扩展移动之一。

实例

举个例子:

-127原码1111 1111,反码1000 0000,补码1000 0001。计算机存储的是1000 0001,用十六进制表示为0x81。

  • 当使用补零扩展时,结果为:

0000 0000 0000 0000 0000 0000 1000 0001 (与补码数值形式一致)
用十六进制表示为0x81。为了计算十进制值,计算它的补码,结果为:
0000 0000 0000 0000 0000 0000 1000 0001
将这个二进制数转成十进制的结果是129。

  • 当使用补符号位扩展时,结果为:

1111 1111 1111 1111 1111 1111 1000 0001 (和补码数值看上去差别较大)
用十六进制表示为0xFFFFFF81。为了计算十进制值,计算它的补码,结果为:
1000 0000 0000 0000 0000 0000 0111 1111
将这个二进制数转成十进制的结果是-127。

由此可以得出结论:
(1)使用补零扩展能够保证二进制存储的一致性(和我们数学常理一致),但不能保证十进制值不变。所以,处理无符号二进制数的时候,可以使用零扩展(zero extension)将小位数的无符号数扩展到大位数的无符号数
(2)使用补符号位扩展能够保证十进制值不变,但不能保证二进制存储的一致性(负数的补码变了,需要 &0xff)而处理不同长度的有符号数时,我们必须使用符号扩展

在C/C++中,如果把一个char向一个整形转换的时候,就会存在着这个问题。

如果你想得到一个正数,那么如果一个字符的ASCII码值是小于零的,而直接用(int)c进行强制类型转换,结果是通过符号扩展得到的也为一个负数。

要得到正数,一定要用(int)(unsigned char)c;因为unsigned char去除了c的符号位,所以,这样的类型转换后,再用(int)进行转换得到的就是一个正数。

#include <iostream> 
#include <string>
#include <algorithm>
#include <bitset>       

int main()
{

	int i = 129;
	char chA = (char)i;
	int c = (int)(unsigned char)chA;
	int b = (int)chA;

	std::cout << "sign extension: " << b << std::endl;
	std::cout << "zero extension: " << c << std::endl;

    char d = -127;
	std::bitset <sizeof(int) * 8> x(d);   
	std::cout << "sign extension: " << x << std::endl;

	unsigned char e = (d & 0XFF);
	std::bitset <sizeof(int) * 8> y(e);
	std::cout << "sign extension: " << y << std::endl;
	
	return 0;
}

 结果

《补码/反码、零扩展和符号位扩展(Zero extension and Sign extension)》

std::bitset

std::bitset 是 一种 位集存储位(元素只有两个可能的值:0或1 truefalse,…)。

  • bitset存储二进制数位。
  • bitset就像一个bool类型的数组一样,但是有空间优化——bitset中的一个元素一般只占1 bit(在大多数系统上,相当于一个char元素所占空间的八分之一,一个char占用一个字节byte,8位bits)
  • bitset中的每个元素都能单独被访问,例如对于一个叫做foo的bitset,表达式foo[3]访问了它的第4个元素,就像访问了数组其元素一样。但是,因为在大多数C ++环境中没有元素类型是单个位,所以可以将单个元素作为特殊引用类型进行访问(请参见bitset :: reference)。
  • bitset具有可以从整数值和二进制字符串构造并转换为整数值的功能(请参阅其构造函数和成员to_ulong和to_string)。它们也可以直接以二进制格式插入和从流中提取(请参阅适用的运算符)。
  • bitset的大小在编译时就需要确定(由其模板参数确定)。有关还可优化空间分配并允许动态调整大小的类,请参见vector的布尔特殊化(vector <bool>)。

同一长度数据类型中,有符号数与无符号数相互转换

直接将内存中的数据赋给要转化的类型,数值大小则会发生变化。另外,短类型扩展为长类型时,短类型与长类型分属有符号数与无符号数时,则先按规则一进行类型的扩展,再按本规则直接将内存中的数值原封不动的赋给对方。以下是有符号数与无符号数之间的转换:

有符号数的转换
 

《补码/反码、零扩展和符号位扩展(Zero extension and Sign extension)》

无符号数的转换 

《补码/反码、零扩展和符号位扩展(Zero extension and Sign extension)》

参考:

关于补零扩展与补符号位扩展_swt369的博客-CSDN博客_无符号short型存储后面补零

C++ 中注意,零扩展和符号位扩展_jaylong35的专栏-CSDN博客_符号位扩展

符号扩展,零扩展与符号缩减

《程序是怎样跑起来的》 – 矢泽久雄 

    原文作者:Jeff_
    原文地址: https://blog.csdn.net/weixin_40539125/article/details/103058420
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞