本节书摘来自华章计算机《C语言编程魔法书:基于C11标准》一书中的第2章,第2.2节,作者: 陈轶 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
2.2 整数在计算机中的表示
我们日常用的整数都是十进制数(Decimal),也就是我们通常所说的逢十进一。因为我们人类有十根手指,所以自然而然地会想到采用十进制的计数和计算方式。然而,现在几乎所有计算机都采用二进制数(Binary)编码方式,所以我们日常所用到的整数如果要用计算机来表示的话,需要表示成二进制的方式。
二进制数则是逢二进一,所以在整串数中只有0和1两种数字。比如,十进制数0,对应二进制为0;十进制数1,对应二进制数1;十进制数2,对应二进制数10;十进制数3,对应二进制数11。因此,对于非负整数而言,二进制数第n位(n从0开始计)如果是1,那么就对应十进制数的2n,然后每个位计算得到的十进制数再依次相加得到最终十进制数的值。比如,一个5位二进制数10010,最低位为最右边的位,记为0号位,数值为0;最高位为最左边的位,记为4号位,数值为1。那么它所对应的十进制数为:24+21=18。因为该二进制数除了4号位和1号位为1之外,其余位都是0,因此0乘以2n肯定为0。图2-3为二进制数10010换算成十进制数的方法图。
https://yqfile.alicdn.com/19e8f1a80cfa8446aa4df1cd5aeaa5536c8be64b.png" >
在计算机术语中,把二进制数中的某一位数又称为一个比特(bit)。比特这个单位对于计算机而言,在度量上是最小的单位。除了比特之外,还有字节(byte)这个术语。一个字节由8个比特构成。在某些单片机架构下还引入了半字节(nybble或nibble)这个概念,表示4个比特。然后,还有字(word)这个术语。字在不同计算机架构下表示的含义不同。在x86架构下,一个字为2个字节;而在ARM等众多32位RISC体系结构下,一个字表示为4个字节。随着计算机带宽的提升,能被处理器一次处理的数据宽度也不断提升,因此出现了双字(double word)、四字(quad word)、八字(octa word)等概念。双字的宽度为2个字,四字宽度为4个字,所以它们在不同处理器体系结构下所占用的字节个数也会不同。
我们上面介绍了非负整数的二进制表达方法,那么对于负数,二进制又该如何表达呢?在计算机中有原码和补码两种表示方法,而最为常用的是补码的表示方法。下面我们分别对原码和补码进行介绍。
2.2.1 原码表示法
对于无正负符号的原码,其二进制表达如上节所述。而对于含有正负符号的原码,其二进制表示含有一位符号位,用于表示正负号。一般都是以二进制数的最高有效位(即最左边的比特)作为符号位,其余各位比特表示该数的绝对值大小。比如,十进制数6用一个8位的原码表示为0000 0110;如果是-6,则表示为1000 0110。二进制的原码表示示例如图2-4所示。
原码的表示非常直观,但是对于计算机算术运算而言就带来了许多麻烦。比如,我们用上述的6与-6相加,即0000 0110+1000 0110,结果为1000 1100,也就是十进制数-12,显然不是我们想要的结果。所以,如果某个处理器用原码表示二进制数,那么它参与加减法的时候必须对两个操作数的正负符号加以判断,然后再判定使用加法操作还是减法操作,最后还要判定结果的正负符号,可谓相当麻烦。所以,当前计算机的处理器往往采用补码的方式来表达带符号的二进制数。
**2.2.2 补码表示法
**
正由于原码含有上述缺点,所以人们开发出了另一种带符号的二进制码表示法——补码。补码与原码一样,用最高位比特表示符号位,其余各位比特则表示数值大小。如果符号位为0,说明整个二进制数为正数或零;如果为1,那么表示整个二进制数为负数。当符号位为0时,二进制补码表示法与原码一模一样,但是当符号位为负数时,情况就完全不同了。此时,对二进制数的补码表示需要按以下步骤进行:
1)先将该二进制数以绝对值的原码形式写好;
2)对整个二进制数(包括符号位),每一个比特都取反。所谓取反就是说,原来一个比特的数值为0时,则要变1;为1时,则要变0。
变换好之后,将二进制数做加1计算,最终结果就是该负数的补码值了。
下面我们还是用6来举例,+6的二进制补码跟原码一样,还是0000 0110。而-6的计算过程,按照上述流程如下:
1)先将-6用绝对值+6的形式表示:0000 0110;
2)对每个比特位取反,包括符号位在内,得到:1111 1001;
3)将变换好的数做加1计算,最终得到:1111 1010。
由于二进制补码的表示与通常我们可直接读懂的二进制数的表示有很大不同,所以给定一个二进制补码,我们往往需要先获得其绝对值大小才能知道它的具体数值。获得其绝对值的过程为:先判定符号位,如果符号位为0,那么就以通常的二进制数表示法来读即可。如果符号位为1,那么就以上述同样的过程得到其对应的绝对值。比如,如果给定1111 1010这个二进制数,我们看到最高位符号位为1,说明是负数,我们就以上述过程来求解:
1)先将该二进制数每个比特做取反计算,得到:0000 0101;
2)然后将变换得到的值做加1计算,最终获得:0000 0110。
所以1111 1010的绝对值为0000 0110,即6。
对于补码表示,我们已经知道最高位比特表示符号位,其余的表示具体数值。但是这里有一个特殊情况,即符号位为1,其余位比特为都为0的情况。比如一个8位二进制补码:1000 0000,此时它的值是多少?因为我们通过上述流程,求得其绝对值的大小也是1000 0000,所以当前大部分计算机处理器的实现将它作为-128,但估计仍然有一些处理器会把它作为-0。因为C语言标准中对于数值范围的表示已经明确表示出8位带符号的整数范围可以是-128到+127,也可以是-127到+127,但最小值不得大于-127,最大值不得小于+127。第5章会有更详细的描述。
补码的这种表示法的优点就是可以无视符号位,随意进行算术运算操作。比如,像我们上面所举的例子:6+(-6),计算结果:
0000 0110+1111 1010=0000 0000
最后,上述计算结果的最高位符号位所产生的进位被丢弃(在处理器中可能会设置相应的进位标志位)。我们自己计算的话也非常方便,在计算过程中,无需关心两个二进制补码的正负数的情况,也无需关心符号位所产生的影响。我们只需要像计算普通二进制数一样去计算即可。把最终的计算结果拿出来判断,是正数还是负数。当然,二进制补码会产生溢出情况,比如两个8位二进制补码加法:
120+50=0111 1000+0011 0010=1010 1010
然而,这个数并不是170,而是-86。首先,170已经超出了带符号8位二进制数可表示的最大范围了;其次,最高位变为1,用补码表示来讲就是负数表示形式。所以,这两个正数的加法计算就产生了负数结果,这种现象称为上溢。如果我们要避免在计算过程中出现上溢情况,需要用更高位宽的二进制数来表示,以提升精度。比如,如果我们将上述加法用16位二进制数表示,那么就不会有上溢问题了。
另外,在C语言标准中没有明确规定C语言编译器的实现以及运行时环境必须采用哪种二进制编码方式,而是对整数类型标明最大可表示的数值范围。目前大部分C语言实现都是对带符号整数采用补码的表示方式。这些会在第5章做进一步讲解。
2.2.3 八进制数与十六进制数
上面我们对二进制数编码形式做了比较详细的介绍。我们在编写程序或者查看一些计算机相关的技术文档时常常还会碰到八进制数与十六进制数的表示,尤其是十六进制数用得非常多。下面我们就简单介绍一下这两种基数(radix)的表示方法。
这里跟各位再分享一个术语——基数。基数也就是我们通常所说的,某一个数用多少进制表达。对于像“01001000是几进制数”这种话,如果用更专业的表达方式来说的话就是,“01001000的基数是几”。基数为2就是二进制;基数为10则是十进制。
八进制数是逢八进一,因此每位数的范围是从0~7。八进制数转十进制数也很简单,我们可以用二进制数转十进制数类似的方法来炮制八进制数转十进制数——以一个八进制数每位数值作为系数,然后乘以8n,然后计算得到的结果全都相加,最后得到相应的十进制数。其中,n表示当前该位所对应的位置索引(同样以0开始计)。比如,八进制数5271对应的十进制数的计算过程如图2-5所示。
八进制数对应于二进制数的话正好占用3个比特(范围从000~111),一般在通信领域以及信息加密等领域会用到八进制编码方式。而十六进制数比八进制数用得更多,因为十六进制数正好占用4个比特,即4位二进制数(范围从0000~1111)。4个比特相当于半个字节。所以,无论是开发工具还是程序调试工具,一般都会用十六进制数来表示计算机内部的二进制数据,这样更易读,而且也更省显示空间(因为一个字节原本需要8位二进制数,而十六进制数只要两位即可表示)。下面就介绍一下十六机制数的表示方法。
十六进制数逢十六进一,因此每一位数的范围是从0到15。由于我们通常在数学上所用的十进制数无法用一位来表示10~15这6个数,因而在计算机领域中,我们通常用英文字母A(或小写a)来表示10;B(或小写b)来表示11;C(或小写c)来表示12;D(或小写d)来表示13;E(或小写e)来表示14;F(或小写f)来表示15。十六机制数转十进制数的方式与八进制数转十进制数类似——以一个十六进制数每位数值作为系数,然后乘以16n,然后计算得到的结果全都相加,最后得到相应的十进制数。其中,n表示当前位所对应的位置索引(同样以0开始计)。比如,一个4位十六进制数C0DE的计算过程如图2-6所示:
上述4位十六进制数C0DE,倘若用二进制数表示,则为:1100 0000 1101 1110。可见,用十六进制数表示要简洁得多,而且换算成十进制数也相对比较容易,尤其对于一个字节长度的整数来说。为了能更快速地换算二进制数、十进制数与十六进制数,请各位读者务必熟记下表:
习惯上,用0或0o打头的数表示八进制数,0x打头的数表示十六进制数。比如,0123、0777表示八进制数;0x123,0xABCD表示十六进制数。