文章目录
- 二进制数制
- 十进制
- 二进制
- 位模式
- 基本数据类型
- 无符号数的编码
- 有符号数的编码
- 原码(Sign-Magnitude)
- 反码(Ones' Complement)
- 补码(Two's Complement)
- 概念导读
- 编码格式
- 按权展开
- 补码加法
- 扩展一个数字的位表示
- 有符号数和无符号数之间的转换
- 参考
二进制数制
所谓数制是指计数的方法。
十进制
人两手加起来共10根手指,故日常计数和做算术都使用十进制。大家熟悉并使用了一千多年的十进制起源于印度,在12世纪被阿拉伯数学家改进,并在13世纪被意大利数学家Leonardo Pisano(Fibonacci)带到西方。1、2、3的罗马计数法是Ⅰ、Ⅱ、Ⅲ,Ⅰ+Ⅱ=Ⅲ 直观展示了加法运算的含义。
数据无论使用哪种进位制,都涉及两个基本要素:基数(radix)与各数位的“位权”(weight)。
十进制数有两个特点:
- 用0、1、2、3、…、9这10个基本符号表示;基本数字符号(数码)的个数叫基数。
- 遵循“逢十进一”原则,每位计满十时向高位进一。
一般地,任意一个十进制数 N 都可以表示为 ∑ i = − m n − 1 K i ∗ 1 0 i \sum_{i=-m}^{n-1}K_i\ast10^i ∑i=−mn−1Ki∗10i:
N = K n − 1 ∗ 1 0 n − 1 + K n − 2 ∗ 1 0 n − 2 + ⋯ + K 1 ∗ 1 0 1 + K 0 ∗ 1 0 0 + K − 1 ∗ 1 0 − 1 + K − 2 ∗ 1 0 − 2 + ⋯ + K − m ∗ 1 0 − m N = K_{n-1}\ast10^{n-1} + K_{n-2}\ast10^{n-2} + \cdots + K_1\ast10^1 + K_0\ast10^0 + K_{-1}\ast10^{-1} + K_{-2}\ast10^{-2} + \cdots + K_{-m}\ast10^{-m} N=Kn−1∗10n−1+Kn−2∗10n−2+⋯+K1∗101+K0∗100+K−1∗10−1+K−2∗10−2+⋯+K−m∗10−m
抛开小数部分,整数按权的展开式为:
N = ∑ i = 0 n − 1 K i ∗ 1 0 i = K n − 1 ∗ 1 0 n − 1 + K n − 2 ∗ 1 0 n − 2 + ⋯ + K 1 ∗ 1 0 1 + K 0 ∗ 1 0 0 N = \sum_{i=0}^{n-1}K_i\ast10^i = K_{n-1}\ast10^{n-1} + K_{n-2}\ast10^{n-2} + \cdots + K_1\ast10^1 + K_0\ast10^0 N=i=0∑n−1Ki∗10i=Kn−1∗10n−1+Kn−2∗10n−2+⋯+K1∗101+K0∗100
一个数字符号在不同位时,代表的数值不同。在上述表达式中,数位 K i K_i Ki 的权为 1 0 i 10^i 10i(以基数 10 10 10为底,序号 i i i为指数),数字符号乘以其位权为这个数字符号所表示的真实数值( K i ∗ 1 0 i K_i\ast10^i Ki∗10i)。
二进制
在 字节存储单元及struct内存分配 中,我们介绍了二进制和以及字节存储单元。现代计算机存储和处理的信息以二值信号表示。这些微不足道的二进制数字,或者称为位(bit),形成了数字革命的基础。
对于有10根手指的人来来说,使用十进制表示法是很自然的事情,但是当构造存储和处理信息的机器时,二进制工作得更好。在计算机内部,二进制总是存放在由具有两种相反状态的存储元件构成的寄存器或存储单元中,即二进制数码0和1是由存储元件的两种相反状态来表示的。这使得二值信号很容易地被表示、存储和传输。
二值信号可以表示为导线上的高电压或低电压、晶体管的导通或截止、电子自旋的两个方向,或者顺时针或逆时针的磁场。指令集及流水线 中提到,在上个世纪的打孔编程时代,纸带上的每个孔代表一位(bit):穿孔(presence)表示1,未穿孔(absence)表示0。
十进制数按权的展开式可以推广到任意进位计数制。二进制中只有0和1两个字符,基数为2,满足“逢二进一”。权用 2 i 2^i 2i 表示,二进制的按权展开式为 N = ∑ i = 0 n − 1 K i ∗ 2 i N = \sum_{i=0}^{n-1}K_i\ast2^i N=∑i=0n−1Ki∗2i。
二进制与其他数制相比,有以下显著特点:
- 数制简单,容易基于元器件的电子特性实现数字逻辑电路。
- 由于二进制只有状态,因此抗干扰性强,可靠性、稳定性高。
- 可以基于布尔逻辑代数进行分析和综合,运算规则相对简单易实现。
基数为2的好处在于基本算术运算表很短,对比一下十进制和二进制的加法和乘法表,长短相形一目了然。
位模式
2个比特可以组合出4( 2 2 2^2 22)种状态,可表示无符号数值范围[0,3];32个比特可以组合出4294967296( 2 32 2^{32} 232)种状态,可表示无符号数值范围[0,4294967295];……。
由于一个位只能表示二元数值,所以单独一位的用处不大。当把位组合在一起,再加上某种解释(interpretasion),即赋予不同的可能位模式以含意。通常将固定位数的位串作为一个基本存储单位,这样就可以存储范围较大的值。在有限范围内的可计量数值几乎都可以用二进制数码位串组合表示,计算机的内存由数以亿万计的比特位存储单元(晶体管)组成。
大多数计算机使用8位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是访问内存中单独的位(bit)。机器级程序将内存视作一个非常大的字节数组,内存的每个字节都由一个唯一的数字来标识,称为它的地址。
每个程序对象可以简单地视为一个字节块,程序本身本身就是一个字节序列(机器指令序列)。
基本数据类型
在 C++ Variable Types 中,我们总结了C/C++中的基本数据类型。C/C++语言支持多种整形数据类型——表示有限范围的整数。每种类型都用关键字来指定大小,这些关键字包括 char、short、long,参考 gnu libc Integers。C/C++语言都支持有符号(默认)和无符号数。
- 长短修饰符
short
和long
用于修饰整形:默认为 long 长整形,短整形需显示指定 short。 - 符号修饰符
signed
和unsigned
用于修饰字符型和整形:缺省为signed
有符号类型,无符号需显示指定unsigned
修饰。 - 当用
signed
/unsigned
、short
/long
来修饰 int 整形时,int 可省略。
以下是字符型、短整型、整型有无符号的区分表示:
- 有符号字符型:char/signed char;无符号字符型:unsigned char
- 有符号短整型:short [int] /signed short [int];无符号短整型:unsigned short [int]
- 有符号整型:int /signed [int];无符号整型:unsigned [int]
无符号数的编码
无符号(unsigned)编码基于传统的二进制表示法,表示大于或等于0的非负数。
假设有一个整数数据类型有w位。我们可以将位向量写成 x ⃗ \vec{x} x 表示整个向量,或者写成 [ x w − 1 , x w − 2 , ⋯ , x 0 ] [x_{w-1}, x_{w-2}, \cdots, x_0] [xw−1,xw−2,⋯,x0],表示向量中的每一位。把 x ⃗ \vec{x} x 看成一个二进制表示的数,就获得了 x ⃗ \vec{x} x 的无符号表示。在这个二进制编码中,每个位 x i x_i xi 都取值0或1,后一种取值意味着数值 2 i 2^i 2i 应为数字值的一部分。我们用一个函数 B 2 U w B2U_w B2Uw(Binary to Unsigned的缩写,长度为 w w w)来表示。
对位向量 x ⃗ = [ x w − 1 , x w − 2 , ⋯ , x 0 ] \vec{x} = [x_{w-1}, x_{w-2}, \cdots, x_0] x=[xw−1,xw−2,⋯,x0]:
B 2 U w ( x ⃗ ) = ˙ ∑ i = 0 w − 1 x i ∗ 2 i B2U_w(\vec{x}) \dot=\sum_{i=0}^{w-1}x_i\ast2^i B2Uw(x)=˙i=0∑w−1xi∗2i
在这个等式中,等号“ = ˙ \dot= =˙”表示左边被定义为等于右边。函数 B 2 U w B2U_w B2Uw 将一个长度为 w w w 的0、1串映射到非负整数。
w w w位所能表示的最小值用位向量 [ 00 ⋯ 0 ] [00\cdots0] [00⋯0](全零)表示,也就是整数值0,而最大值是用位向量 [ 11 ⋯ 1 ] [11\cdots1] [11⋯1](全1),也就是整数值 U M a x w = ˙ ∑ i = 0 w − 1 x i ∗ 2 i = 2 w − 1 UMax_w\dot=\sum_{i=0}^{w-1}x_i\ast2^i=2^w-1 UMaxw=˙∑i=0w−1xi∗2i=2w−1。即 w w w位二进制位串所能表示的数值范围是 [ 0 , 2 w − 1 ] [0, 2^w-1] [0,2w−1]。
在主流64位LP64实现中,unsigned char、unsigned short [int]、unsigned [int]、unsigned long [int](或 long unsigned [int])分别占1、2、4、8个字节(8、16、32、64位)。无符号字符(unsigned char)占用1个字节(8位),所能表示的数值范围是 [ 0 , 2 8 − 1 ] = [ 0 , 255 ] [0, 2^8-1] = [0, 255] [0,28−1]=[0,255]。
有符号数的编码
对于很多应用,我们还希望表示负数值。在计算机中,如何表示符号呢?
在计算机中,对于数的符号(正号+
和负号-
)也只能用0和1这两位数字表示。通常用一个数的最高位作为符号位,最高位为0表示符号位为正;最高位为1表示符号位为负。这样,数的符号标识也“数码化”了。即带符号数的数值和符号统一用二进制数码形式来表示。
在将数的符号用数码(0或1)表示后,数值部分究竟是保留原来的形式,还是按一定的规则做某些变化,这要取决于运算方法的需要,从而有四种常见的机器数形式:原码、反码、补码 和 移码。
为了区别原来的数与它在机器中的表示形式,将一个数(连同符号)在机器中加以数码化后的形式,称为机器数或机器码,而把机器数所代表的实际数值称为真值。
原码(Sign-Magnitude)
原码表示法比较直观,其数值部分保留其真值(的绝对值)。
- 8位二进制原码表示的数值范围为 11111111-10000000,00000000-01111111,即 -127 - -0,+0 - +127。其中,“0”有-0和+0之分, [ − 0 ] 原 = 10000000 [-0]_原=1 0000000 [−0]原=10000000, [ + 0 ] 原 = 00000000 [+0]_原=0 0000000 [+0]原=00000000。
例如,正数89的二进制表示为
+
1011001
+1011001
+1011001,其原码表示为:
负数-89的二进制表示为
−
1011001
-1011001
−1011001,其原码表示为:
原码表示法的优点是比较直观、简单易懂,后面在浮点数中有使用到原码编码。
原码的符号位不是数值的一部分,不能直接参与运算,导致加法运算复杂。为了解决这些矛盾,引入了反码和补码。
反码(Ones’ Complement)
对于正数而言,其反码形式与其原码相同:最高位为符号位,用0表示正数,其余位为数值位不变。对于负数而言,其反码表示为:最高位符号位为1,其余数值位在原码的基础上按位取反。反码在机器中的表示形式如下:
- 8位二进制反码表示的数值范围为 10000000-11111111,00000000-01111111(负数数值部分求反复原:11111111-10000000,00000000-01111111),即 -127 - -0,+0 - +127。其中,“0”有-0和+0之分, [ − 0 ] 反 = 11111111 [-0]_反=1 1111111 [−0]反=11111111, [ + 0 ] 反 = 00000000 [+0]_反=0 0000000 [+0]反=00000000。
-89的二进制表示为
−
1011001
-1011001
−1011001,其原码表示为
11011001
1 1011001
11011001,则其反码表示为
10100110
1 0100110
10100110。
将反码表示规则用表达式形式定义如下:
[ X ] 反 = { X , X > 0 或 X = + 0 ( 2 n − 1 ) − ∣ X ∣ , X < 0 或 X = − 0 % 反码 [X]_反= \begin{cases} X, & X>0 或 X=+0 \\ (2^n-1)- \lvert X \rvert, & X<0 或 X=-0 \end{cases} [X]反={X,(2n−1)−∣X∣,X>0或X=+0X<0或X=−0
当 X<0 或 X=-0 时,按照无符号数解析位向量, ∣ X ∣ + [ X ] 反 → = 2 n − 1 \lvert X \rvert + \overrightarrow{[X]_反}=2^n-1 ∣X∣+[X]反=2n−1。例如当n=8时, ∣ X ∣ + [ X ] 反 → = 11111111 = 255 \lvert X \rvert + \overrightarrow{[X]_反}=11111111=255 ∣X∣+[X]反=11111111=255。
虽然过去生产过基于反码表示的机器,但是几乎所有的现代机器都使用补码形式表示有符号整数。现在通常已不再单独使用反码,而主要是作为求补码的一个中间步骤来使用。
补码(Two’s Complement)
在计算机中,最常见的有符号(整)数的表示方式是补码。采用补码运算可以将减法变成补码加法运算,在微处理器中只需加法的电路就可以实现加法、减法运算。
概念导读
为了理解补码的概念,我们先来看看圆周运动的例子。在现实生活中,一个圆周的可视角度度量为0-2π(或0°-360°),以原点为起点的射线OA逆时针旋转一圈的弧度为2π。
圆周运动具有周期性,即具有“周而复始”的变化规律。假设OA的初始弧度为α,终边OA每绕原点旋转一周,α增加2π弧度(旋转k周后,弧度变成α+k·2π),但OA位置不变。由三角函数的定义可知,终边相同的角的同一三角函数的值相等。
sin ( α + k ⋅ 2 π ) = sin α cos ( α + k ⋅ 2 π ) = cos α tan ( α + k ⋅ 2 π ) = tan α \begin{gather*} \sin(\alpha+k·2\pi) = \sin\alpha \\ \cos(\alpha+k·2\pi) = \cos\alpha \\ \tan(\alpha+k·2\pi) = \tan\alpha \end{gather*} sin(α+k⋅2π)=sinαcos(α+k⋅2π)=cosαtan(α+k⋅2π)=tanα
由上面的公式可知,可以把求任意角的三角函数值,转化为求0-2π(或0°-360°)角的三角函数值。射线OA逆时针旋转π和顺时针旋转π(-π)的终边是相同的,逆时针旋转5π/3和顺时针旋转π/3(-π/3)的终边是相同的。以下将负弧度的正弦计算转化到0-2π区间换算:
sin ( − π + 2 π ) = sin π sin ( − π 3 + 2 π ) = sin 5 π 3 \begin{gather*} \sin(-\pi+2\pi) = \sin\pi \\ \sin(-\frac{\pi}{3}+2\pi) = \sin\frac{5\pi}{3} \end{gather*} sin(−π+2π)=sinπsin(−3π+2π)=sin35π
我们再来进一步讨论日常生活中校正时钟的例子。假定时钟停在7点,而正确时间是5点,要拨准时钟可以有两种不同的拨法:倒拨2个格(时光倒流2h),顺拨10个格(穿越到未来10h后)。想象一下,龟兔在7点钟刻度背向而行,假设兔子的速度是乌龟的5倍,兔子顺时针跑10格,乌龟逆时针跑2格,它们将在5点刻度处迎面相遇。
倒拨2个格,即7-2=5(做减法);顺拨10个格,即7+10=12+5=5(做加法,钟面上12=0)。这就表明,在舍掉进位的情况下,“从7减去2”和“往7加上10”所得的结果是一样的。而2和10的和恰好等于模数12。我们把10称为-2对于模数的补数。
之所以顺拨(做加法)与倒拨(做减法)的结果相同,是由于钟面的容量有限,其刻度是是十二进制,超过12以后又从零开始计数,自然丢失了12。此处12是溢出量,又称为模(mod)。在圆周运动中,每转动一周的2π弧度可视为溢出量(模)。
在电脑和手机的日期和时间偏好设置中,通常可以设置显示24小时,因为地球自转一圈是一天(接近24h)。24h制的1点和13点都对应钟表上的1点。“天天向上”、“日复一日”中的“天”和“日”即为溢出量,每过24h(模)又将开启崭新的一天。
计算机中的运算受一定字长的限制,它的运算部件与寄存器都有一定的位数,因而在运算过程中也会产生溢出量,所产生的溢出量实际上就是模。可见,计算机的运算也是一种有模运算。
编码格式
对于正数而言,其补码形式与其原码、反码相同:最高位为符号位,用0表示正数,其余位为数值位不变。对于负数而言,其补码表示为:最高位符号位为1,其余数值位在原码的基础上按位取反并加1(反码+1)。补码在机器中的表示形式如下:
-89的二进制表示为
−
1011001
-1011001
−1011001,其原码表示为
11011001
1 1011001
11011001,其反码表示为
10100110
1 0100110
10100110,则其补码表示为
10100111
1 0100111
10100111。
- 8位二进制补码表示的数值范围为 10000000-11111111,00000000-01111111,即 -128 - -1,+0 - +127。其中,无-0和+0之分,保证了0的唯一性。另外,取值范围为连贯区间 [-128, 127],包含128个负数、128个非负数。负数、非负数各占一半,负数比正数多一个。
将补码表示规则用表达式形式定义如下:
[ X ] 补 = { X , X ≥ 0 2 n − ∣ X ∣ ( 2 n + X ) , X < 0 % 补码 [X]_补= \begin{cases} X, & X\ge0 \\ 2^n- \lvert X \rvert(2^n+X), & X<0 \end{cases} [X]补={X,2n−∣X∣(2n+X),X≥0X<0
当 X<0 时,按照无符号数解析位向量, ∣ X ∣ + [ X ] 补 → = 2 n \lvert X \rvert+\overrightarrow{[X]_补}=2^n ∣X∣+[X]补=2n。例如当n=8时, ∣ X ∣ + [ X ] 补 → = 11111111 + 1 = 256 \lvert X \rvert+\overrightarrow{[X]_补}=11111111+1=256 ∣X∣+[X]补=11111111+1=256。
-89的二进制补码表示为 10100111 1 0100111 10100111,按照无符号数解析位向量的值为167,满足以下:
167 = 256 − ∣ − 89 ∣ ∣ − 89 ∣ + 167 = 256 \begin{gather} 167=256 - \lvert -89 \rvert \\ \lvert -89 \rvert + 167 = 256 \end{gather} 167=256−∣−89∣∣−89∣+167=256
按权展开
-89的二进制表示为 − 1011001 -1011001 −1011001,其原码表示为 11011001 1 1011001 11011001,其反码表示为 10100110 1 0100110 10100110,则其补码表示为 10100111 1 0100111 10100111。
∣ − 89 ∣ \lvert -89 \rvert ∣−89∣ 的二进制位向量为 01011001 01011001 01011001:
- 反码数值部分位向量= ( 2 n − 1 − 1 ) − ∣ X ∣ = 127 − ∣ − 89 ∣ = 0 b 01111111 − 0 b 01011001 = 0 b 00100110 (2^{n-1}-1)-\lvert X \rvert = 127-\lvert -89 \rvert=0b01111111-0b01011001=0b00100110 (2n−1−1)−∣X∣=127−∣−89∣=0b01111111−0b01011001=0b00100110。
- 补码数值部分位向量= ( 2 n − 1 − 1 ) − ∣ X ∣ + 1 = 2 n − 1 − ∣ X ∣ = 127 − ∣ − 89 ∣ + 1 = 0 b 01111111 − 0 b 01011001 + 1 = 0 b 00100111 (2^{n-1}-1)-\lvert X \rvert+1 = 2^{n-1}-\lvert X \rvert = 127-\lvert -89 \rvert+1=0b01111111-0b01011001+1=0b00100111 (2n−1−1)−∣X∣+1=2n−1−∣X∣=127−∣−89∣+1=0b01111111−0b01011001+1=0b00100111。
补码高位符号位占1位,其余数值部分占 n-1 位。当将最高符号位解释为负权( 2 n − 1 2^{n-1} 2n−1)时,整体位向量刚好可计算出原始负值:
− 2 n − 1 + ( 2 n − 1 − ∣ X ∣ ) = − ∣ X ∣ -2^{n-1} + (2^{n-1}-\lvert X \rvert) = -\lvert X \rvert −2n−1+(2n−1−∣X∣)=−∣X∣
我们用一个函数 B 2 T w B2T_w B2Tw(Binary to Two’s Complement 的缩写,长度为 w w w)来表示位向量 x ⃗ = [ x w − 1 , x w − 2 , ⋯ , x 0 ] \vec{x} = [x_{w-1}, x_{w-2}, \cdots, x_0] x=[xw−1,xw−2,⋯,x0] 到补码的编码映射:
B 2 T w ( x ⃗ ) = ˙ − x w − 1 ∗ 2 w − 1 + ∑ i = 0 w − 2 x i ∗ 2 i B2T_w(\vec{x}) \dot=-x_{w-1}\ast2^{w-1} + \sum_{i=0}^{w-2}x_i\ast2^i B2Tw(x)=˙−xw−1∗2w−1+i=0∑w−2xi∗2i
最高有效位 x w − 1 x_{w-1} xw−1 称为符号位,它的“权重”为 − 2 w − 1 -2^{w-1} −2w−1,是无符号表示中权重的负数。符号位被设置为1时,表示值为负,而当设置为0时,值为非负。
∣ − 89 ∣ \lvert -89 \rvert ∣−89∣ 的二进制位向量为 01011001 01011001 01011001,-89的二进制补码表示为 10100111 1 0100111 10100111,可基于 B 2 T w B2T_w B2Tw 函数按权展开复原补码的真值:
B 2 T 8 ( [ 01011001 ] ) = − 0 ∗ 2 7 + 1 ∗ 2 6 + 1 ∗ 2 4 + 1 ∗ 2 3 + 1 ∗ 2 0 = − 0 + 64 + 16 + 8 + 1 = 89 B 2 T 8 ( [ 10100111 ] ) = − 1 ∗ 2 7 + 1 ∗ 2 5 + 1 ∗ 2 2 + 1 ∗ 2 1 + 1 ∗ 2 0 = − 128 + 32 + 4 + 2 + 1 = − 89 B2T_8([0 1011001]) = -0\ast2^7+1\ast2^6+1\ast2^4+1\ast2^3+1\ast2^0=-0+64+16+8+1=89 \\ B2T_8([1 0100111]) = -1\ast2^7+1\ast2^5+1\ast2^2+1\ast2^1+1\ast2^0=-128+32+4+2+1=-89 B2T8([01011001])=−0∗27+1∗26+1∗24+1∗23+1∗20=−0+64+16+8+1=89B2T8([10100111])=−1∗27+1∗25+1∗22+1∗21+1∗20=−128+32+4+2+1=−89
让我们再来基于 B 2 T w B2T_w B2Tw 展开式,重新推导一下 w w w 位补码所能表示的取值范围。
- 最小值是位向量 [ 10 ⋯ 0 ] [10\cdots0] [10⋯0],只设置负权,其他正权位清零,此种情形负得最多,其整数值为 T M i n w = ˙ − 2 w − 1 TMin_{w}\dot=-2^{w-1} TMinw=˙−2w−1。
- 当设置了负权时,设置其他所有正权位,位向量为 [ 11 ⋯ 1 ] [11\cdots1] [11⋯1]。此种情形负得最少,其整数值为 − 2 w − 1 + ∑ i = 0 w − 2 x i ∗ 2 i = − 2 w − 1 + ( 2 w − 1 − 1 ) = − 1 -2^{w-1}+\sum_{i=0}^{w-2}x_i\ast2^i=-2^{w-1}+(2^{w-1}-1)=-1 −2w−1+∑i=0w−2xi∗2i=−2w−1+(2w−1−1)=−1,即最大负整数。
- 最大值是位向量 [ 01 ⋯ 1 ] [01\cdots1] [01⋯1],清除负权(即为非负数),清除其他所有正权位,其值为0;若设置其他所有正权位,其整数值为 T M a x w = ˙ ∑ i = 0 w − 2 x i ∗ 2 i = 2 w − 1 − 1 TMax_{w}\dot=\sum_{i=0}^{w-2}x_i\ast2^i=2^{w-1}-1 TMaxw=˙∑i=0w−2xi∗2i=2w−1−1。
以 w = 8 w=8 w=8为例,一个字节(byte)的补码编码所能表示数值范围是 [ − 2 8 − 1 , 2 8 − 1 − 1 ] [-2^{8-1}, 2^{8-1}-1] [−28−1,28−1−1],即 [ − 128 , 127 ] [-128, 127] [−128,127],包含128个负数(-128 - -1)、128个非负数(0 - 127)。
补码加法
计算机的表示法使用有限数量的位对一个数字编码,当结果太大以至不能表示时,某些运算就会溢出(overflow)。因此,我们说计算机运算也是一种有模运算。当然,在计算机中不是像上述时钟例子那样以12为模,在定点小数的补码表示中是以 2 2 2 为模,在定点整数中则以 2 n 2^n 2n 为模(n=8,16,32,64,…)。
计算机中用补码表示法编码有符号数,把负数用补码表示。减去一个正数可以看成加上一个负数,这样在计算机中不用单独设置减法器,而是基于补码一律按加法运算规则实现减法的等效计算。
回想调拨钟表的例子,2和10的和恰好等于模数12,我们把10称为-2对于mod12的补数。其含义是在钟表盘上,逆时针回拨2格等效于顺时针拨动10格。
【例1】已知X=+0000111(7),Y=-0010011(-19),求两数的补码之和。
- [ X ] 补 = 00000111 , [ Y ] 补 = 11101101 [X]_补=0 0000111,[Y]_补=1 1101101 [X]补=00000111,[Y]补=11101101,人工计算 Z=X+Y=7+(-19)=-12(补码为11110100)。
若将 [ Y ] 补 → \overrightarrow{[Y]_补} [Y]补 也视作无符号数=237,将计算结果Z也直视为无符号数 [ Z ] 补 → \overrightarrow{[Z]_补} [Z]补=244,满足 7+237=244。244恰为-12对于mod256的补数。
设想一个有256个刻度的大笨钟,现在在刻度7处,逆时针回拨19格和顺时针拨动237格,效果都是拨到刻度244处。当采用补码表示时,表盘的256个刻度被划分成左边逆时针半盘[-128,-1]和右边顺时针半盘[0,127],原先的244刻度点映射为新的逆时针-12点。
在钟表盘刻度范围内,逆时针的减法和顺时针的加法,最终达到同一刻度点。在模数(例如mod256)范围内,减法可以视作加补码(计算反码本身已经做了减法运算),补码可以视作无符号数直接参与竖式按位加计算。另一方面,由于操作数和运算结果都统一用补码表示,补码的最高符号位统一按照负权解释,即补码的符号位可视作整体数值的一部分。从这个角度讲,符号位直接参与运算貌似也是解释得通。
接下来,我们重点看看补码加法的溢出问题及溢出判断。
【例2】已知X=+1000000(64),Y=+1000001(65),求两数的补码之和。
- 直接对补码列竖式计算,两个正数相加,结果为负数!?补码之和是129,超出了 [0, 127],即产生了溢出现象。此时,数值部分向符号位产生进位 C 0 = 1 C_0=1 C0=1,符号位未向高位产生进位 C f = 0 C_f=0 Cf=0。
0 1 ( C f C 0 ) [ X ] 补 0 1000000 ( + 64 的补码 ) + ) [ Y ] 补 0 1000001 ( + 65 的补码 ) 1 0000001 129 \begin{array}{c|lcr} \:\:\:\:\: & \: \quad 0\ 1 \qquad\qquad\qquad (C_fC_0)\\ \:\:\:\:\: [X]_补 & \qquad 0\ 1000000 \qquad (+64的补码) \\ +) [Y]_补 & \qquad 0\ 1000001 \qquad (+65的补码) \\ \hline \ & \qquad 1\ 0000001 \qquad 129 \end{array} [X]补+)[Y]补 0 1(CfC0)0 1000000(+64的补码)0 1000001(+65的补码)1 0000001129
用以下C语言代码测试验证,计算结果Z的位向量为0x81(即无符号数129),补码对应的真值=-127。
signed char X = 64;
signed char Y = 65;
signed char Z = X+Y;
printf("Z=0x%hhx, %hhd\n", Z, Z);
【例3】已知X=-1111111(-127),Y=-0000010(-2),要求进行补码的加法运算。
- 直接对补码列竖式计算,两个负数相加,结果为正数!?预期的结果-129超出了8位补码所能表示的负数范围 [-128, -1],即产生了溢出现象。此时,数值部分向符号位未产生进位 C 0 = 0 C_0=0 C0=0,符号位向高位产生进位 C f = 1 C_f=1 Cf=1。
1 0 ( C f C 0 ) [ X ] 补 1 0000001 ( − 127 的补码 ) + ) [ Y ] 补 1 1111110 ( − 2 的补码: 254 ) 0 1111111 127 \begin{array}{c|lcr} \:\:\:\:\: & \: \quad 1\ 0 \qquad\qquad\qquad (C_fC_0)\\ \:\:\:\:\: [X]_补 & \qquad 1\ 0000001 \qquad (-127的补码) \\ +) [Y]_补 & \qquad 1\ 1111110 \qquad (-2的补码:254) \\ \hline \ & \qquad 0\ 1111111 \qquad 127 \end{array} [X]补+)[Y]补 1 0(CfC0)1 0000001(−127的补码)1 1111110(−2的补码:254)0 1111111127
用以下C语言代码测试验证,计算结果Z的位向量为0x7f,符号位溢出为0,补码对应的真值即无符号值127。
signed char X = -127;
signed char Y = -2;
signed char Z = X+Y;
printf("Z=0x%hhx, %hhd\n", Z, Z);
【例4】已知X=-0000011(-3),Y=-0000010(-2),要求进行补码的加法运算。
- 直接对补码列竖式计算,直接补码运算产生的结果的无符号数值是251,正是预期结果-5的补码!此时,数值部分向符号位产生进位 C 0 = 1 C_0=1 C0=1,符号位向高位产生进位 C f = 1 C_f=1 Cf=1。
1 1 ( C f C 0 ) [ X ] 补 1 1111101 ( − 3 的补码: 253 ) + ) [ Y ] 补 1 1111110 ( − 2 的补码: 254 ) 1 1111011 ( − 5 的补码: 251 ) \begin{array}{c|lcr} \:\:\:\:\: & \: \quad 1\ 1 \qquad\qquad\qquad (C_fC_0)\\ \:\:\:\:\: [X]_补 & \qquad 1\ 1111101 \qquad (-3的补码:253) \\ +) [Y]_补 & \qquad 1\ 1111110 \qquad (-2的补码:254) \\ \hline \ & \qquad 1\ 1111011 \qquad (-5的补码:251) \end{array} [X]补+)[Y]补 1 1(CfC0)1 1111101(−3的补码:253)1 1111110(−2的补码:254)1 1111011(−5的补码:251)
以上例1和例4中,8位正数之和、8位负数之和未超出8位补码表示的数值范围时,没有产生溢出,结果正确。此时,满足
C
0
=
C
f
C_0=C_f
C0=Cf,同时为0或同时为1。
以上例2和例3中,8位正数之和、8位负数之和超出8位补码表示的数值范围时,产生了溢出,结果错误。此时,满足
C
0
≠
C
f
C_0 \ne C_f
C0=Cf,一个为0且一个为1。
综合以上,可用下列逻辑表达式进行补码加法的溢出判断:
O F = C f ⊕ C 0 OF = C_f \oplus C_0 OF=Cf⊕C0
扩展一个数字的位表示
一个常见的运算是在不同字长的整数之间转换,同时又保持数值不变。考虑从一个较小的数据类型转换到一个较大的类型,即扩大位宽。
要将一个无符号数转换成一个更大的数据类型时,只要简单地在开头添加0。具体来说,原数值的字节保持在低(权)位不变,在新增的高(权)位补0。
要将一个补码数字转换为一个更大的数据类型,可以执行一个符号扩展(sign extension)。具体来说,原数值的字节保持在低(权)位不变,在新增的高(权)位复制符号位。
- 对于正数,高位复制符号位0,不影响其真值。
- 对于负数,高位复制符号位1,也不影响真值。
考虑负数的补码位向量 x ⃗ = [ x w − 1 , x w − 2 , ⋯ , x 0 ] \vec{x} = [x_{w-1}, x_{w-2}, \cdots, x_0] x=[xw−1,xw−2,⋯,x0],其编码映射如下:
B 2 T w ( x ⃗ ) = ˙ − x w − 1 ∗ 2 w − 1 + ∑ i = 0 w − 2 x i ∗ 2 i B2T_w(\vec{x}) \dot=-x_{w-1}\ast2^{w-1} + \sum_{i=0}^{w-2}x_i\ast2^i B2Tw(x)=˙−xw−1∗2w−1+i=0∑w−2xi∗2i
当复制高符号位 x w − 1 x_{w-1} xw−1 时,位向量 x ′ ⃗ = [ x w − 1 , ⋯ , x w − 1 , x w − 1 , x w − 2 , ⋯ , x 0 ] \vec{x'} = [x_{w-1}, \cdots, x_{w-1}, x_{w-1}, x_{w-2}, \cdots, x_0] x′=[xw−1,⋯,xw−1,xw−1,xw−2,⋯,x0],则有 B 2 T w ( x ⃗ ) = B 2 T w ′ ( x ′ ⃗ ) B2T_w(\vec{x}) = B2T_{w'}(\vec{x'}) B2Tw(x)=B2Tw′(x′)。
以-89为例,其8位补码位向量为 [ 10100111 ] [1 0100111] [10100111],将其符号扩展为16位的位向量位 [ 1111111110100111 ] [11111111 10100111] [1111111110100111]:
B 2 T 8 ( [ 10100111 ] ) = − 1 ∗ 2 7 + 1 ∗ 2 5 + 1 ∗ 2 2 + 1 ∗ 2 1 + 1 ∗ 2 0 = − 128 + 32 + 4 + 2 + 1 = − 89 B 2 T 16 ( [ 1111111110100111 ] ) = − 1 ∗ 2 15 + 1 ∗ 2 14 + ⋯ + 1 ∗ 2 8 + 1 ∗ 2 7 + 1 ∗ 2 5 + 1 ∗ 2 2 + 1 ∗ 2 1 + 1 ∗ 2 0 = − 128 + 32 + 4 + 2 + 1 = − 89 \begin{aligned} & B2T_8([1 0100111]) = -1\ast2^7+1\ast2^5+1\ast2^2+1\ast2^1+1\ast2^0=-128+32+4+2+1=-89 \\ & B2T_{16}([11111111 10100111]) =-1\ast2^{15}+1\ast2^{14}+\cdots+1\ast2^8+1\ast2^7+1\ast2^5+1\ast2^2+1\ast2^1+1\ast2^0=-128+32+4+2+1=-89 \end{aligned} B2T8([10100111])=−1∗27+1∗25+1∗22+1∗21+1∗20=−128+32+4+2+1=−89B2T16([1111111110100111])=−1∗215+1∗214+⋯+1∗28+1∗27+1∗25+1∗22+1∗21+1∗20=−128+32+4+2+1=−89
我们设补码除符号位后的数值位值为v,则
B 2 T 8 ( [ 10100111 ] ) = − 1 ∗ 2 7 + v B 2 T 16 ( [ 1111111110100111 ] ) = − 1 ∗ 2 15 + 1 ∗ 2 14 + ⋯ + 1 ∗ 2 8 + 1 ∗ 2 7 + v \begin{aligned} & B2T_8([1 0100111]) =-1\ast2^7+v \\ & B2T_{16}([11111111 10100111]) =-1\ast2^{15}+1\ast2^{14}+\cdots+1\ast2^8+1\ast2^7+v \end{aligned} B2T8([10100111])=−1∗27+vB2T16([1111111110100111])=−1∗215+1∗214+⋯+1∗28+1∗27+v
更进一步有:
B 2 T 16 ( [ 1111111110100111 ] ) = 2 8 ( − 2 7 + 2 6 + ⋯ + 2 0 ) + 2 7 + v = 2 8 ( − 2 7 + ( 2 7 − 1 ) ) + 2 7 + v = − 2 8 + 2 7 + v = − 2 7 + v \begin{aligned} B2T_{16}([11111111 10100111]) & = 2^8(-2^7+2^6+\cdots+2^0)+2^7+v \\ & = 2^8(-2^7+(2^7-1))+2^7+v \\ & = -2^8+2^7+v \\ & = -2^7+v \end{aligned} B2T16([1111111110100111])=28(−27+26+⋯+20)+27+v=28(−27+(27−1))+27+v=−28+27+v=−27+v
对比可知,符号扩展前后的补码所表示的真值不变。
有符号数和无符号数之间的转换
C语言允许在各种不同的数据类型之间做强制类型转换。例如,假设变量x声明为int,u声明为unsigned。表达式 (unsigned)x
会将x的值转换为一个无符号数值,而 (int)u
将u的值转换为一个有符号整数。
考虑以下代码:
short int v = -12345;
unsigned short uv = (unsigned short)v;
printf("v=%hd, uv=%hu\n", v, uv);
printf("v=0x%hx, uv=0x%hx\n", v, uv);
在一台采用补码的机器上,上述代码运行输出:
v=-12345, uv=53191
v=0xcfc7, uv=0xcfc7
可以看到,强制类型转换的结果保持位向量(位模组)不变,只是改变了解释这些位的方式。
v=-12345 的补码表示和 uv=53191 的无符号表示是完全一样的。
以上代码片段示例演示了负数补码转换成无符号数。对于负数,补码中最高位作为符号位,本来解释为负权 − 2 w − 1 -2^{w-1} −2w−1,按照无符号解释为正权数值位 2 w − 1 2^{w-1} 2w−1。由于后面位的数值 v v v 保持不变,相当于在补码表示的真值基础上加上 2 w 2^w 2w。
对满足 T M i n w ≤ x ≤ T M a x w TMin_w \le x \le TMax_w TMinw≤x≤TMaxw 的 x x x 有:
T 2 U w ( x ) = { x , x ≥ 0 2 w + x ( 2 w − ∣ x ∣ ) , x < 0 T2U_w(x)= \begin{cases} x, & x\ge0 \\ 2^w+x(2^w- \lvert x \rvert), & x<0 \end{cases} T2Uw(x)={x,2w+x(2w−∣x∣),x≥0x<0
简单推导如下:
B 2 U w ( x ) − B 2 T w ( x ) = ( 2 w − 1 + v ) − ( − 2 w − 1 + v ) = 2 ∗ 2 w − 1 = 2 w ⟹ B 2 U w ( x ) = B 2 T w ( x ) + 2 w \begin{aligned} B2U_w(x) - B2T_w(x) & = (2^{w-1}+v)-(-2^{w-1}+v) = 2\ast2^{w-1}=2^w \\ & \implies B2U_w(x) = B2T_w(x) + 2^w \end{aligned} B2Uw(x)−B2Tw(x)=(2w−1+v)−(−2w−1+v)=2∗2w−1=2w⟹B2Uw(x)=B2Tw(x)+2w
例如: T 2 U 16 ( − 12345 ) = − 12345 + 2 16 = 53191 T2U_{16}(-12345) = -12345+2^{16}=53191 T2U16(−12345)=−12345+216=53191。
同理,无符号数的最高位解释为正权数值位 2 w − 1 2^{w-1} 2w−1,当将其位向量解释成补码时,最高位将作为符号位,解释为负权 − 2 w − 1 -2^{w-1} −2w−1。由于后面位的数值 v v v 保持不变,相当于在无符号真值基础上减去 2 w 2^w 2w。
对满足 0 ≤ u ≤ U M a x w 0 \le u \le UMax_w 0≤u≤UMaxw 的 u u u 有:
U 2 T w ( u ) = { u , u ≤ T M a x w u − 2 w , u > T M a x w U2T_w(u)= \begin{cases} u, & u \le TMax_w \\ u - 2^w, & u>TMax_w \end{cases} U2Tw(u)={u,u−2w,u≤TMaxwu>TMaxw
对于 w = 8 w=8 w=8,当 u ≤ T M a x w = 127 u \le TMax_w = 127 u≤TMaxw=127 时,即 0 ≤ u ≤ 127 0 \le u \le 127 0≤u≤127,刚好落入8位补码所能表示的非负数区间 ;当 u > T M a x w = 127 u \gt TMax_w = 127 u>TMaxw=127 时,例如 128 ≤ u ≤ 255 128 \le u \le 255 128≤u≤255,则原始高位1变成负号,补码释义为负数。原来位向量按无符号解释出的真值需减去 2 8 = 256 2^8=256 28=256,换算出按补码解释的正确真值,对应负数区间 [ − 1 , − 128 ] [-1, -128] [−1,−128]。
我们在数轴上把有符号数和无符号数画出来的话,就能很清晰的看出相对的关系:
参考
《深入理解计算机系统》(第3版)
《计算机组成原理》(第3版),唐朔飞
《微机原理与接口技术》(第2版),王克义
《汇编语言程序设计》(第4版),文全刚
深入谈谈二进制
补码表示法的本质原理
【读薄 CSAPP】壹 数据表示
从晶体管开始聊聊计算机为什么采用二进制