short、int、long 是C语言中常用的三种整数类型,分别称为短整型、整型、长整型。在现代操作系统中,short、int、long 的长度分别是 2、4、4 或者 8,它们只能存储有限的数值,当数值过大或者过小时,超出的部分会被直接截掉,数值就不能正确存储了,我们将这种现象称为溢出(Overflow)。溢出的简单理解就是,向木桶里面倒入了过量的水,木桶盛不了了,水就流出来了。要想知道数值什么时候溢出,就得先知道各种整数类型的取值范围。
无符号数的取值范围
计算无符号数(unsigned 类型)的取值范围(或者说最大值和最小值)很容易,将内存中的所有位(Bit)都置为 1 就是最大值,都置为 0 就是最小值。以 unsigned char 类型为例,它的长度是 1,占用 8 位的内存,所有位都置为 1 时,它的值为 2
8 - 1 = 255,所有位都置为 0 时,它的值很显然为 0。由此可得,unsigned char 类型的取值范围是 0~255。 前面我们讲到,char 是一个字符类型,是用来存放字符的,但是它同时也是一个整数类型,也可以用来存放整数,请大家暂时先记住这一点,更多细节我们将在《 在C语言中使用英文字符 》一节中介绍。 有读者可能会对 unsigned char 的最大值有疑问,究竟是怎么计算出来的呢?下面我就讲解一下这个小技巧。将 unsigned char 的所有位都置为 1,它在内存中的表示形式为
1111 1111
,最直接的计算方法就是: 2
0 + 2
1 + 2
2 + 2
3 + 2
4 + 2
5 + 2
6 + 2
7 = 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 = 255 这种“按部就班”的计算方法虽然有效,但是比较麻烦,如果是 8 个字节的 long 类型,那足够你计算半个小时的了。我们不妨换一种思路,先给 1111 1111 加上 1,然后再减去 1,这样一增一减正好抵消掉,不会影响最终的值。给 1111 1111 加上 1 的计算过程为: 0B1111 1111 + 0B1 = 0B1 0000 0000 = 2
8 = 256 可以发现,1111 1111 加上 1 后需要向前进位(向第 9 位进位),剩下的 8 位都变成了 0,这样一来,只有第 9 位会影响到数值的计算,剩下的 8 位对数值都没有影响。第 9 位的权值计算起来非常容易,就是: 2
9-1 = 2
8 = 256 然后再减去 1: 2
8 - 1 = 256 - 1 = 255 加上 1 是为了便于计算,减去 1 是为了还原本来的值;当内存中所有的位都是 1 时,这种“凑整”的技巧非常实用。按照这种巧妙的方法,我们可以很容易地计算出所有无符号数的取值范围(括号内为假设的长度): unsigned char unsigned short unsigned int(4字节) unsigned long(8字节) 最小值 最大值
|
|
|
|
0 |
0 |
0 |
0 |
28 - 1 = 255 |
216 - 1 = 65,535 ≈ 6.5万 |
232 - 1 = 4,294,967,295 ≈ 42亿 |
264 - 1 ≈ 1.84×1019
|
有符号数的取值范围
有符号数以补码的形式存储,计算取值范围也要从补码入手。我们以 char 类型为例,从下表中找出它的取值范围: 补码 反码 原码 值
|
|
|
|
1111 1111 |
1111 1110 |
1000 0001 |
-1 |
1111 1110 |
1111 1101 |
1000 0010 |
-2 |
1111 1101 |
1111 1100 |
1000 0011 |
-3 |
…… |
…… |
…… |
…… |
1000 0011 |
1000 0010 |
1111 1101 |
-125 |
1000 0010 |
1000 0001 |
1111 1110 |
-126 |
1000 0001 |
1000 0000 |
1111 1111 |
-127 |
1000 0000 |
-- |
-- |
-128 |
0111 1111 |
0111 1111 |
0111 1111 |
127 |
0111 1110 |
0111 1110 |
0111 1110 |
126 |
0111 1101 |
0111 1101 |
0111 1101 |
125 |
…… |
…… |
…… |
…… |
0000 0010 |
0000 0010 |
0000 0010 |
2 |
0000 0001 |
0000 0001 |
0000 0001 |
1 |
0000 0000 |
0000 0000 |
0000 0000 |
0 |
我们按照从大到小的顺序将补码罗列出来,很容易发现最大值和最小值。淡黄色背景的那一行是我要重点说明的。如果按照传统的由补码计算原码的方法,那么 1000 0000 是无法计算的,因为计算反码时要减去 1,1000 0000 需要向高位借位,而高位是符号位,不能借出去,所以这就很矛盾。是不是该把 1000 0000 作为无效的补码直接丢弃呢?然而,作为无效值就不如作为特殊值,这样还能多存储一个数字。计算机规定,1000 0000 这个特殊的补码就表示 -128。为什么偏偏是 -128 而不是其它的数字呢?首先,-128 使得 char 类型的取值范围保持连贯,中间没有“空隙”。其次,我们再按照“传统”的方法计算一下 -128 的补码:
- -128 的数值位的原码是 1000 0000,共八位,而 char 的数值位只有七位,所以最高位的 1 会覆盖符号位,数值位剩下 000 0000。最终,-128 的原码为 1000 0000。
- 接着很容易计算出反码,为 1111 1111。
- 反码转换为补码时,数值位要加上 1,变为 1000 0000,而 char 的数值位只有七位,所以最高位的 1 会再次覆盖符号位,数值位剩下 000 0000。最终求得的 -128 的补码是 1000 0000。
-128 从原码转换到补码的过程中,符号位被 1 覆盖了两次,而负数的符号位本来就是 1,被 1 覆盖多少次也不会影响到数字的符号。你看,虽然从 1000 0000 这个补码推算不出 -128,但是从 -128 却能推算出 1000 0000 这个补码,这么多么的奇妙,-128 这个特殊值选得恰到好处。负数在存储之前要先转换为补码,“从 -128 推算出补码 1000 0000”这一点非常重要,这意味着 -128 能够正确地转换为补码,或者说能够正确的存储。
关于零值和最小值
仔细观察上表可以发现,在 char 的取值范围内只有一个零值,没有
+0
和
-0
的区别,并且多存储了一个特殊值,就是 -128,这也是采用补码的另外两个小小的优势。如果直接采用原码存储,那么
0000 0000
和
1000 0000
将分别表示
+0
和
-0
,这样在取值范围内就存在两个相同的值,多此一举。另外,虽然最大值没有变,仍然是 127,但是最小值却变了,只能存储到 -127,不能存储 -128 了,因为 -128 的原码为 1000 0000,这个位置已经被
-0
占用了。按照上面的方法,我们可以计算出所有有符号数的取值范围(括号内为假设的长度): char short int(4个字节) long(8个字节) 最小值 最大值
|
|
|
|
-27 = -128 |
-215 = -32,768 ≈ -3.2万 |
-231 = -2,147,483,648 ≈ -21亿 |
-263 ≈ -9.22×1018
|
27 - 1= 127 |
215 - 1 = 32,767 ≈ 3.2万 |
231 - 1 = 2,147,483,647 ≈ 21亿 |
263 - 1≈ 9.22×1018
|
上节我们还留下了一个疑问,
[1000 0000 …… 0000 0000]补
这个 int 类型的补码为什么对应的数值是 -2
31,有了本节对 char 类型的分析,相信聪明的你会举一反三,自己解开这个谜团。
数值溢出
char、short、int、long 的长度是有限的,当数值过大或者过小时,有限的几个字节就不能表示了,就会发生溢出。发生溢出时,输出结果往往会变得奇怪,请看下面的代码:
#include int main(){ unsigned int a = 0x100000000; int b = 0xffffffff; printf("a=%u, b=%d\n", a, b); return 0;}
运行结果: a=0, b=-1变量 a 为 unsigned int 类型,长度为 4 个字节,能表示的最大值为 0xFFFFFFFF,而 0x100000000 = 0xFFFFFFFF + 1,占用33位,已超出 a 所能表示的最大值,所以发生了溢出,导致最高位的 1 被截去,剩下的 32 位都是0。也就是说,a 被存储到内存后就变成了 0,printf 从内存中读取到的也是 0。变量 b 是 int 类型的有符号数,在内存中以补码的形式存储。0xffffffff 的数值位的原码为 1111 1111 …… 1111 1111,共 32 位,而 int 类型的数值位只有 31 位,所以最高位的 1 会覆盖符号位,数值位只留下 31 个 1,所以 b 的原码为: 1111 1111 …… 1111 1111 这也是 b 在内存中的存储形式。当 printf 读取到 b 时,由于最高位是 1,所以会被判定为负数,要从补码转换为原码: [1111 1111 …… 1111 1111]
补
= [1111 1111 …… 1111 1110]
反
= [1000 0000 …… 0000 0001]
原
= -1 最终 b 的输出结果为 -1。