深入理解计算机系统——信息的表示和处理

寻址和字节顺序

​ ​ ​ ​ ​ ​ ​ 在几乎所有的机器上,多字节对象被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。比如,类型为 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 0x1000x1010x1020x103 位置。

​ ​ ​ ​ ​ ​ ​ 那么具体怎么存放呢?有两种方式:

  • 大端法:最高有效字节在最前面。
  • 小端法:最低有效字节在最前面。

​ ​ ​ ​ ​ ​ ​ 还是以刚刚的例子说明, 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_MAXINT_MINUINT_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>


C语言中的有符号数和无符号数

( i n t ) 2147483648 U → − 2147483648 [ i n t ] (int)2147483648U→-2147483648[int] (int)2147483648U2147483648[int]

2147483648U 的补码是:1000 0000, 0000 0000, 0000 0000, 0000 0000

− 2147483647 − 1 U -2147483647-1U 21474836471U

-2147483647 的补码是:1000 0000, 0000 0000, 0000 0000, 0000 0001
和1U运算先变成无符号数,对应十进制2147483649U
所以最终运算得到:2147483648U,补码:1000 0000, 0000 0000, 0000 0000, 0000 0000


C编译器怎么确定数值常量的类型

对于不同版本的c标准,相关规定的具体内容有所差异,对于c90,相关规定如下表所示:

范围类型
0~2 31 ^{31} 31-1int
2 31 ^{31} 31~2 32 ^{32} 32-1unsigned int
2 32 ^{32} 32~2 63 ^{63} 63-1long long
2 63 ^{63} 63~2 64 ^{64} 64-1unsigned long long

而对于c99,相关标准有了些变化:

范围类型
0~2 31 ^{31} 31-1int
2 31 ^{31} 31~2 63 ^{63} 63-1long long
2 63 ^{63} 63~2 64 ^{64} 64-1unsigned long long

编译器在遇到负值的时候,先不看符号,而是根据数字来确定类型,然后再处理负号。


C语言中 TMin 的写法

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 intlongunsigned 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 intlonglong 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 类型常量。


数据大小转换

short sx = -12345;
unsigned uy = sx;
  • 在一台大端法机器上:uy = 4294954951: ff ff cf c7
  • 这表明当把short转换成unsigned时,我们先要改变大小,之后再完成有符号无符号的转换。这个规则是C语言标准要求的。

截断数字

  • 将一个 w w w 位的数 x ⃗ = [ x w − 1 , x w − 2 ,   . . .   , x 0 ] \vec{x}=[x_{w-1},x_{w-2},\ ...\ ,x_{0}] x =[xw1,xw2, ... ,x0] 截断为一个 k k k 位数字时,我们会丢弃高 w − k w-k wk 位,得到一个位向量 x ′ ⃗ = [ x k − 1 , x k − 2 ,   . . .   , x 0 ] \vec{x^{'}}=[x_{k-1},x_{k-2},\ ...\ ,x_{0}] x =[xk1,xk2, ... ,x0]。截断一个数字会改变它的值——溢出的一种形式。

无符号数减法

int sum(int a[], unsigned len) {
	int res = 0;
	for(int i = 0; i <= len - 1; ++i)
		res += a[i]
	return res;
}
  • 对于这样一个求和函数,当 l e n = = 0 len==0 len==0的时候,本应该返回 0 0 0,但实际上会出错。循环条件是无符号数加减法,这等效于模数加法(因为会溢出,而实际上都是按照无符号数读取数值)。最终陷入死循环。
  • 很多 函数(例如strlen、STL集合相关) 返回值都是 unsigned ,要格外小心。最好不要在加减法中使用无符号数。
  • 可以将条件修改为 i < len

判断溢出

  • 加法

    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转换到float精度缺失

  • 首先来看看我们可爱的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) [12 [ 0 , 1 ) [0,1) [01)。除了上述规定以外,根据指数域的二进制表示情况不同,被编码的float型数字又可以分成三种情况——

    1. 规格化值。当指数域的 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(271)(公式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

    2. 非规格化值。当指数域的 8 8 8个二进制数字为全 0 0 0时,float数值就为这种情况。这时指数域所表示的十进制数为 0 0 0,规定指数值为 E = 1 − ( 2 7 − 1 ) E = 1 - (2^7 - 1) E=1(271), 也就是 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

    3. 特殊值。当指数域的 8 8 8个二进制数字为全1时即为这种情况。当小数域为全零时,该float值根据符号位的不同表示正无穷或者负无穷;当小数域为非全零时,该float值为NaN(Not a Number)。

  • 以上,只是在C语言中对intfloat的规约。具体在代码中执行强制类型转化究竟会发生什么?从下面两句很简单的语句开始:

    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 M2E 的形式.由于该数明显大于 1 1 1,所以按照IEEE的标准,其浮点形势必然为规格化值。因此 ,转化后的形式为

    a = 1.101010100001100100001 ∗ 2 2 1 a = 1.101010100001100100001 * 2^21 a=1.101010100001100100001221

  • 根据 规格化值的定义, 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+(271)=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型的缘故。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值