short、int、long 是C语言中常用的三种整数类型,分别称为短整型、整型、长整型。
在现代操作系统中,short、int、long 的长度分别是 2、4、4 或者 8,它们只能存储有限的数值,当数值过大或者过小时,超出的部分会被直接截掉,数值就不能正确存储了,我们将这种现象称为溢出(Overflow)。
溢出的简单理解就是,向木桶里面倒入了过量的水,木桶盛不了了,水就流出来了。
要想知道数值什么时候溢出,就得先知道各种整数类型的取值范围。
计算无符号数(unsigned 类型)的取值范围(或者说最大值和最小值)很容易,将内存中的所有位(Bit)都置为 1 就是最大值,都置为 0 就是最小值。
以 unsigned char 类型为例,它的长度是 1,占用 8 位的内存,所有位都置为 1 时,它的值为 28 - 1 = 255,所有位都置为 0 时,它的值很显然为 0。由此可得,unsigned char 类型的取值范围是 0~255。
前面我们讲到,char 是一个字符类型,是用来存放字符的,但是它同时也是一个整数类型,也可以用来存放整数,请大家暂时先记住这一点,更多细节我们将在《C语言中的字符(char)》一节中介绍。
有读者可能会对 unsigned char 的最大值有疑问,究竟是怎么计算出来的呢?下面我就讲解一下这个小技巧。
将 unsigned char 的所有位都置为 1,它在内存中的表示形式为1111 1111,最直接的计算方法就是:
这种“按部就班”的计算方法虽然有效,但是比较麻烦,如果是 8 个字节的 long 类型,那足够你计算半个小时的了。
我们不妨换一种思路,先给 1111 1111 加上 1,然后再减去 1,这样一增一减正好抵消掉,不会影响最终的值。
给 1111 1111 加上 1 的计算过程为:
可以发现,1111 1111 加上 1 后需要向前进位(向第 9 位进位),剩下的 8 位都变成了 0,这样一来,只有第 9 位会影响到数值的计算,剩下的 8 位对数值都没有影响。第 9 位的权值计算起来非常容易,就是:
然后再减去 1:
加上 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 从原码转换到补码的过程中,符号位被 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 类型的补码为什么对应的数值是 -231,有了本节对 char 类型的分析,相信聪明的你会举一反三,自己解开这个谜团。
#include <stdio.h>
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 的原码为:
这也是 b 在内存中的存储形式。
当 printf 读取到 b 时,由于最高位是 1,所以会被判定为负数,要从补码转换为原码:
最终 b 的输出结果为 -1。