C 语言常见误解/整数/表示法与位元运算
整数(一)表示法和位元运算
编辑有号整数表示法中的位元分三种:正负号、值和填充。正负号和值的格式可以是二补数(补码)、一补数(反码)或正负号加上大小(原码)三种格式其中之一。无号整数表示法则少了正负号位元。(参考 N1256 6.2.6.2p1~p2 、以及 C Defect Report #069)
并不是所有的字节合都能表示合理的数字,存取某些字节合在某些机器上可能会造成严重错误,此种组合称作陷阱表示法(trap representation)。除非使用位元运算或是违反标准其他规定(如溢位),一般的运算不可能产生陷阱表示法。标准明确允许实作自行决定在以下两种状况下是否是陷阱表示法:
- 型态为有号整数且正负号及值位元为特定组合时(三种格式各有一特殊组合)。
- 填充位元为某些组合时。(参考 N1256 注脚# 44, 45)
位元运算会忽略填充位元,因此(等级不输给 unsigned int
的)无号整数可安心使用。为了最大可携性,位元运算不应该用在有号整数上。
uintN_t
和 intN_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_t
和 int_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_MAX
或 INT_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_MAX
和 INT_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)
问:假设 a
和 b
两个变数有相同的整数型态,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)