2.1 信息存储
计算机一般采用8位的块,即字节,作为最小的可寻址的内存单位。机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。内存的每个字节都有地址,所有地址的集合称为虚拟地址空间。
虚拟地址空间只是一个抽象概念,实际的实现是将DRAM、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来,为程序提供一个看上去统一的字节数组。
C语言中一个指针的值(无论他指向一个整数、一个结构或是某个其他程序对象),都是某个存储块的第一个字节的虚拟地址。
2.1.1 十六进制表示法
C语言中,以0x
或者0X
开头的是十六进制的值。字符‘A’~‘F’不区分大小写,也可以混合写。牢记A、C、F相对应的十进制值和二进制表示,可以很方便的转换。
给定二进制去转换十六进制,首先从右往左分为4位一组,最左不满4位的就在前面用0补足。
当值x
是2的非负整数n
次幂时,也就是x=2n
,其x
的二进制表示就是1后面跟n
个0。因为十六进制中数字0代表4个二进制0,所以可以将n
写成i+4j(0<=i<=3)
的形式,可以把x
写成开头的十六进制数字为1(i=0
)、2(i=1
)、4(i=2
)、8(i=3
),后面跟随者j
个十六进制的0。例如x=2048=211
,所有有n=11=3+4*2
,即二进制表示为1000 0000 0000
得出其十六进制表示为0x800
。
2.1.2 字数据大小
每台计算机都有一个字长(word wize) ,指明指针数据的标称大小(nominal size) 。因为虚拟地址是以这样的一个字来编码的,所以字长决定了虚拟地址空间的最大大小。例如,对于一个字长为w
为的机器而言,虚拟地址的范围为0~2w-1
,程序最多访问2w
个字节。
为了避免由于依赖“典型”大小和不同编译器设置带来的奇怪行为,ISO C99引入了数据大小固定的一类数据类型,不随编译器和机器设置而变化:int32_t
和int64_t
,大小分别为4个字节和8个字节。
编写程序时应力图使得程序是可移植的,可移植性的一个方面就是程序对不同数据类型的大小不敏感。
2.1.3 寻址和字节寻址
对于跨越多字节的程序对象,有两个规则:
- 如何排列这些字节:都被存储为连续的字节序列。
- 对象地址是什么:对象地址为所使用的连续字节序列的最小的地址。
- 小端法(little endian):从低有效字节到高有效字节的顺序。
- 大端法(big endian):从高有效字节到低有效字节的顺序。
- 双端法(bi-endian):可以配置成小端法或者大端法的机器。
unsigned char在C里面就是表示一个字节,值为从00到FF。带上unsigned表示这个字节是无符号。如果代码中出现了unsigned char类型,那就是表示这是一个字节。
2.1.4 表示字符串
略
2.1.5 表示代码
不通机器类型使用不通的且不兼容的指令和编码方式。二进制代码是不兼容的,二进制代码很少能在不同机器和操作系统组合之间移植。
2.1.6 布尔代数简介
George Boole(乔治 · 布尔)
四种:
- 与 :&
- 或 :|
- 非 :~
- 异或 :^
运算规则如下图:
Claude Shannon(克劳德 · 香农)
首先建立了布尔代数和数字逻辑之间的联系。在1937年的硕士论文中表明了布尔代数可以用来设计和分析机电继电器网络。
2.1.7 C语言中的位级运算
C语言支持按位布尔运算,这些运算适用于任何“整型”的数据类型上。下图就是对char数据类型表达式求值的例子:
将十六进制转换为二进制然后进行运算,最后再转回二进制。
2.1.8 C语言中的逻辑运算
C语言还提供了一组逻辑运算符:||
、&&
和!
,分别对应命题逻辑中的OR
、AND
和NOT
。
注意区分逻辑运算和2.1.7中的位级运算:
- 逻辑运算认为所有非零的参数都表示TRUE,而参数0表示FALSE。他们返回1或者0,分别表示TRUE或者FALSE。
- 位级运算只有在参数被限制为0或者1时,才和逻辑运算有相同的行为。
- 逻辑运算符
&&
和||
,如果对第一个参数求值就能得到表达式的结果,那么逻辑运算符就不会对第二个参数求值。
2.1.9 C语言中的移位运算符
C语言提供了一组移位运算,向左或者向右移动位模式:
- 左移:
<<
。x向左移动k位,丢弃最高的k位,并有右端补k个0。移位运算是从左向右可结合的,x<<j<<k
等价于(x<<j)<<k
- 右移:
>>
。机器支持两种右移:
- 逻辑右移:左端补k个0,丢弃右端最低的k位
- 算术右移:左端补k个最高有效位的值(对有符号整数数据非常有用)
图中斜体数字代表填充的值。关注算术右移[10010101]
,因为最高有效位是1,所以填充的值就是1
C语言并没有明确定义对于有符号数应该使用哪种类型的右移。所以这就可能面临可移植性问题。实际上,对于大部分的编译器/机器组合都对有符号数使用算术右移。另一方面,对于无符号数,右移必须是逻辑右移。
一个w
位组成的数据,假设移动k >= w
位,会有什么结果?
移位指令会考虑位移量的低log2w
位,即实际位移量是通过计算k mod w
取余得到的。
假设当w=32
,位移量k分别为32、36、40,那么实际位移量位0、4、8位。
C程序对该行为没有保证,应该保持位移量小于待位移值的位数。
2.2 整数表示
本节要使用的一些数学术语:
2.2.1 整型数据类型
2.2.2 无符号数的编码
假设一个整数数据类型有w
位,可以写成向量的形式:x⃗\vec{x}x
那么就有:
w
位所能表示的最小最大范围是:0~2w-1
。
无符号数的二进制表示有一个很重要的属性,就是每个介于0~2w-1
之间的数都有唯一一个w
位的值编码。
2.2.3 补码编码
最常见的有符号数的计算及表示方法为补码(two’s-complement) 。在这个定义中,将最高有效位解释为负权(negtive weight) 。
二进制转补码表示:
其中最高有效位xw-1
称为符号位,权重为-2w-1
。符号位设置为1时,表示为负,设置为0时,值为非负。那么就有:
w
位补码所能表示的范围为:最小值[10…0](设置为负权,并清空其他位),其整数值为TMinw=−2w−1TMin_w=-2^{w-1}TMinw=−2w−1;最大值[01…1](清除具有负权的位,并设置其他位),其整数值为
TMaxw=∑i=0w−22i=2w−1−1TMax_w=\sum_{i=0}^{w-2} 2^i=2^{w-1}-1TMaxw=∑i=0w−22i=2w−1−1.
同样的,每个介于最小值和最大值之间的值都有唯一一个补码编码。
不同字长下,几个比较重要的数字的位模式和数值:
- 补码的范围是不对称的:∣TMin∣=∣TMax∣+1\mid TMin \mid=\mid TMax \mid+1∣TMin∣=∣TMax∣+1,因为0是非负数。
- UMaxw=2TMaxw+1UMax_w=2TMax_w+1UMaxw=2TMaxw+1
有符号数还有两种标准的表示方法:
- 反码(Ones’ Complementz):最高有效位为
-(2w-1-1)
,其他和补码一样:
B2Ow(x⃗)=−xw−1(2w−1−1)+∑i=0w−22iB2O_w(\vec x)=-x_{w-1}(2^{w-1}-1)+\sum_{i=0}^{w-2} 2^iB2Ow(x)=−xw−1(2w−1−1)+∑i=0w−22i - 原码(Sign-Magnitude):最高有效位是符号位,用来确定剩下的位应该取负权还是正权:
B2Sw(x⃗)=(−1)xw−1⋅(∑i=0w−2xi2i)B2S_w(\vec x)=(-1)^{x_{w-1}} \cdot (\sum_{i=0}^{w-2} x_i2^i)B2Sw(x)=(−1)xw−1⋅(∑i=0w−2xi2i)
这两种表示方法对数字0有不同的编码方式:
- 将[00…0]解释为+0,
- 值-0在源码中表示为[10…0],而反码中表示为[11…1]。
现在几乎所有的现代机器都是使用补码。在浮点数中有使用源码编码。
2.2.4 有符号数和无符号数之间的转换
unsigned u = 4294967295u;
int tu = (int) u;
printf("u=%u, tu=%d\n", u, tu);
---
u=4294967295, tu=-1
图2-14可以看到,32位字长的情况下,无符号形式的4294967296(UMax32
)和补码形式的-1的位模式是完全一样的。也就是说将unsigned强制类型转换成int,底层的位表示保持不变。
在C语言中,处理同样字长的有符号数和无符号数之间的转换的规则是:数值可能会变,但是位模式不变。
即:(这两个函数描述了大多数C语言实现中这两种数据类型之间的的强制类型转换效果)
- T2Uw(x)=B2Uw(T2Bw(x))T2U_w(x)=B2U_w(T2B_w(x))T2Uw(x)=B2Uw(T2Bw(x)),有符号(补码)转成无符号。
- U2Tw(x)=B2Tw(U2Bw(x))U2T_w(x)=B2T_w(U2B_w(x))U2Tw(x)=B2Tw(U2Bw(x)),无符号转成有符号(补码)。
无符号表示中的UMax
和补码表示的-1有相同的位模式。且有以下关系1+UMaxw=2w
。
??? 补码转为无符号:
T2Uw(x)={x+2w,x<0x,x≥0
T2U_w(x) = \begin{cases}
x+2^w ,& x<0 \\
x ,& x\geq0
\end{cases}
T2Uw(x)={x+2w,x,x<0x≥0
??? 无符号转为补码:
U2Tw(u)={u,u≤TMaxwu−2w,u>TMaxw
U2T_w(u) = \begin{cases}
u ,& u\leq TMax_w \\
u-2^w ,& u > TMax_w
\end{cases}
U2Tw(u)={u,u−2w,u≤TMaxwu>TMaxw
2.2.5 C语言中的有符号数与无符号数
C语言中Tmin的写法:
# limits.h
#define INT_MAX 2147483647
#define INT_MIN (-INT_MAX - 1)
2.2.6 扩展一个数字的位表示
- 无符号数转换为一个更大的数据类型,只要在表示的开头添加0,叫做零扩展(zero extension)
- 补码数字转换为一个更大的数据类型,在表示中添加最高有效位的值,叫做符号扩展(sign extension):
2.2.7 截断数字
- 截断无符号数:将一个
w
位的数截断为一个k
位数字,丢弃高w-k
位。截断一个数字可能会改变它的值——溢出的一种形式。 - ???截断补码数值:
2.2.8 关于有符号数和无符号数的建议
有符号数到无符号数的转换会导致某些非直观地行为:错误或者漏洞,且不易发现。所以尽量避免使用无符号数。实际上,除了C语言以外,很少有语言支持无符号整数。
2.3 整数运算???
2.3.1 无符号加法
对于两个4位的字长,其和可能需要5位。例如:
x=9
和y=12
的位表示分别为[1001]
和[1100]
,和为21,5位的位表示位[10101]
。丢弃最高位,就得到[0101]
,也就是十进制的5。这就和值21 mod 16 = 5
一致。
2.3.2 补码加法
2.3.3 补码的非
2.3.4 无符号乘法
2.3.5 补码乘法
2.3.6 乘以常数
在以往的大多数计算机上,整数乘法指令相当慢,需要10个或更多的时钟周期,而其他整数运算(加法、减法、位级运算和移位等)只需要一个时钟周期。因此编译器使用了一种优化:用移位和加法运算的组合来代替乘以常数的运算。
- 乘以2的幂
- 与2的幂相乘的无符号乘法
- 与2的幂相乘的补码乘法
2.3.7 除以2的幂
整数除法一般要比整数乘法更慢,需要30甚至更多的时钟周期。因此也可以用移位运算来实现,使用右移,而不是左移。无符号和补码数分别使用逻辑右移和算术右移来达到目的。
整数除法总是舍入到零。
- 除以2的幂的无符号除法。
- 除以2的幂的补码除法,向下舍入
- 除以2的幂的补码除法,向上舍入
2.3.8 关于整数运算的最后思考
计算机执行的“整数”运算实际上是一种模运算形式。
2.4 浮点数
IEEE标准754
2.4.1 二进制小数
- 二进制小数点向左移动一位相当于这个数被2除。
- 向右移动一位相当于这个数乘以2。
- 小数的二进制表示法只能表示被写成
x X 2y
的数,其他值只能被近似地表示。
2.4.2 IEEE浮点表示
后续内容没太看懂,为了不耽误进度,先搁置一下