[CSAPP]2-1信息的表示和处理
现代计算机存储和处理都是以二进制处理。
我们考虑三种重要的编码:
- 无符号编码:表示大于0的整数,例如C/C++里面的unsigned int, unsigned char。
- 二进制补码:用于表示有符号整数,例如C/C++里面的int, char。
- 浮点数编码:浮点编码是表示实数的科学计数方式,例如C/C++里面的float, double.
信息存储
块
大多数计算机使用8位的块,也叫做字节,它为最小的可寻址的寄存器单位
计算机内存可以看作一个数组,数据存储以块(字节)为最小存储单位,对应的每个块(字节)都有个标识数,称为地址,所有的地址集合就称之为虚拟地址空间
十六进制表示法
对于每个字节,二进制范围为$(00000000~11111111)_2$,十进制范围为$(0~255)_{10}$,事实都不直观,一般习惯用十六进制来表示
$(0x00~0xFF)$
数据大小
计算机和编译器以不同方式来编码数据,C语言中支持整数和浮点数的编码,且不同数据字长也有差异,如表
C声明 | 32位(字节) | 64位(字节) |
---|---|---|
char | 1 | 1 |
short char | 2 | 2 |
int | 4 | 4 |
long int (long) | 4 | 8 |
T* | 4 | 8 |
float | 4 | 4 |
double | 4 | 8 |
寻址和字节顺序
数据的每个字节,在内存中每字节都存储在连续的地址空间中。且根据存储的方式分为大端和小端
例如C语言中,假设有个变量int x = 0x123456
,且位于地址0x100
处,那么地址范围0x100~0x103
的字节顺序。
存储方式如图
我们可以写个demo来直观观察数据的存储顺序
代码:
#include <stdio.h>
#include <string.h>
typedef unsigned char* byte_pointer;
void show_bytes(byte_pointer start, int len) {
int i;
for (i = 0; i < len; i++) {
printf(" %p", &start[i]);
}
puts("");
for (i = 0; i < len; i++) {
printf(" %.2x", start[i]);
}
puts("\n");
}
void show_int(int x) {
show_bytes((byte_pointer) &x, sizeof(int));
}
void show_float(float x) {
show_bytes((byte_pointer) &x, sizeof(float));
}
void show_pointer(void* x) {
show_bytes((byte_pointer) &x, sizeof(void*));
}
void test_show_types(int x) {
int ival = x;
float fval = (float) x;
int* pval = &ival;
show_int(ival);
show_float(fval);
show_pointer(pval);
}
int main() {
test_show_types(0x123456);
}
运行结果:
整数表达
下面以C/C++的数据类型为例
无符号整数类型
例如usigned int、usigned car无符号整数类型编码是以原码形式,假设整数类型有w位,将位向量表示成$\vec{x}$,写成$[x_{w-1},x_{w-2}..x_0]$表示向量中每一位
利用下面公式转换成十进制:
$$ B2T(\vec x) = \sum^{w-1}_{i=0}x_i2^i $$
有符号整数类型
例如int、long有符号数据编码是以补码形式编码,假设有符号整数类型有w位,将位向量表示成$\vec{x}$,写成$[x_{w-1},x_{w-2}..x_0]$表示向量中每一位
利用下面公式转换成十进制:
$$ B2T(\vec x) = -x_{w-1}2^{w-1}+\sum^{w-2}_{i=0}x_i2^i $$
我们可以直观的观察出最高位,及$w-1$位觉得的符号的正负。
Q:有符号整数为啥用将不将最高位为符号位,其他位以原码的形式表示,这样不直观吗?
A:将破坏二进制的加法运算规则。
PS:
- 有符号类型数据的正整数补码及原码
- 原码 = 补码取反 + 1
无符号整数和有符号整数之间转换
两则转换的基本原则就是二进制数位不变,编码形式改变。
C语言允许两者之间强制转换,也常常发生隐式转换导致出现意想不到的问题
unsigned int x = 1;
// -1转换成无符号类型
if (-1 < x) {
std::cout << "ok" << std::endl;
} else {
std::cout << "unexception" << std::endl;
}
扩展一个数的位表示
不同字长的整数通常可能进行类型转换,当从字长短的类型转换成字长长的时:
- 无符号整数转换成更大类型:进行零扩展,及简单的在最高位前添加$0$
- 有符号整数转换成更大类型:进行符号位扩展,及在最高位前添加符号位的数字,如$[x_{w-1},x_{w-2}...x_0]$转换成$[x_{w-1},...,x_{w-1},x_{w-2},...,x_0]$
截断数字
当将有符号或无符号数字转换成子长短的的类型,会将数字截断。
例如w位的整数$\vec x = [x_w, w_{w-1},...,w_0]$,转换成k位时,丢弃最高位w-k位,即转换成$\vec x = [x_{k-1},x_{k-2},...,x_0]$
有符号和无符号整除间建议
由于有符号和无符号之间转换和运行,经常出现我们难以检查出的问题。可以遵循下面建议
- 当我们将数字进行计算,我们可以在算数只用有符号计算。
- 当数值没有实际,仅仅当作把每个数位当做元素或把数字掩码,可以用无符号整数
浮点数
IEEE 浮点数表示
IEEE浮点使用$V = (-1)^s \times M \times 2^E$来表示一个数
- 符号:s符号位,0正,1负
- 有效位:M是个二进制小数
- 指数:E是2的幂
浮点数被编码成3个部分
- 一位单独符号位s
- k位指数域$exp = e_{k-1}e_{k-2}...e_{0}$编码指数E,根据exp的值,E将编码成三种不同情况
- n位小数域$frac = f_{n-1}f_{n-2}...f_0$编码有效位M,编码分成依赖指数域是否为0讨论
下面分情况讨论以上编码
规格化值
当exp不为全为0(十进制0)和全为1(十进制单精度255,双精度2047)时为规格化值
这时指数$E = e - Bias$,其中$e=e_{k-1}...e_1e_0$,而$Bias = 2^k-1$(单精度是127,双精度是1023)
$M =1 + f$,其中小数域$f = 0.f_{n-1}f_{n-2}...f_0$
非规格化值
当exp全为0时为非规格化
这时$E = 1 - Bias$
$M = f$
特殊数值
当exp全为1时为特殊 数值
当小数域全为0时,表示无穷,且当s=0时$-\infty$,s=1时$+\infty$
当小数域为全为0时,表示NaN,即不是一个数,如计算$\sqrt{-1},-\infty + \infty$
舍入
由于精度问题,浮点计算需要考虑舍入,其中分为
- 向上舍入
- 向下舍入
- 向零舍入
- 向偶数舍入
特殊说明:向偶数舍入可以减少有效位
浮点运算
浮点运算每次运算都存在舍入,即假设某种运算$\otimes$,则浮点$z = Round(x \otimes y)$,我们很快会发现由于浮点的舍入我们会缺少结合律的性质
浮点与整形的转换
- int转换成float,数字不会溢出,但会舍入(有效位减少)
- int或float转换成double,会有更高的精度,所以可以保存精确的数值
- 从double转换成float,因为float范围小会溢出,(产生$-\infty,+\infty$),另外精度较小还可能舍入
- 从float和double转换成int,值会向零截断(超出int范围即会截断成int边界)
#include <iostream>
#include <cmath>
using namespace std;
int main () {
float x = -2e10;
int y = x;
std::cout << x << " " << y << std::endl;
// 结果 -2e+10 -2147483648
int xx = 123456789;
float yy = xx;
std::cout << xx << " " << yy << std::endl;
//结果 123456789 1.23457e+08
}
此处评论已关闭