Lazy loaded imageICS02 数据

type
status
date
slug
summary
tags
category
icon
password
前言(Created by GPT)
前言(Created by GPT)
计算机中最基本的数据是如何表示和操作的?为什么计算机选择二进制,而不是人类更熟悉的十进制?什么是字节,它能够存储多少信息?为什么同样的数会有有符号和无符号之分?浮点数的表示又为何如此复杂,以至于需要专门的国际标准?
在本章中,我们将围绕这些问题展开讨论,帮助你理解:
  • 计算机如何通过二进制存储和表示数据;
  • 数据的基本单位——位与字节的意义,以及它们在数据类型中的应用;
  • 整型和浮点型的编码方式及其独特的表现形式;
  • 位运算和浮点运算的规则与特性,为何它们是高效程序设计的基础;
  • 内存如何组织数据,以及小端序和大端序背后的设计逻辑。
通过这些内容的学习,你将深入了解计算机的底层数据表示及操作方式,为后续编程实践和系统设计打下坚实的基础。

数据的表示

二进制表达

计算机之所以使用二进制,主要是因为底层硬件(如晶体管)基于开关电路来实现。二进制能够以“开关”(0和1)的形式自然适配硬件逻辑。在C语言中,以0b作为二进制数的前缀。
十进制与二进制之间的换算规则如下:
  • 十进制转二进制(dec → bin):使用带余除法。
  • 二进制转十进制(bin → dec):将每一位按其权重(以2的幂次形式)累加。
此外,十六进制表达方式以0x为前缀,编码范围为0-F(不区分大小写)。这是因为16是2的幂,十六进制可以方便地与二进制转换,同时表达数据时更加简洁。
page icon
一些进制的英文及缩写:
十进制:Decimal (DEC) 十六进制:Hexadecimal (HEX) 八进制:Octal (OCT) 二进制:Binary (BIN)

位与字节

一个比特(bit)就是一个二进制位,可以用硬件单元的开与关表示。为了便于存储资源的管理,多个比特被合并为一个字节(byte),字节是存储和编码的基本单位。常用的字符集(如大小写字母、符号、非可见字符等)需要至少256种编码,因此8位作为1字节能够满足需求。
若干字节可以组合成各种数据类型。

常见数据类型及其字节数

  • char(字符类型):1字节
  • int(基本整型):4字节
  • short(短整型):2字节
  • long(长整型):8字节(64位机器)4字节(32位机器)
  • float(单精度浮点型):4字节
  • double(双精度浮点型):8字节
  • long double(高精度浮点型):10或16字节(x86-64架构)
  • pointer(指针):8字节(64位机器)4字节(32位机器)
32位机器和64位机器的区别在于其指针所占用的字节数。从硬件角度来说,这也是其CPU中算术逻辑单元(ALU)支持的位数,直接影响了内存的可访问地址范围。值得注意的是,以Intel的x86-64标准为例,尽管定义为64位,但实际的RAM容量远未达到2^64个存储单元,一般而言只到2^48

位运算

位运算是计算机操作的核心之一;乔治·布尔(George Boole)提出了布尔代数。基本逻辑包括:
  • 与运算(&):两个操作数均为1时结果为1。
  • 或运算(|):任意一个操作数为1时结果为1。
  • 非运算(~):将比特取反。
  • 异或运算(^):两个操作数不同则结果为1。
在C语言中,位运算的优先级低于加减法。位运算可以扩展到比特向量。例如,将一个w位的比特向量视为一个集合,则与、或、非运算可以分别对应集合的交集、并集和补集。

