limits.h
的不足
通过库 limits.h
中的常量,我们可以得知绝大多数整型的范围。但是其中并没有 long long
类型的取值范围。在 C++ 的库 climits
中定义了常量 LLONG_MIN, LLONG_MAX, ULLONG_MAX
,但是这并不在 C 语言的范畴中。我们希望有一种方法,可以得到 C 语言中任意整型数据的取值范围。
根据编码的原理的不同,有符号类型和无符号类型要分开讨论。我们先讨论有符号整型。
有符号整型的编码
假设用 n n n 位(以下例子中 n = 8 n=8 n=8)来存储一个整数,用最高位作为符号位,那么这个整数的取值范围应当是 ( − 2 n − 1 , 2 n − 1 ) (-2^{n-1},2^{n-1}) (−2n−1,2n−1) 考虑到 0 0 0,实际取值范围是 ( − 2 n − 1 , 2 n − 1 − 1 ) (-2^{n-1},2^{n-1}-1) (−2n−1,2n−1−1)
正数的编码就是其本身的二进制数,所以 2 7 − 1 2^7-1 27−1的二进制数就是 01111111,而负数采用补码的方式存储,是其原码非符号位取反加一。例如 − 1 -1 −1 的原码为 10000001,反码为11111110,补码为 1111111
则 − 2 7 + 1 -2^7+1 −27+1 的原码为 1111111,反码为 10000000,补码为 1000001。整数每-1,原码就+1,反码就-1,补码也-1。
这样又体现了补码的好处。因为按照原码, 2 n − 1 2^{n-1} 2n−1 是无法表示的,那么 − 2 n − 1 -2^{n-1} −2n−1 也就无法表示。某种角度上说也是因为原码有 − 0 -0 −0,即就是 10000000 的存在。相应的反码也有这样的问题,因为在反码中 11111111 被 − 0 -0 −0 所使用,导致 − 2 7 + 1 -2^7+1 −27+1 就已经到了 10000000 10000000 10000000。但是在补码中,我们有 10000001-1=10000000,用 10000000 来表示 − 2 7 -2^7 −27 就十分的平凡了。
计算方法
也就是说,8位整型的最小值为10000000,最大值为01111111。通过二进制运算 1<<7
和 ~(1<<7)
我们可以分别得到这两个数。
将这种方法拓展,则有符号整型的最小值和最大值分别为 1<<sizeof(type) - 1,~(1<<sizeof(type) - 1)
需要注意的是,有时 long 和 long long 的位数高于 int。我们不妨假设分别为16、32、64位。1 默认是一个 int 类型的数,在 1<<31
时会溢出而丢弃最高位,得到0的结果。所以我们应当使用 1L<<sizeof(long) - 1
和 1LL<<sizeof(long long) - 1
的方法。
但是 C 总是充满了奇奇怪怪的特例。
需要注意的地方
以 int 传递 short
首先要说的就是在传参的时候,short 类型会以 int 类型来传递。这可能是因为 int 往往是一个计算机的字长,具有更高的传输效率。例子如下:
short int b = 1;
printf("%hd, %d\n", b<<16, b<<16);
printf("%d, %lld\n", 1<<32, 1<<32);
// 输出结果:
// 0, 65536
// 0, 0
这是因为 b<<16
实际上是作为一个int被传输,就使得其并没有产生之前的溢出归零的问题。
取模位移
另一个特例是,如果变量向左移位多于存储位数,会对存储位数取模然后移位。但是常量和short不会。如下:
short a = 1;
int b = 1;
long c = 1;
long long d = 1;
printf("%hd\n", a<<18);
printf("%d, %d\n", 1<<34, b<<34);
printf("%ld, %ld\n", 1L<<34, c<<34);
printf("%lld, %lld\n", 1LL<<66, d<<66);
// 输出结果:
// 0
// 0, 4
// 0, 4
// 0, 4
并且short不取模,不是因为它作为 printf()
的参数以int被传输,而是它真的不会取模。如下所示:
short a = 1, aa = 3;
aa = a<<18;
printf("%hd, %hd\n", a<<18, aa);
// 输出结果:
// 0, 0