一、信息的存储
几个重要概念:
- 虚拟内存 —— 机器级程序将内存视为一个非常大的字节数组,称为虚拟内存
- 虚拟地址空间 —— 虚拟内存中的每个字节都由一个唯一数字标识,称为它的地址。所有可能的地址的集合就称为虚拟地址空间
- 程序对象 —— 程序数据、指令和控制信息的统称
1.1、十六进制表示法
由于二进制表示法太冗长,十进制表示法又不容易与位模式进行转换,所以比较中庸的一个数值表示法是十六进制表示法。
①十六进制与二进制的转换
一个十六进制值可以转换为4个二进制位:
②十六进制与十进制的转换:
- 十六进制数字转换为十进制数字:用相应的16的幂乘以每个十六进制数字
- 十进制数字转换为十六进制数字:用16反复除十进制数字,最后使用最后一个余数放在最前面的方式将所有余数相连就是转换后的十六进制数。
1.2、字数据大小
字长指明指针数据的标称大小。
由于虚拟地址是以字长对应的字来编码的,所以字长大小决定了虚拟地址空间的最大大小。
对于一个位的机器来说,它能编码的虚拟地址的个数最多为个,所以它的虚拟地址的范围就是0 ~ ,程序也即最多能访问个字节。
因此,可以算出32位字长的机器虚拟地址空间的最大大小为字节=4千兆字节=4GB。64位字长的机器虚拟地址空间的最大大小为字节=16777216TB=16EB!
程序被称为"32位程序"或"64位程序",区别在于该程序是如何编译的,而不是其运行的机器类型。
程序可移植性的一个方面就是使程序对不同数据类型的确切大小不敏感。
1.3、寻址和字节顺序
在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。
机器存储字节的顺序:
- 小端法: 在内存中按照从最低有效字节到最高有效字节的顺序存储对象
- 大端法: 在内存中按照从最高有效字节到最低有效字节的顺序存储对象
一旦选择了特定操作系统,那么字节顺序也就固定下来。
1.4、表示字符串
C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组。某个字符都由某个标准编码来表示,最常见的是ASCII字符码。
在使用ASCII码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。因而文本数据比二进制数据具有更强的平台独立性。
1.5、表示代码
不同的机器类型使用不同的且不兼容的指令和编码方式,因此二进制代码是不兼容的。
二进制代码很少能在不同机器和操作系统组合之间移植。
1.6、布尔代数简介
布尔代数就是将逻辑值TRUE和FALSE编码为二进制值1和0,以研究逻辑推理的基本原则。
布尔运算有非、与、或、异或。
位向量就是固定长度为,由0和1组成的串。
1.7、C语言中的位级运算
C语言中的位级运算符有:
- "~" —— 非
- "|" —— 或
- "&" —— 与
- "^" —— 异或
位级运算的一个常见用法就是实现掩码运算,比如:
- 位级运算x & 0xFF生成一个由x的最低有效字节组成的值
- 位级运算~0将生成一个全1的掩码,不管机器的字大小是多少。
1.8、C语言中的逻辑运算
逻辑运算和位级运算的区别是,逻辑运算只返回1或0,分别表示结果为TRUE或FALSE。
C语言中的逻辑运算符有:
- "||" —— 或
- "&&" —— 与
- "!" —— 非
C语言中的逻辑或运算和逻辑与运算是短路运算。
1.9、C语言中的移位运算
①C语言中的左移运算
C语言的左移运算只有逻辑左移这一种运算方式,比如位向量向左移动k位,则在右端补k个0。
②C语言中的右移运算
C语言的右移运算有逻辑右移和算术右移两种运算方式:
- 逻辑右移 —— 位向量向右移动k位,则在左端补k个0。
- 算术右移 —— 位向量向右移动k位,则在左端补k个最高有效位的值。
几乎所有的编译器/机器组合都对有符号数使用算术右移。
在Java中,用运算符>>表示算术右移,用>>>表示逻辑右移。
二、整数表示
2.1、整数数据类型
C语言标准只定义了每种数据类型必须能够表示的最小的取值范围,但是并没有规定每种数据类型能够表示的确切的取值范围。
为不同的大小分配的字节数根据程序编译为32位还是64位而有所不同,也只有long数据类型以及unsigned long数据类型不同。
C和C++都支持有符号数和无符号数,Java只支持无符号数。
2.2、无符号数的编码
将一个位的位向量转换为无符号数的公式为:
位的位向量可表示的最大无符号数为:
位的位向量可表示的最小无符号数为0。
将一个无符号数转换为位向量的函数为。
由于既是单射又是反射(既它是一个双射),所以和互为反函数,因此有:
2.3、补码编码
将一个位的位向量转换为有符号数的公式为:
位的位向量可表示的最大有符号数为:
位的位向量可表示的最小有符号数为:
将一个有符号数转换为位向量的函数为。
由于既是单射又是反射(既它是一个双射),所以和互为反函数,因此有:
2.4、有符号数和无符号数之间的转换
补码转换为无符号数的公式为:
推导:
下图说明了T2U的一般行为:
无符号数转换为补码的的公式为:
推导:
下图说明了U2T的一般行为:
2.5、C语言中的有符号数与无符号数
C语言中涉及有符号数与无符号数的转换的场景有:
①显示的强制类型转换
②隐式的类型转换
③使用printf()且指定指示符%d、%u、%x进行打印
④无符号数与有符号一起执行运算时,隐式地将有符号数转换为无符号数
2.6、扩展一个数字的位表示
①无符号数的零扩展
要将一个无符号数转换为一个更大的数据类型,只要简单地在表示的开头添加0,这种运算称为零扩展:
②补码数的符号扩展
要将一个补码数字转换为一个更大的数据类型,可以在表示的开头添加最高有效位的值,这种运算称为符号扩展:
推导:
上式可以用数学归纳法归纳为:
然后证明:
其实原理就是:
不太难!
一个数据大小到另一个数据大小的转换,以及无符号和有符号数字之间的转换的相对顺序能够影响一个程序的行为。
因此C语言标准规定先要改变大小,之后在完成从有符号到无符号的转换。
2.7、截断数字
①截断无符号数
推导:
②截断补码数值
推导:
截断补码数值的方式是将补码数先转换为无符号数,然后对无符号数进行截断,再转换回补码数。
2.8、关于有符号数与无符号数的建议
有符号数到无符号数的隐式强制类型转换导致了某些非直观的行为,这些非直观的特性经常导致程序错误。
为了避免这种错误,要尽量避免运算操作中,运算数数据类型的不一致。如果不一致,应当保持灵敏,避免错误。
三、整数运算
3.1、无符号加法
由于数据类型的的内存大小是固定的,因此无符号数之间的加法有可能产生溢出。
①无符号数加法的规则
推导:
②检测无符号数加法是否溢出
推导:
③无符号数求反
无符号数加法就是一种模数加法,它会形成一个数学结构,称为阿贝尔群。
阿贝儿群中的每个元素都有一个加法逆元,可以通过以下公式求取加法逆元:
推导:
3.2、补码加法
补码加法同样也会产生溢出,但补码加法的结果却不像模数加法那样在数学上感觉很熟悉。
①补码加法的规则
推导:
推导的核心是补码加法和无符号数加法有相同的位级表示,于是可以根据以下过程进行推导:将补码数转换为无符号数,无符号数进行模数加法,最后再转换回补码数。
②检测补码加法是否溢出
推导:
根据数学理论一个正数加上一个正数结果不可能为负,一个负数加上一个负数的结果不可能为正,所以产生"意外"的原因就是发生了溢出。
3.3、补码的非
范围在 x 中的每个数字都有补码加法下的加法逆元。
推导:
同时也可以使用以下方法来得到补码的非:
- 对于任意整数值x,计算表达式-x和~x+1得到的结果完全一样
- 找到x位级表示的最右边的1,然后将其左边的全部位值取反
3.4、无符号乘法
无符号数乘法的结果最后也要被截断为位,也等价于计算该值模。
3.5、补码乘法
将一个补码数截断为位相当于计算该值模(会得到无符号数),再把无符号数转换为补码。
①补码乘法规则
②无符号数和补码乘法的位级等价性
对于无符号数和补码乘法来说,乘法运算的位级表示都是一样的。
推导: