C 语言常见误解/整数/表示法与位元运算

维基教科书,自由的教学读本

整数(一)表示法和位元运算[编辑]

有号整数表示法中的位元分三种:正负号、值和填充。正负号和值的格式可以是二补数(补码)、一补数(反码)或正负号加上大小(原码)三种格式其中之一。无号整数表示法则少了正负号位元。(参考 N1256 6.2.6.2p1~p2 、以及 C Defect Report #069

并不是所有的字节合都能表示合理的数字,存取某些字节合在某些机器上可能会造成严重错误,此种组合称作陷阱表示法(trap representation)。除非使用位元运算或是违反标准其他规定(如溢位),一般的运算不可能产生陷阱表示法。标准明确允许实作自行决定在以下两种状况下是否是陷阱表示法:

  • 型态为有号整数且正负号及值位元为特定组合时(三种格式各有一特殊组合)。
  • 填充位元为某些组合时。(参考 N1256 注脚# 44, 45

位元运算会忽略填充位元,因此(等级不输给 unsigned int 的)无号整数可安心使用。为了最大可携性,位元运算不应该用在有号整数上。

uintN_tintN_t 保证没有填充位元,intN_t 一定是二补数,而且 intN_t 不可能有陷阱表示法,堪称是最安全的整数型态。实作可能不提供这些型态,但一旦提供就要保证这些好性质。(参考标准 N1256 7.18.1.1p1~p3)


问:int 刚好 32 位元不是吗?

答:不一定。整数的宽度(正负号和值位元的数量)没有上界,只要能表示标准规定的数字范围即可。更何况除了宽度之外,可能还有其他填充位元。同理 short 也不一定是 16 位元,long long 也不一定是 64 位元。想要固定宽度请使用 int32_t


问:那 int 至少 32 位元吧?

答:也不一定。因为 int 只保证能存下 -215+1 (-32767) 到 215-1(32767) 之间的整数,16 位元已经足够。int_least32_tint_fast32_t 可以保证存下至少 -231+1到231-1之间的整数(由于不一定是没有陷阱表示法的二补数,所以保证范围的下限不是-231而是-231+1)。


我想要有一个 300 乘 300 的 double 阵列,malloc(300*300*sizeof(double)) 有什么问题? 300*300 可能超出 INT_MAX,而且 300*300*sizeof(double) 可能超出 SIZE_MAXINT_MAX(看实作决定转型成哪个型态)。实际上比较危险的状况是有可能 malloc 实际上只给了一块很小的内存,但程式却当作一块很大的内存使用,造成可能的缓冲区溢位漏洞。为了要安全可携可以做两件事情:第一个是尽量从头到尾维持型态 size_t 或范围更大的无号整数型态,所以 sizeof 摆前面(顺序很重要),而且中途所有数字都是等级在 unsigned int 以上的无号整数(如常量尾巴加上 u);第二个要保证运算结束后不会超过 size_t 的范围。一个可能写法如下:

  if (SIZE_MAX / 300u / 300u < sizeof(double)) {
     p = NULL;
  } else {
     p = malloc(sizeof(double)*300u*300u) ;
  }

(参考只写一半的 clc FAQ 7.16, clang 的检查


问:到底要怎么用 int 才不会超出范围?!

答:int 保证可以存下 -215+1 到 215-1 之间的整数。更一般的写法是使用 INT_MAXINT_MIN 得知真正的范围。其他整数型态都可用类似的方法得到范围。


问:假如 sizeof(int) 为 4 或是确定 int 占 32 位元,是不是代表 int 刚好可以储存 -231 到 231-1 之间的整数?

答:不一定。首先一个字节不一定是 8 位元(见此问题)。即使是,int 表示法中不一定每个位元都会用来表示值(这种位元称作填充位元)。退万步言,即使宽度(不含填充位元)刚好为 32 位元,int 的格式可能也不是二补数,所以不一定是从 -231 开始算。再退万步言,纵使用二补数,特定组合可能是陷阱表示法,所以可能还是无法表示-231。用 int32_t 可以避开以上所有问题,满足所有需求,除了 sizeof(int32_t) 不一定是 4.


问:假如 i 的型态是整数。能不能用 memset(&i, 0, sizeof(i)) 归零?

答:标准委员会已决定向广大程式码妥协。注意只有 0 有特赦条款保证。(参考 C Defect Report #263 看标准如何妥协,以及 N1256 6.2.6.2p5)


问:假设 ab 两个变数有相同的整数型态,a^=b; b^=a; a^=b; 是否可让两数交换?

答:不保证。因为 a^b 可能会产生陷阱表示法。(参考陷阱表示法)


问:该不会 | ^ & ~ 四种位元运算都可能产生陷阱表示法(trap representation)?

答:没错。例如在有号整数上都可能产生陷阱表示法(其他某些型态也有可能)。(参考陷阱表示法)


问:那应该如何安全的使用位元运算?

答:使用等级不输给 unsigned int 的无号整数可高枕无忧。


问:假设 a 的型态是 unsigned int 且宽度洽为 32, a<<32 结果会是 0 吗?

答:不保证(参考 N1256 6.5.7p3)。实际上有些处理器结果会是 a 而不是 0, 因为在那些机器上 a << b 实际上是 a << (b % 32).(参考 KennyTM 举的现实例子


问:要如何区办是 little-endianness 还是 big-endianness?

答:世界上有机器两者都不是,理论上也不可能有(简单的)可携写法可以判断,请仔细考虑是否真的需要判断机器怎么存数字。网络传资料时请用系统提供之转换函式。(参考 middle-endian 和 bi-endian)