补码反码、零扩展和符号位扩展
(ZeroextensionandSignextension)
众所周知,每种基本数据类型都有⼀个固定的位数,⽐如byte占8位,short占16位,int占32位等。正因如此,当把⼀个低精度的数据类型转成⼀个⾼精度的数据类型时,必然会涉及到如何扩展位数的问题。这⾥有两种解决⽅案:
负数二进制补码运算法则(1)补零扩展:填充⼀定位数的0。
(2)补符号位扩展:填充⼀定位数的符号位(⾮负数填充0,负数填充1)。
对于⽆符号类型(相当于都是⾮负数)与有符号类型中的⾮负数部分,这两种⽅法没有区别,都是填充0;对于有符号类型中的负数部分,这两种⽅法就会产⽣差异了,补零扩展会填充0,⽽补符号位扩展会填充1。下⾯将byte类型的-127转为int类型为例,探讨⼀下这两种⽅法的区别。
⾸先必须明确⼀些知识点:
计算机是⽤补码来存储数字的;
⼀个数的补码的补码等于原码。
反码、补码
⾸先,我们从位权的含义说起。例如,⼗进制39的各个数位的数值,并不是简单的3和9,这点⼤家都知道,3表⽰的是3x10,9表⽰的是9x1。这⾥和各个数位的数值相乘的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.. 按照上⾯钟表的例⼦,其实也⾏的通,但是其似乎偏离了设计者得本意与它得本质)
补码的思考⽅式,虽然直观上不易理解,但逻辑上⾮常严谨,例如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;
我认为,补码不过是表⽰负数的⼀种⽅式,补码不是相对正数的吗?"正数的补码"有何含义吗?希望有⼈给我解答其具体含义?
⾼级程序设计语⾔允许程序员使⽤包含不同字节⼤⼩整数的对象表达式。
那么,当⼀个表达式的两个操作数⼤⼩不同时,有些语⾔会报错,有些语⾔则会⾃动将操作数转换成⼀个统⼀的格式。这种转换是有代价的,因此如果你不希望编译器在你不知情的情况下⾃动加⼊各种转换到原本⾮常完美的代码中,你就需要掌握编译器如何处理这些表达式。
零扩展
在移动或转换操作中,零扩展是指将⽬标的⾼位设置为零,⽽不是将其设置为源的最⾼有效位的副本。 如果操作的源是⽆符号数字,则零扩展通常是在保留其数值的同时将其移⾄更⼤字段的正确⽅法,⽽符号扩展对于有符号数字是正确的。
⾼位直接补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
例如,在,有两种⽅式进⾏符号扩展:
使⽤指令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;
}
结果
std::bitset
std::bitset 是 ⼀种 位集存储位(元素只有两个可能的值:0或1 true或false,...)。
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>)。
同⼀长度数据类型中,有符号数与⽆符号数相互转换
直接将内存中的数据赋给要转化的类型,数值⼤⼩则会发⽣变化。另外,短类型扩展为长类型时,短类型与长类型分属有符号数与⽆符号数时,则先按规则⼀进⾏类型的扩展,再按本规则直接将内存中的数值原封不动的赋给对⽅。以下是有符号数与⽆符号数之间的转换:
有符号数的转换
⽆符号数的转换
《程序是怎样跑起来的》 - ⽮泽久雄
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论