[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位(字节)
char11
short char22
int44
long int (long)48
T*48
float44
double48

寻址和字节顺序

数据的每个字节,在内存中每字节都存储在连续的地址空间中。且根据存储的方式分为大端和小端

例如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
}
最后修改:2023 年 10 月 18 日
如果觉得我的文章对你有用,请随意赞赏