扭曲的INT_MIN

这个问题是在CSAPP上看到的,详情可以看这个

简单来说,问题就是:在用补码表示有符号整型的机器上,C语言中的整型的最大值和最小值的表示方法不同。接下来就以32位int为例子讲解这个问题。

int的最大值INT_MAX和int的最小值INT_MIN,是这么定义的:

1
2
3
#define INT_MAX 2147483647

#define INT_MIN (-INT_MAX – 1)

熟悉补码的童鞋应该都知道,32位的数据,能表示的最大的整数的位级表示是0x7FFFFFFF,即2147483647,最小的整数的位级表示是0x80000000,即-2147483648。那么是为什么不能直接#define INT_MIN -2147483648或者#define INT_MIN 0x80000000?

CSAPP上是这么来说的,我觉得总结的很好:

不幸的是,补码表示的不对称性和C语言转换规则之间这种奇怪的交互,迫使我们使用奇怪的方式来写Tmin。虽然理解这个问题需要我们钻研C语言标准中一些隐晦的角落,但是它也能帮助我们理解整数数据类型和表示的一些细微之处。

接下来以我自己的理解来阐述一下原因。

首先,C语言里有整数常量(integer constant),让整型数值直接在表达式中使用。比如说int a = 10这里出现的10,就是个整型常量。整型常量除了默认的十进制,可以在开头加0表示八进制,比如010就是表示的八进制数字10;还可以在开头加0x或者0X表示十六进制,比如说0xFF就表示十六进制数字FF

而且integer constant可以添加后缀。可选的后缀有u或者Ul或者Lll或者LL。带u和带l的后缀可以进行组合(而且不用分先后顺序)。于是这样就有了6种后缀方式:

  • 无后缀
  • u 或者 U
  • l 或者 L
  • ll 或者 LL
  • 既有 l/L又有 u/U
  • 既有 ll/LL 又有 u/U

integer constant 有自己的类型。而决定每个 integer constant 类型的规则有点复杂。上面提到的3种进制方式和6种后缀方式一共带来了18种组合,每个组合都会有一个由若干个整数类型组成的候选列表,会依次匹配,第一个能容纳这个数值的类型就是这个 integer constant 的类型。完整的表格可以上cppreference上看

现在再来看INT_MIN。如果#define INT_MIN -2147483648,由于没有负整数常量,那么首先来判断2147483648的类型。它属于无后缀的十进制,从C99开始,候选列表为:

  • int
  • long int
  • long long int

而在int是32位补码表示的机器上,int最大为 21474836472147483648会被当成long int类型或者long long int类型,具体情况由 data models 来定。总而言之,就无法用 -2147483648 来表示最小的int值了。

#define INT_MIN 0x80000000也是类似的情况。首先来判断 0x80000000的类型。它属于无后缀的十六进制,候选列表为:

  • int
  • unsigned int
  • long int
  • unsigned long int
  • unsigned long long int

在 int 是32位补码表示的机器上,int最大为 2147483647,即 0x7FFFFFFF,小于 0x80000000。所以也不能用 int 来表示 0x80000000,自然就无法用 0x80000000 来表示最小的int值了。

在网上看到的一句话总结,我觉得说的挺有道理:虽然 -2147483648 这个数值能够用int类型来表示,但在C语言中却没法写出对应这个数值的int类型常量。