位移运算

  • 左移(<<:高位溢出被丢弃,低位补0。
  • 右移(>>
    • 算术右移:高位补符号位(正数补0,负数补1)。
    • 逻辑右移:高位总是补0。
需要注意,当右移的幅度超出字长范围时,行为是未定义的

与逻辑运算的对比

C语言中的逻辑运算符包括逻辑与(&&)、逻辑或(||)、逻辑非(!)。与位运算不同的是:
  • 逻辑运算将非零视为真,零视为假,并且结果只能是0或1。
  • 逻辑运算具有短路行为:当逻辑与(&&)的左操作数为假时,右操作数不再计算;当逻辑或(||)的左操作数为真时,右操作数不再计算。这种特性在防止空指针访问等场景中非常有用。

整型的编码

无符号与有符号整型

无符号整型直接以二进制存储值。有符号整型则采用补码表示:
  • 正数:直接转为二进制。
  • 负数:绝对值取二进制形式,按位取反后加1。
在补码表示法中,有符号整数的范围为,其中为字长。无符号整数的范围则为
在C语言中,无符号常量可以通过后缀U标注(如100U)。当有符号数和无符号数混用时,会进行隐式类型转换,通常会优先转换为无符号数。

操作规则

  • 符号拓展:通过在高位补符号位(正数补0,负数补1),将较短的有符号整数扩展为较长的有符号整数。
  • 截断:仅保留低位部分的值。

溢出行为

  • 加法溢出:相当于对模数取模。
  • 乘法溢出:更易发生,通常需要使用额外的寄存器存储高位部分。

字节在内存中的组织

在初学阶段,内存通常被视为一个字节数组,每个字节都有唯一的索引,即地址。标号较小的地址成为低地址,标号较大的地址成为高地址。在多字节数据的存储中,地址的排列方式被称为字节序
  • 小端法(Little Endian):从高地址向低地址排;如Android和iOS。
  • 大端法(Big Endian):从低地址向高地址排;如IBM、Oracle。
例如,如果一个int 型变量是0x01234567 ,其地址为0x100
在小端法机器上,实际存储在内存中的字节是:
在大端法机器上,实际存储在内存中的字节是:

浮点型

小数的二进制表示

小数的表示可以通过以下公式实现:
其中, 表示二进制位(0或1)。利用反复取余法(十进制转二进制)和加权法(二进制转十进制),可以实现二进制小数与十进制小数之间的转换。
在二进制小数表示中,可以约定某些比特用于存储整数部分,另一些比特用于存储小数部分,这种表示方法称为定点数。然而,定点数存在局限性:它无法同时处理数量级跨度较大的数据。
为了解决这个问题,引入了浮点数(Floating Point),即允许小数点的位置“浮动”。尽管如此,浮点数在二进制中面临一个重要限制——十进制中的某些有限小数(如1/3或1/5)在二进制中可能变为无限循环小数,导致精度损失。

IEEE浮点数标准

IEEE浮点数标准是当前主流计算机系统中浮点运算的基础,它的引入解决了以下问题:
  1. 扩展数量级的表示范围:支持非常大或非常小的数值。
  1. 进位一致性:避免进位错误。
  1. 控制溢出与下溢:通过特殊编码处理极端情况。
然而,IEEE浮点数标准的实现较为复杂,需要专门的浮点运算单元(FPU)来支持运算。即便是最基础的运算(如加法),也包含了许多复杂的步骤。

IEEE标准浮点数的表示方法

IEEE标准采用三部分编码表示浮点数:
  • 符号位(S):1位,用于表示数值的正负,0为正,1为负。
  • 指数位(Exp):表示指数部分(但不等同于实际指数)。
  • 尾数位(Frac):表示小数部分,通常隐含一个以1为开头的二进制小数。
为了便于区分,我们这里用小写开头的s、e、f来表示小数的符号、指数和尾数;用大写开头的S、Exp、Frac来表示浮点型的符号位、指数位和尾数位。

不同精度的位分配

精度
符号位S
指数位Exp
尾数位Frac
表示范围
有效精度
单精度(32位)
1
8
23
~7位
双精度(64位)
1
11
52
~16位
半精度(16位)
1
5
10
~3位

IEEE标准浮点数大致可以分为三种情况。我们这里先介绍最普遍的情况:

规格化数(Normalized)

也即不为全0或全1的情况。

指数位(Exp)

  • 指数值的编码为:,其中偏置为指数位的比特数)。
  • 单精度中,。有效的编码范围为,因此实际指数
 
page icon
对指数值的编码实际上是移码。这与我们编码整型时使用的补码不同:

补码:正数的补码与原码相同;负数的补码是反码加1。
例如,8位系统中,+5的补码是00000101,-5的补码是11111011

反码:正数的反码与原码相同;负数的反码是符号位不变,其余位取反。
例如,8位系统中,+5的反码是00000101,-5的反码是11111010

移码:通过一个固定的偏移量加到真值上,使所有数的移码为正。
例如,8位系统中,偏移量为127,+5的移码是10000100,-5的移码是01111010

尾数位(Frac)

  • 尾数位表示小数部分,在规格化数中,实际存储的是去掉“1.”后的部分(例如1.xxxxxxxxx中的xxxxxxxxx)。计算时需要将尾数右对齐,尾部补零。

将小数转换为规格化浮点型的例子

例如:有C表达式 float f = 15213.0;
首先,将它转化为二进制科学计数法: 之后,根据其符号取定符号位:正数为 根据其科学计数法中的指数e,加上偏置即得指数位(Exp):,也即10001100 截取f小数点后的部分,构成尾数位(Frac):11011011010000…
因此,最后的结果为:0 10001100 11011011010000… (之后全为0)

以上就是规格化数的转换方法;当指数位Exp 为全0、而尾数位Frac不为0时,浮点数为

非规格化数(Denormalized)

此时,指数位的转换规则变为实际指数(而非)。 尾数位的转换规则为Frac前面加成为实际小数f(而非

将非规格化浮点型转换为小数

例如我们有浮点型的二进制表示:
  1. 确定符号
      • 符号位为 0,表示这是一个正数。
  1. 确定指数e
      • 指数部分全为 0,表示这是一个非规格化数。
      • 非规格化数的实际指数为:
  1. 确定尾数f
      • 非规格化数的尾数部分不隐含前导的 1,而是加上
      • 尾数f为 0.00...001,转换为小数为:2^{-23}
  1. 计算十进制值
      • 公式:
      • 代入值:
  1. 最终结果
而这也是单精度浮点数所能(精确)表示的最小正数。

指数位Exp 为0的另一种情况是零:
  • S=0Exp=0Frac=0时,表示正零。
  • S=1Exp=0Frac=0时,表示负零。
 
page icon
正负零同时存在的意义:
边界处理:在极限和导数运算中,区分正负零可以帮助明确方向。
符号敏感计算:例如1/(+0)为正无穷,1/(-0)为负无穷
 
最后是浮点数的第三种情况:也即指数位Exp 全为1时。此时又分为两种:
  1. 无穷大(Infinity)
      • Exp全1,Frac=0时,表示无穷。
      • 根据符号位,可以分为正无穷和负无穷。这种情况通常发生在除以零等运算中。
  1. 非数(NaN, Not a Number)
      • Exp全1,Frac≠0时,表示NaN。这通常代表非法操作,例如:

动态范围(Dynamic Range)

浮点数的动态范围描述了它能表示的数字大小范围。以8位浮点数(1位符号 + 4位指数 + 3位尾数)为例:
  • 数字从最大到最小依次排列为:
  • 非规格化数在接近0的邻域内提供了更高的精度。这也是我们要在指数位为0时进行非规格化的原因。以下是笔者回课时用以说明的例子,可以看出使用非规格化数时(左侧长条图),能在0周围分布得更为均匀,不会出现“突变”的情况。
notion image

浮点型的比较

  1. 浮点数的比较可以参考无符号整型的规则。
  1. 注意特殊情况:
      • NaN与任何数比较结果均为False,特殊情况:NaN != NaN返回True

浮点型的运算

基本思路

  1. 加法时,需要首先将两个数的指数位对齐
  1. 执行运算后,进行规格化。
  1. 若结果超出范围,则处理溢出。
  1. 按照规则进行舍入。
运算结果的尾数精读超出了该精度浮点数的尾数位精度范围时,需要进行舍入。舍入的规则有:
  1. 向0舍入。
  1. 向下舍入。
  1. 向上舍入。
  1. 向最近的偶数舍入(这是IEEE的默认方法)。它的规则是:
    1. 如果要舍入的值位于两个可表示值的中间(即恰好是中间值),则舍入到最接近的偶数。
    2. 否则,舍入到最接近的可表示值。
以下例子中数的表示均为二进制。例如,假设运算结果的尾数位为 10001,需要舍入到3位尾数位。可以把前3位看成是“整数部分”,后面看成“小数部分”,则问题转化为100.01 舍入到整数。.01 不处在两个整数的中间值,遵循四舍五入原则,舍入为0,因此最终结果为100
再如,假设运算结果的尾数位为 10001,需要舍入到4位尾数位。通过上面的方法,可以看成是1000.1 舍入到整数。此时.1 处在两个整数正中间,因此向最近的偶数,也就是1000 舍入。
同样地,10011舍入到4位尾数位,结果应该是1010

乘法规则

  1. 符号位s相加并对2取模。
  1. 实际指数e相加,若溢出则结果为无穷。
  1. 小数部分f相乘,并按指定精度舍入。

加法规则

  1. 首先对齐尾数:将指数较小的一方尾数右移。
  1. 根据符号位s执行加减运算。
  1. 规范化、舍入,并处理特殊情况。

C语言中的浮点型规则

  1. 浮点转整型:向0舍入。
  1. 整型转浮点
      • int → double:精确转换。这是由于int的上限(2147483647)可以被double 精确表示,不需舍入。
      • int → float:非精确转换,需舍入。

Prev
ICS01 概述
Next
ICS03 汇编语言
Loading...
Article List
SunVapor的小站
计算机系统导论
文档