2.1 信息存储
1字节(byte)= 8位(bit)
2.1.1 十六进制表示法
十六进制(简写为"hex")使用数字 '0' ~ '9' 以及字符 'A' ~ 'F' 来表示。
1 个十六进制数表示 4 个二进制数。
当 x=2n 时,很容易将 x 写成十六进制形式,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。
2.1.2 字数据大小
C声明 | C声明 | 字节数 | 字节数 |
---|---|---|---|
有符号 | 无符号 | 32位 | 64位 |
[signed] char | unsigned char | 1 | 1 |
short | unsigned short | 2 | 2 |
int | unsigned | 4 | 4 |
long | unsigned long | 4 | 8 |
int32_t | uint32_t | 4 | 4 |
int64_t | uint64_t | 8 | 8 |
char * | 4 | 8 | |
float | 4 | 4 | |
double | 8 | 8 |
2.1.3 寻址和字节顺序
最低有效字节在最前面的方式,为小端法(little endian)。
最高有效字节在最前面的方式,为大端法(big endian)——与书写习惯一致。
假设变量 x 的类型为 int,位于地址 0x100 处,它的十六进制值为 0x01234567。地址范围 0x100〜0x103 的字节顺序依赖于机器的类型:
注意,在字 0x01234567 中,高位字节的十六进制值为 0x01,而低位字节值为 0x67。
// 判断大小端
int is_little_endian(void) {
int i = 0xff;
unsigned char *p = &i;
return p[0] == 0xff;
}
2.1.4 表示字符串
C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组。
字母 'a' ~ 'z' 的 ASCII 码为 0x61 ~ 0x7A。
2.1.5 表示代码
不同的机器类型使用不同的且不兼容的指令和编码方式。
2.1.6 布尔代数简介
1 为 true(真)
0 为 false(假)
图2-7↓ 布尔代数的运算。二进制值 1 和 0 表示逻辑值 TRUE 或者 FALSE ,而运算符 〜、&、丨和 ^ 分别表示逻辑运算 NOT、 AND、OR 和 EXCLUSIVE-OR
可以将上述 4 个布尔运算扩展到位向量的运算,位向量就是固定长度为 W 、由 0 和 1 组成的串。位向量的运算可以定义成参数的每个对应元素之间的运算。
举个例子,假设 w=4 ,参数 a = [0110] ,b=[1100]。那么 4 种运算a&b、a|b、a^b 和〜b 分别得到以下结果:
关于布尔代数和布尔环的更多内容
布尔运算 & 对 | 的分配律,写为 a & (b | c) = (a & b) | (a & c)。
布尔运算 | 对 & 的分配律,写为 a | (b & c) = (a | b) & (a | c)。
对于任何值 a 来说,a ^ a = 0。
(a ^ b) ^ a = b。
2.1.7 C语言中的位级运算
C的表达式 | 二进制表达式 | 二进制结果 | 十六进制结果 |
---|---|---|---|
~0x41 | ~[0100 0001] | [1011 1110] | 0xBE |
~0x00 | ~[0000 0000] | [1111 1111] | 0xFF |
0x69&0x55 | [0110 1001]&[0101 0101] | [0100 0001] | 0x41 |
0x69|0x55 | [0110 1001]|[0101 0101] | [0111 1101] | 0x7D |
位级运算的一个常见用法就是实现掩码运算,这里掩码是一个为模式,表示从一个字中选出的位的集合。
如:x = 0x89ABCDEF,x&0xFF = 0x000000EF
表达式 ~0 将生成一个全 1 的掩码,不管机器的字大小是多少。
2.1.8 C语言中的逻辑运算
|| OR 或
&& AND 与
! NOT 非
表达式 | 结果 |
---|---|
!0x41 | 0x00 |
!0x00 | 0x01 |
!!0x41 | 0x01 |
0x69&&0x55 | 0x01 |
0x69||0x55 | 0x01 |
如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。
2.1.9 C语言中的位移运算
左移:x<<k,x向左移动k位,丢弃最高的k位,并在右端补k个0。移位量应该是一个0~w-1之间的值。
逻辑右移:x>>k,在左端补k个0。
算术右移:x>>k,在左端补k个最高有效位的值。
2.2 整数表示
数学术语
下标w表示数据表示中的位数
符号 | 类型 | 含义 |
---|---|---|
函数 | 二进制转补码 | |
函数 | 二进制转无符号数 | |
函数 | 无符号数转二进制 | |
函数 | 无符号转补码 | |
函数 | 补码转二进制 | |
函数 | 补码转无符号数 | |
常数 | 最小补码值 | |
常数 | 最大补码值 | |
常数 | 最大无符号数 | |
操作 | 补码加法 | |
操作 | 无符号数加法 | |
操作 | 补码乘法 | |
操作 | 无符号数乘法 | |
操作 | 补码取反 | |
操作 | 无符号数取反 |
2.2.1 整型数据类型
32位程序上C语言整型数据类型的典型取值范围
C数据类型 | 最小值 | 最大值 |
---|---|---|
[signed]char | -128 | 127 |
unsigned char | 0 | 255 |
short | -32 768 | 32 767 |
unsigned short | 0 | 65 535 |
int | -2 147 483 648 | 2 147 483 647 |
unsigned | 0 | 4 294 967 295 |
long | -2 147 483 648 | 2 147 483 647 |
unsigned long | 0 | 4 294 967 295 |
int32_t | -2 147 483 648 | 2 147 483 647 |
uint32_t | 0 | 4 294 967 295 |
int64_t | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 |
uint_64_t | 0 | 18 446 744 073 709 551 615 |
64位程序上C语言整型数据类型的典型取值范围
C数据类型 | 最小值 | 最大值 |
---|---|---|
[signed]char | -128 | 127 |
unsigned char | 0 | 255 |
short | -32 768 | 32 767 |
unsigned short | 0 | 65 535 |
int | -2 147 483 648 | 2 147 483 647 |
unsigned | 0 | 4 294 967 295 |
long | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 |
unsigned long | 0 | 18 446 744 073 709 551 615 |
int32_t | -2 147 483 648 | 2 147 483 647 |
uint32_t | 0 | 4 294 967 295 |
int64_t | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 |
uint_64_t | 0 | 18 446 744 073 709 551 615 |
C语言的整型数据类型的保证的取值范围
C数据类型 | 最小值 | 最大值 |
---|---|---|
[signed]char | -127 | 127 |
unsigned char | 0 | 255 |
short | -32 767 | 32 767 |
unsigned short | 0 | 65 535 |
int | -32 767 | 32 767 |
unsigned | 0 | 65 535 |
long | -2 147 483 647 | 2 147 483 647 |
unsigned long | 0 | 4 294 967 295 |
int32_t | -2 147 483 648 | 2 147 483 647 |
uint32_t | 0 | 4 294 967 295 |
int64_t | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 |
uint_64_t | 0 | 18 446 744 073 709 551 615 |
2.2.2 无符号数的编码
无符号数编码的定义
对向量
最大值是用位向量[11...1]表示
以4位情况为例,
2.2.3 补码编码
补码编码的定义
对向量
最小值是用位向量[10...0]表示
(也就是设置这个位为负权,但是清除其他所有的位)
以4位情况为例,
最大值是用位向量[01...1]表示
(清除具有负权的位,而设置其他所有的位)
以4位情况为例,
注意:
1、补码的范围是不对称的:|TMin| = |TMax| + 1 ,也就是说,TMin没有与之对应的正数。
2、最大的无符号数值刚好比补码的最大值的两倍大一点:
3、-1 和 UMax 有同样的位表示——一个全 1 的串,数值 0 在两种表示方式中都是全 0 的串。
2.2.4 有符号数和无符号数之间的转换
补码转换为无符号数
对满足 的 有:
比如,,同时
推导:补码转换为无符号数
根据上个公式的两种情况,在的补码表示中,位决定了是否为负。
无符号数转换为补码
对满足 的有:
推导:无符号数转换为补码
在u的无符号表示中,对上个公式的两种情况来说,位决定了 u 是否大于
2.2.5 C语言中的有符号数与无符号数
C语言支持所有整型数据类型的有符号和无符号运算。尽管C语言标准没有指定有符号数要采用某种表示,但是几乎所有机器都使用补码。通常,大多数数字都默认为是有符号的。要创建一个无符号常量,必须加上后缀字符 'U' 或者 'u' 。
C语言无符号数和有符号数之间的转换,一般原则是底层的位表示保持不变。
当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么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 截断数字
无符号数的截断结果是:
补码数字的截断结果是:
2.2.8 关于有符号数与无符号数的建议
有符号数到无符号数的隐式转换,是会导致错误或者漏洞的方式,避免这类错误的一种方法就是绝不使用无符号数。
2.3 整数运算
2.3.1 无符号加法
将操作 描述为无符号数加法
对满足 的和有:
检测无符号数加法中的溢出
对在范围 中的和,令。则对计算,当且仅当(或者等价地)时,发生了溢出。
阿贝尔群
每个元素有一个加法逆元。考虑 w 位的无符号数的集合,执行加法运算。对于每个值 x,必然有某个值满足。
无符号数求反
对满足 的任意 ,其位的无符号逆元由下式给出:
2.3.2 补码加法
对满足 之内的整数 和 ,有:
两个数的 位补码之和有完全相同的位级表示。我们可以按如下步骤表示运算:将其参数转换为无符号数,执行无符号数加法,再将结果转换为补码:
检测补码加法中的溢出
对满足 的 和 ,令 。当且仅当 ,但 时,计算 发生了正溢出。当且仅当 ,但 时,计算 发生了负溢出。
2.3.3 补码的非
可以看到范围在 中的每个数字 都有 下的加法逆元,我们将 表示如下:
对满足 的 ,其补码的非 由下式给出
也就是说,对 位的补码加法来说, 是自己的加法的逆,而对其他任何数值 都有 作为其加法的逆。
补码非的位级表示
计算一个位级表示的值的补码非有几种聪明的方法。
1、执行位级补码非的第一种方法是对每一位求补,再对结果加1。在C语言中,我们可以说,对于任意整数值 x ,计算表达式 -x 和 ~x+1 得到的结果完全一样。
2、计算一个数 x 的补码非的第二种方法是建立在将位向量分为两部分的基础之上的。假设 是最右边的 1 的位置,因而 的位级表示形如 。(只要 就能够找到这样的 。)这个值的非写成二进制格式就是 。也就是,我们对位 左边的所有位取反。
2.3.4 无符号乘法
对满足 的 和 有:
2.3.5 补码乘法
对满足 的 和 有:
对于无符号和补码乘法来说,乘法运算的位级表示都是一样的。
2.3.6 乘以常数
乘以 2 的幂
设 为位模式 表示的无符号整数。那么,对于任何 ,我们都认为 给出了 的 位的无符号表示,这里右边增加了 个 0 。
与 2 的幂相乘的无符号乘法
C 变量 x 和 k 有无符号数值 和 ,且 ,则 C 表达式 x<<k 产生数值 。
与 2 的幂相乘的补码乘法
C 变量 x 和 k 有补码值 和 无符号数值,且 ,则 C 表达式 x<<k 产生数值 。
由于整数乘法比移位和加法的代价要大得多,许多 C 语言编译器试图以移位、加法和减法的组合来消除很多整数乘以常数的情况。
对于某个常数 K 的表达式 x * K 生成代码。编译器会将 K 的二进制表示表达为一组 0 和 1 交替的序列:
考虑一组从位位置 n 到位位置 m 的连续的 1(nm)。我们可以用下面两种不同形式中的一种来计算这些位对乘积的影响:
形式A:(x<<n)+(x<<(n-1)+...+(x<<m)
形式B:(x<<(n+1))-(x<<m)
把每个这样连续的 1 的结果加起来,不用做任何乘法,我们就能计算出 x * K 。
2.3.7 除以 2 的幂
整数除法要比整数乘法更慢。除以 2 的幂也可以用移位运算来实现。无符号和补码数分别使用逻辑移位和算术移位来达到目的。
整数除法总是舍入到零。它将向下舍入一个正值,而向上舍入一个负值。
除以 2 的幂的无符号除法
C 变量 x 和 k 有无符号数值 和 ,且 ,则 C 表达式 x >> k 产生数值 。
除以 2 的幂的补码除法,向下舍入
C 变量 x 和 k 有补码值 和 无符号数值,且 ,则当执行算术位移时, C 表达式 x >> k 产生数值 。
对于负数来说,向下舍入结果会有偏差。
除以 2 的幂的补码除法,向上舍入
C 变量 x 和 k 有补码值 和 无符号数值,且 ,则当执行算术位移时, C 表达式 (x+(1<<k)-1) >> k 产生数值 。
2.3.8 关于整数运算的最后思考
- 计算机执行的“整数”运算实际上是一种模运算形式。
- 表示数字的有限字长限制了可能的值的取值范围,结果运算可能溢出。
- 补码表示提供了一种既能表示负数也能表示正数的灵活方法,同时使用了与执行无符号算术相同的位级实现。
- 无论运算数是以无符号形式还是以补码形式表示的,都有完全一样或者类似的位级行为。
2.4 浮点数
浮点表示对形如 的有理数进行编码。它对执行涉及非常大的数字(|V|>>0)、非常接近于0(|V|<<1)的数字,以及更普遍地作为实数运算的近似值的计算,是很有用的。
2.4.1 二进制小数
形如 的表示法,其中每个二进制数字,或者称为位, 的取值范围是 0 和 1 ,这种表示方法表示的数 b 定义如下:
点左边的位的权是 2 的正幂,点右边的位的权是 2 的负幂。
例如, 表示数字 =
形如 的数表示的是刚好小于 1 的数。例如, 表示 ,我们将用简单的表达法 来表示这样的数值。
定点表示法不能很有效的表示非常大的数字。
2.4.2 IEEE 浮点表示
IEEE浮点标准用 的形式来表示一个数:
- 符号(sign) 决定这数是负数()还是正数(),而对于数值 的符号位解释作为特殊情况处理。
- 尾数(significand) 是一个二进制小数,它的范围是 ,或者是 。
- 阶码(exponent) 的作用是对浮点数加权,这个权重是 的 次幂(可能是负数)。
将浮点数的位表示划分为三个字段,分别对这些值进行编码:
- 一个单独的符号位 直接编码符号 。
- 位的阶码字段 编码阶码 。
- 位小数字段 编码尾数 ,但是编码出来的值也依赖于阶码字段的值是否等于 0 。
在单精度浮点格式(C 语言中的 float)中,s、exp 和 frac 字段分别为 位、 位和 位,得到一个 32 位的表示。
在双精度浮点格式(C 语言中的 double)中,s、exp 和 frac 字段分别为 位、 位和 位,得到一个 64 位的表示。
给定位表示,根据 exp 的值,被编码的值可以分成三种不同的情况(最后一种情况有两个变种)。图 2-33 说明了对单精度格式的情况。
情况1:规格化的值
当 exp 阶码域的位模式既不全为 0,也不全为 1 时。
阶码字段被解释为以偏置(biased)形式表示的有符号整数。阶码的值是 ,其中 e 是无符号数,Bias 是一个等于 (单精度是127,双精度是1023)的偏置值。
小数字段 frac 被解释为描述小数值 ,其中 ,其二进制表示为 ,也就是二进制小数点在最高有效位的左边。
尾数定义为 。这种方式叫做隐含的以 1 开头的(implied leading 1)表示,因为可以把 看成一个二进制表达式为 的数字。
情况2:非规格化的值
当 exp 阶码域为全 0 时,所表示的数是非规格化形式。
阶码值是
尾数的值是 ,也就是小数字段的值,不包含隐含的开头的1。
非规格化数有两个用途:
1.提供了一种表示数值 0 的方法。
2.表示那些非常接近于 0.0 的数。
情况3:特殊值
当 exp 阶码域全为 1时,表示特殊值。
当小数域全为 0 时,得到的值表示无穷。当 s=0 时是 ,或者当 s=1 时是 。
当小数域为非零时,结果值被称为 “NaN”,即“不是一个数(Not a Number)”的缩写。
2.4.3 数字示例
图 2-35 展示了假定的 8 位浮点格式的示例,其中有 的阶码位和 的小数位。偏置量是 。
可以观察到最大非规格化数 和最小规格化数 之间的平滑转变。这种平滑性归功于我们对非规格化数的 E 的定义。通过将 E 定义为 1 - Bias ,而不是 -Bias ,我们可以补偿非规格化数的尾数没有隐含的开头的 1。
假如我们将图 2-35 中的值的位表达式解释为无符号整数,它们就是按升序排列的,就像它们表示的浮点数一样。这不是偶然的——IEEE格式如此设计就是为了浮点数能够使用整数排序函数来进行排序。
图 2-36 展示了一些重要的单精度和双精度浮点数的表示和数字值。依据图 2-35 中展示的 8 位格式,我们能够看出有 k 位阶码和 n 位小数的浮点表示的一般属性。
- 值 +0.0 总有一个全为 0 的位表示。
- 最小的正非规格化值的位表示,是由最低有效位为 1 而其他所有位为 0 构成的。它具有小数(和尾数)值 和阶码值 。因此它的数字值是 。
- 最大的非规格化值的位模式是由全为 0 的阶码字段和全为 1 的小数字段组成的。它有小数(和尾数)值 (我们写成 )和阶码值 。因此,数值 ,这仅比最小的规格化值小一点。
- 最小的正规格化值的位模式的阶码字段的最低有效位为 1 ,其他位全为 0 。它的尾数值 ,而阶码值 。因此,数值 。
- 值 1.0 的位表示的阶码字段除了最高有效位等于 0 以外,其他位都等于 1。它的尾数值是 ,而它的阶码值是 。
- 最大的规格化值的位表示的符号位为 0,阶码的最低有效位等于 0,其他位等于 1。它的小数值 ,尾数 (我们写作 )。它的阶码值 ,得到数值
整数值转换成浮点形式,相关的区域对应于整数的低位,刚好在等于 1 的最高有效位之前停止(这个位就是隐含的开头的位 1),和浮点表示的小数部分的高位是相匹配的。
2.4.4 舍入
因为表示方法限制了浮点数的范围和精度,所以浮点运算只能近似地表示实数运算。因此,对于值x,能够找到“最接近的”匹配值x',它可以用期望的浮点形式表示出来。这就是舍入(rounding)运算的任务。
舍入方式有四种:
方式 | 1.40 | 1.60 | 1.50 | 2.50 | -1.50 |
---|---|---|---|---|---|
向偶数舍入 | 1 | 2 | 2 | 2 | -2 |
向零舍入 | 1 | 1 | 1 | 2 | -1 |
向下舍入 | 1 | 1 | 1 | 2 | -2 |
向上舍入 | 2 | 2 | 2 | 3 | -1 |
向偶数舍入是舍入到一个最接近的值。
2.4.5 浮点运算
浮点加法不具有结合性,例如,使用单精度浮点,表达式 (3.14+1e10)-1e10 求值得到 0.0 —— 因为舍入,值 3.14 会丢失。表达式 3.14+(1e10-1e10) 得出值 3.14。
浮点加法满足了单调性属性:如果 ,那么对于任何 a、b 以及 x 的值,除了 NaN,都有 。无符号或补码加法不具有这个实数(和整数)加法的属性。
浮点乘法不具有结合性。例如,单精度浮点情况下,表达式 (1e201e20)1e-20 求值为,而 1e20(1e201e-20) 将得出 1e20。
浮点乘法在加法上不具备分配性。例如,单精度浮点情况下,表达式 1e20(1e20-1e20)求值为 0.0,而 1e201e20-1e20*1e20 会得出 NaN。
对于任何a、b和c,并且a、b和c都不等于 NaN,浮点乘法满足下列单调性:
只要 ,就有
2.4.6 C 语言中的浮点数
所有的 C 语言版本提供了两种不同的浮点数据类型:float 和 double。
当程序文件中出现下列句子时,GUN 编译器 GCC 会定义程序常数 INFINITY(表示)和 NAN(表示 NaN):
#define _GUN_SOURCE 1
#include <math.h>
当在 int、float 和 double 格式之间进行强制类型转换时,程序改变数值和位模式的原则如下(假设 int 是 32 位的):
- 从 int 转换成 float,数字不会溢出,但是可能被舍入。
- 从 int 或 float 转换成 double,因为 double 有更大的范围(也就是可表示值的范围),也有更高的精度(也就是有效位数),所以能够保留精确的数值。
- 从 double 转换成 float,因为范围要小一些,所以值可能溢出成 或。另外,由于精确度较小,它还可能被舍入。
- 从 float 或者 double 转换成 int,值将会向零舍入。例如,1.999 将被转换成 1,而 -1.999 将被转换成 -1。