在几乎所有的机器上,多字节对象被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。比如,类型为 i n t int int 的变量 x x x 的地址为 0 x 100 0x100 0x100,即在 C C C 中地址表达式 & x \&x &x 的值为 0 x 100 0x100 0x100,那么 x x x 的4个字节将会被存储在内存的 0 x 100 、 0 x 101 、 0 x 102 、 0 x 103 0x100、0x101、0x102、0x103 0x100、0x101、0x102、0x103 位置。
那么具体怎么存放呢?有两种方式:
大端法:最高有效字节在最前面。小端法:最低有效字节在最前面。 还是以刚刚的例子说明, x x x 的 16 16 16 进制值是 0 x 01234567 0x01234567 0x01234567,地址范围 0 x 100 0x100 0x100 ~ 0 x 103 0x103 0x103 的字节顺序按照大端和小端法如下排列:
采用大端还是小端要看机器的类型,大多数是只用小端模式,也有新的微处理器采用双端法。
对于我们来说,机器使用的字节顺序我们是无法见到的,但是字节顺序对于使用者来说确实很重要的。比如说,一台大端机器和一台小端机器进行网络通信,接收程序中会发现字里的字节是反序的。为了避免这样的问题,接受方就需要做出一些转换。
C C C 库中的文件 < l i m i t s . h > <limits.h> <limits.h> 定义了一组常量: I N T _ M A X 、 I N T _ M I N 、 U I N T _ M A X INT\_MAX、INT\_MIN、UINT\_MAX INT_MAX、INT_MIN、UINT_MAX 确定宽度类型的带格式打印需要使用宏,以与系统相关的方式扩展为格式串。比如:变量 x x x 和 y y y 的类型是 i n t 32 _ t int32\_t int32_t 和 i n t 34 _ t int34\_t int34_t ,可以通过调用 p r i n t f printf printf 来打印它们的值,如下所示
printf("x = %" PRId32 ", y = %" PRId64 "\n", x, y);编译为 64 64 64 位程序时,宏 P R I d 32 PRId32 PRId32 展开成字符串 “ d d d”,宏 P R I d 64 PRId64 PRId64 展开成两个字符串 “ l l l” “ u u u”。当 C C C 预处理器遇到仅用空格(或其他空白字符)分隔的一个字符串常量序列时,就把他们串联起来。因此上面的 p r i n t f printf printf 调用就变成了:
printf("x = %d, y = %lu\n", x, y);使用宏能保证:不论代码是如何被编译的,都能生成正确的格式字符串。 注: P R I d 32 PRId32 PRId32 和 P R I d 64 PRId64 PRId64 在 C C C 中需要使用头文件 < i n t t y p e s . h > <inttypes.h> <inttypes.h>
( i n t ) 2147483648 U → − 2147483648 [ i n t ] (int)2147483648U→-2147483648[int] (int)2147483648U→−2147483648[int]
2147483648U 的补码是:1000 0000, 0000 0000, 0000 0000, 0000 0000
− 2147483647 − 1 U -2147483647-1U −2147483647−1U
-2147483647 的补码是:1000 0000, 0000 0000, 0000 0000, 0000 0001 和1U运算先变成无符号数,对应十进制2147483649U 所以最终运算得到:2147483648U,补码:1000 0000, 0000 0000, 0000 0000, 0000 0000
对于不同版本的c标准,相关规定的具体内容有所差异,对于c90,相关规定如下表所示:
范围类型0~2 31 ^{31} 31-1int2 31 ^{31} 31~2 32 ^{32} 32-1unsigned int2 32 ^{32} 32~2 63 ^{63} 63-1long long2 63 ^{63} 63~2 64 ^{64} 64-1unsigned long long而对于c99,相关标准有了些变化:
范围类型0~2 31 ^{31} 31-1int2 31 ^{31} 31~2 63 ^{63} 63-1long long2 63 ^{63} 63~2 64 ^{64} 64-1unsigned long long编译器在遇到负值的时候,先不看符号,而是根据数字来确定类型,然后再处理负号。
C C C 头文件 < l i m i t s . h > <limits.h> <limits.h> 中:
/* Minimum and maximum values a 'signed int' can hold. */ #define INT_MAX 2147483647 #define INT_MIN (-INT_MAX - 1)根据 C C C 语言版本和常量的格式(十进制和十六进制),常量的数据类型是从上面表格里选择第一个最合适(能表示常量而不溢出的)的类型。
对于 I S O C 90 ISO\ C90 ISO C90,编译器依次尝试 i n t 、 l o n g 、 u n s i g n e d int 、long、 unsigned int、long、unsigned( 32 32 32位机器上 l o n g long long 跟 i n t int int 一样,是 32 32 32 位), 最终选择 u n s i g n e d unsigned unsigned 来表示。对于 2147483648 2147483648 2147483648 和 − 2147483648 -2147483648 −2147483648,如果表示为 32 32 32 位的二进制数字,它们的位表示是一样的,都是 0 x 80000000 0x80000000 0x80000000。所以这个常量表达式 − 2147483648 -2147483648 −2147483648 的数据类型为 u n s i g n e d unsigned unsigned 且值为 2147483648 2147483648 2147483648。
对于 I S O C 99 ISO\ C99 ISO C99,编译器依次选择 i n t 、 l o n g 、 l o n g l o n g int、long、long\ long int、long、long long,最终选择 l o n g l o n g long\ long long long 类型才能容纳 2147483648 2147483648 2147483648 。用 64 64 64位,可以唯一表示 2147483648 2147483648 2147483648 和 − 2147483648 -2147483648 −2147483648,所以这个常量表达式的数据类型为 l o n g l o n g long\ long long long,值为 − 2147483648 -2147483648 −2147483648。
对于 16 16 16 进制常数 0 x 80000000 0x80000000 0x80000000 (注意,按照C语言中整型常量的定义,这个整数常量是正数,值为 2417483648 2417483648 2417483648),在 32 32 32 位机器上,编译器也是利用同样的规则,依照表一中的 16 16 16 进制的列表来处理。两个语言标准中,都是首先跟 T M a x 32 TMax32 TMax32( 0 x 7 F F F F F F F 0x7FFFFFFF 0x7FFFFFFF)比较,由于 0 x 80000000 0x80000000 0x80000000 更大,所以这个值不能用 i n t int int 来表示。接下来和 U M a x 32 UMax32 UMax32( 0 x F F F F F F F F 0xFFFFFFFF 0xFFFFFFFF)比较,由于比它小一些,所以选择 u n s i g n e d unsigned unsigned 来表示。所以这个常量表达式的数据类型是 u n s i g n e d unsigned unsigned,值为 0 x 80000000 0x80000000 0x80000000(或者说,是等于 2147483648 2147483648 2147483648 )。
在 64 64 64 位的机器上,事情稍微有些不同。两个语言标准中,十进制的格式 − 2417483648 -2417483648 −2417483648 都是 l o n g long long( 64 64 64位)类型,值为 − 2417483648 -2417483648 −2417483648,然而十六进制格式 0 x 80000000 0x80000000 0x80000000 都是 u n s i g n e d unsigned unsigned 类型,值为 0 x 80000000 0x80000000 0x80000000(或者说,是 2147483648 2147483648 2147483648)。
用一句话来解释 C C C 语言中 T M i n 32 TMin32 TMin32的古怪写法的原因:虽然 − 2147483648 -2147483648 −2147483648 这个数值能够用int类型来表示,但在 C C C 语言中却没法写出对应这个数值的 i n t int int 类型常量。
加法
int tadd_ok(int x, int y) { int sum = x+y; int neg_over = x < 0 && y < 0 && sum >= 0; int pos_over = x >= 0 && y >= 0 && sum < 0; return !neg_over && !pos_over; }减法
int tsub_ok(int x, int y) { // 当y为最小整数的时候,就产生了溢出,因为任何数减最小数都会溢出 if (y == INT_MIN) { return 0; } int neg_y = -y; int sum = x + neg_y; int pos_over = x > 0 && neg_y > 0 && sum < 0; int neg_over = x < 0 && neg_y < 0 && sum >= 0; return !(pos_over || neg_over); }乘法
int tmult_ok(int x, int y) { int p = x*y; /* Either x is zero, or dividing p by x gives y */ return !x || p/x == y; }首先来看看我们可爱的int型变量吧,在一台典型的32位机器上一个有符号的int型的取值范围为-2147483648 ~ 2147483647(-2^31 ~ (2^31-1))。也就是说,在一个4字节(32位2进制),除去首位用于符号位表示正负外,其余的31位都是数字的有效位。
下面再来看看“万恶的”float型变量:根据IEEE的浮点标准,一个浮点数应该用下述形式来表示:
V=(-1)^s * M * 2^E (公式1)
在C语言中,32位的float型变量有着这样的规定:首位表示符号位 s s s ,接下来的 8 8 8位(指数域)用于表示 2 2 2的指数 E E E,剩余的 23 23 23位(小数域)表示 M M M(取值范围为 [ 1 , 2 ) [1,2) [1,2) 或 [ 0 , 1 ) [0,1) [0,1))。除了上述规定以外,根据指数域的二进制表示情况不同,被编码的float型数字又可以分成三种情况——
规格化值。当指数域的 8 8 8个二进制数字既非全零又非全 1 1 1 时,float数值就是这种情况。设指数域的八位二进制所表示的十进制数为e, 则公式 1 1 1中的 E E E就是 E = e − ( 2 7 − 1 ) E = e - (2^7 - 1) E=e−(27−1)(公式2); 而且此时,将小数域所表示的二进制假设为 ( f 22 ) ( f 21 ) . . . ( f 1 ) ( f 0 ) (f_{22})(f_{21})...(f_1)(f_0) (f22)(f21)...(f1)(f0),则该小数域所表示的值即为 f = 0. ( f 22 ) ( f 21 ) . . . ( f 1 ) ( f 0 ) f = 0.(f_{22})(_{f21})...(f_1)(f_0) f=0.(f22)(f21)...(f1)(f0).于是 M = 1 + f M = 1 + f M=1+f
非规格化值。当指数域的 8 8 8个二进制数字为全 0 0 0时,float数值就为这种情况。这时指数域所表示的十进制数为 0 0 0,规定指数值为 E = 1 − ( 2 7 − 1 ) E = 1 - (2^7 - 1) E=1−(27−1), 也就是 E E E为定值 − 126 -126 −126;此时小数域的值仍表示 f = 0. ( f 22 ) ( f 21 ) . . . ( f 1 ) ( f 0 ) f = 0.(f_{22})(f_{21})...(f_1)(f_0) f=0.(f22)(f21)...(f1)(f0),但是M的值却变成 M = f M = f M=f。
特殊值。当指数域的 8 8 8个二进制数字为全1时即为这种情况。当小数域为全零时,该float值根据符号位的不同表示正无穷或者负无穷;当小数域为非全零时,该float值为NaN(Not a Number)。
以上,只是在C语言中对int和float的规约。具体在代码中执行强制类型转化究竟会发生什么?从下面两句很简单的语句开始:
int a = 3490593; float b = (float)a;那么在内存中 a a a和 b b b究竟存放的是什么值呢?
将 a a a展开为二进制,其值为0000 0000 0011 0101 0100 0011 0010 0001,其十六进制即为0x00354321。 因为要转化为float型,所以首先要对上述二进制的表示形式改变为 M ∗ 2 E M * 2^E M∗2E 的形式.由于该数明显大于 1 1 1,所以按照IEEE的标准,其浮点形势必然为规格化值。因此 ,转化后的形式为
a = 1.101010100001100100001 ∗ 2 2 1 a = 1.101010100001100100001 * 2^21 a=1.101010100001100100001∗221
根据 规格化值的定义, M = 1 + f M = 1 + f M=1+f. 所以 f = 0.101010100001100100001.因为float型变量的小数域一共23位。所以b的最后23位可以得出,其值为1010 1010 0001 1001 000 0100
下面再演绎指数域的值:因为a的指数表示法中,指数 E = 21 E = 21 E=21。根据公式2, e = E + ( 2 7 − 1 ) = 148 e = E + (2^7 -1) = 148 e=E+(27−1)=148.所以可以得出b的指数域的二进制表示为:10010100。在加上原数为正,所以符号位 s = 0 s=0 s=0。
所以,可以得出b的二进制表示为0 10010100 10101010000110010000100。转化为十六位进制则是0x4A550C84。换句话说,它存储在内存中的值是与a是完全不同的。但是其间还是有关联性的——a的首位为 1 1 1的数值位后的二进制表示是与b的小数域完全相同的。
很快,问题就出现了。int型的有效位数是31,而float型小数域的有效位只有 23 23 23位,也就是说如果上面的 a a a的二进制的有效位超过了 24 24 24位,那么float型的小数域的精度就不够了。因此必须进行舍入。比如:如果上面的a的二进制为0000 0001 1111 0101 0100 0011 0010 0001。这时b的小数域必须有 24 24 24位才够,但是,这显然是不现实的,因此必须舍入到 23 23 23位,舍入的原则是:所得结果的最低有效位为0。因此这个 a a a在转换到float时,其精度就会丢失,因为该float的最后 23 23 23位变成了11110101010000110010000——这显然是与原值不符的。
实际上,C语言中对于double型在 32 32 32位机器上的小数域有 52 52 52位,对于int型的 31 31 31位有效位是绰绰有余了。这就是为什么大部分C语言教材上鼓励读者在执行强制类型转换时将int型转换成double。同时,这可能也是为什么int型能够直接隐式转换到double型的缘故。