13

C语言signed char型变量为什么能够表示的范围是-128到127,而不是-127到128?

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80signed-char%E5%9E%8B%E5%8F%98%E9%87%8F%E4%B8%BA%E4%BB%80%E4%B9%88%E8%83%BD%E5%A4%9F%E8%A1%A8%E7%A4%BA%E7%9A%84%E8%8C%83%E5%9B%B4%E6%98%AF-128%E5%88%B0127-%E8%80%8C%E4%B8%8D/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

C语言signed char型变量为什么能够表示的范围是-128到127,而不是-127到128?

发表于 2019-08-15 19:08:50   |   已被 访问: 525 次   |   分类于:   C语言 , 杂谈   |   暂无评论

初学者在学习C语言,谈到不同数据类型时,一般都能理解 unsigned 和 signed 的区别,无非就是有无符号而已。但是对于 signed 数据类型的数据范围,初学者却常常会感到迷惑。

例如提到 char 型能够表示的数据范围时,unsigned char 类型很好理解,所有的 8 位全部用于表示数值,因此 unsigned char 类型能够表示 2^8 = 256 个数,例如 0 到 255。

对于 signed char 类型,最高一位表示符号(+/-),能够用于表示数值的是低 7 位。因此按理说,signed char 类型能够表示的数值范围为 -0b111111到 +0b1111111,也即 -127 到 127。

但是很多教科书上却说 signed char 类型能够表示的数值范围为 -128 到 127,类似的还有 signed short 类型能够表示的数值范围为 -32768 到 32767,signed int 类型能够表示的数值范围为 -2147483648 到 2147483647,为什么有符号的整数类型,负数部分总是比整数部分多出一个数呢

这里假设 short 类型占用 2 字节内存空间,int 类型占用 4 字节内存空间。

在C语言中,char 型变量占用 8 个位,对于 signed char 类型,最高位表示符号位,此时有 7 个位用于表示数值。按照数学中的排列组合,7 个位能够表示 2^7 也即 128 个不同的数,若考虑正负号,signed char 类型最多也能表示 2*128=256 个不同的数。

但是,如果 signed char 类型能够表示的数值范围是 -0b111111到 +0b1111111(-127 到 127),那么能够表示的只有 255 个不同的数字了,与理论最大能够表示的不同数字数 256 相比,少了一个,这是因为 -0 和 +0 其实是同一个数字,也即 0b10000000 和 0b00000000 是同一个数字 0。

这对于计算机来说很不友好,同样的一个数字有两种二进制码,在处理时会显得很麻烦。这种麻烦对于 CPU 的电路设计,同样如此。

谈到 CPU 的电路设计,我们还应该明白,固定的面积上能够容纳的电路单元数是固定的。如果 CPU 只需处理加法运算,那么设计师就只需设计加法电路,这样既简单,又能最大程度的保障计算力。

可是在实际应用中,不可避免的需要减法运算,再考虑到 +/- 0 导致的数字二进制码重复问题,计算机中“补码”的概念被提出了。

关于“补码”的定义就不写了,感兴趣的读者可以自行百科。这里仅简要的说下C语言中补码是如何获得的,其实很简单:正数和 0 的补码等于自身,负数的补码则是将其对应正数按位取反再加1。

例如正数 0b00000011(3) 的补码等于其自身,仍然为 0b00000011,而 0b10000001(-1)的补码等于 0b11111110 + 1 也即 0b11111111(0xFF)。

补码的最大优点是可以在加法或减法处理中,不需因为数字的正负而使用不同的计算方式。只要一种加法电路就可以处理各种有符号数加法(减法可以用一个数加上另一个数的补码来表示),因此只要有加法电路及补码电路即可完成各种有符号数加法及减法,在电路设计上相当方便。

另外,补码下的 0 就只有一个表示方式,因此在判断数字是否为 0 时,只要比较一次即可。

简单来说,数字 a(正负数皆可)的补码即为 -a。

为什么 signed char 型能够表示的数值范围为 -128~127

因为 -0 和 +0 其实是同一个数字,因此原码中 0b10000000 和 0b00000000 都表示数字 0。现在补码下的 0 只有一个表示方式:0b00000000,二进制码 0b10000000 就多余出来了。

浪费是可耻的,多出的二进制码 0b10000000 不能白白丢弃。若考虑数字 0 的二进制码 0b00000000,从它的符号位来看,计算机应该是将其当做“正数”的,0~127 是 128 个“正数”。

现在考察多出的二进制码 0b10000000,从它的符号位来看,把它当做负数是合情合理的,事实上在C语言中,它表示-128,从 -128 到 -1,恰好是 128 个“负数”。

在C语言中,signed char 型二进制码 0b10000000 的补码仍然为 0b10000000,因此它是“数字a的补码为 -a”原则的例外。

下表是一些 char 类型整数的补码,可表示的范围为 -128 到 127,总共 256 个不同整数。

03ab11ab6be1ce7e849b15de8cf75041.png

延伸:为什么补码能这么巧妙实现了正负数的加减运算?

答案是:指定 n 位字长,就有 2^n 个可能的值,加减法运算都存在上溢出与下溢出的情况,实际上都等价于模 2^n 的加减法运算。这对于 n 位无符号整数类型或是 n 位有符号整数类型都同样适用。

例如,8 位无符号整数的值的范围是 0 到 255。因此 4+254 将上溢出,结果是 2,即

4+254 = 258 = 2 (mod 256)

8 位有符号整数的值的范围,如果规定为−128到127,则 126+125 将上溢出,结果是−5,即

126+125 = 251 = -5(mod 256)

对于 8 位字长的有符号整数类型,以 2^8 即 256 为模,则

-128 = 128 (mod 256)
-127 = 129 (mod 256)
...
-2 = 254 (mod 256)
-1 = 255 (mod 256)

所以模 256 下的加减法,用 0, 1, 2,…, 254,255 表示其值,或者用 −128, −127,…, −1, 0, 1, 2,…,127 是完全等价的。−128与128,−127与129,…,−2与254,−1与255 可以互换而加减法的结果不变,需要的 CPU 加法运算器的电路实现与 8 位无符号整数并无不同。

实际上对于 8 位的存储单元,把它的取值 [00000000,…, 11111111] 解释为 [0, 255],或者 [-1, 254],或者[-2, 253],或者[-128, 127],或者[-200, 55],甚至[500, 755],对于加法硬件实现并无不同,都是一样的。

本节主要讨论了C语言中 signed char 型变量能够表示的数值范围,一般认为其能够表示 -128~127 的整数,而不是 0b11111111(-127)到 0b01111111(127),这也是初学者常常感到迷惑的地方。其实简单来看,计算机的设计讨厌一切“浪费”,数字 0 用两个二进制码(+/-0)表示就是一种浪费,处理起来也比较麻烦。倒不如只为 0 保留一种二进制码 0b00000000,而将多出的二进制码 0b10000000 用于表示 -128 方便了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK