ICS02 数据
type
status
date
slug
summary
tags
category
icon
password
前言(Created by GPT)
计算机中最基本的数据是如何表示和操作的?为什么计算机选择二进制,而不是人类更熟悉的十进制?什么是字节,它能够存储多少信息?为什么同样的数会有有符号和无符号之分?浮点数的表示又为何如此复杂,以至于需要专门的国际标准?
在本章中,我们将围绕这些问题展开讨论,帮助你理解:
- 计算机如何通过二进制存储和表示数据;
- 数据的基本单位——位与字节的意义,以及它们在数据类型中的应用;
- 整型和浮点型的编码方式及其独特的表现形式;
- 位运算和浮点运算的规则与特性,为何它们是高效程序设计的基础;
- 内存如何组织数据,以及小端序和大端序背后的设计逻辑。
通过这些内容的学习,你将深入了解计算机的底层数据表示及操作方式,为后续编程实践和系统设计打下坚实的基础。
数据的表示
二进制表达
计算机之所以使用二进制,主要是因为底层硬件(如晶体管)基于开关电路来实现。二进制能够以“开关”(0和1)的形式自然适配硬件逻辑。在C语言中,以
0b
作为二进制数的前缀。十进制与二进制之间的换算规则如下:
- 十进制转二进制(dec → bin):使用带余除法。
- 二进制转十进制(bin → dec):将每一位按其权重(以2的幂次形式)累加。
此外,十六进制表达方式以
0x
为前缀,编码范围为0-F
(不区分大小写)。这是因为16是2的幂,十六进制可以方便地与二进制转换,同时表达数据时更加简洁。一些进制的英文及缩写:
十进制: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浮点数标准是当前主流计算机系统中浮点运算的基础,它的引入解决了以下问题:
- 扩展数量级的表示范围:支持非常大或非常小的数值。
- 进位一致性:避免进位错误。
- 控制溢出与下溢:通过特殊编码处理极端情况。
然而,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)
- 指数值的编码为:,其中偏置(为指数位的比特数)。
- 单精度中,。有效的编码范围为,因此实际指数。
对指数值的编码实际上是移码。这与我们编码整型时使用的补码不同:
补码:正数的补码与原码相同;负数的补码是反码加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(而非)
将非规格化浮点型转换为小数
例如我们有浮点型的二进制表示:
- 确定符号:
- 符号位为
0
,表示这是一个正数。
- 确定指数e:
- 指数部分全为
0
,表示这是一个非规格化数。 - 非规格化数的实际指数为:。
- 确定尾数f:
- 非规格化数的尾数部分不隐含前导的 1,而是加上
- 尾数f为
0.00...001
,转换为小数为:2^{-23}
。
- 计算十进制值:
- 公式:
- 代入值:
- 最终结果:
而这也是单精度浮点数所能(精确)表示的最小正数。
指数位
Exp
为0的另一种情况是零:- 当
S=0
且Exp=0
,Frac=0
时,表示正零。
- 当
S=1
且Exp=0
,Frac=0
时,表示负零。
正负零同时存在的意义:
边界处理:在极限和导数运算中,区分正负零可以帮助明确方向。
符号敏感计算:例如
1/(+0)
为正无穷,1/(-0)
为负无穷最后是浮点数的第三种情况:也即指数位
Exp
全为1时。此时又分为两种:- 无穷大(Infinity):
- 当
Exp
全1,Frac=0
时,表示无穷。 - 根据符号位,可以分为正无穷和负无穷。这种情况通常发生在除以零等运算中。
- 非数(NaN, Not a Number):
- 当
Exp
全1,Frac≠0
时,表示NaN。这通常代表非法操作,例如:
动态范围(Dynamic Range)
浮点数的动态范围描述了它能表示的数字大小范围。以8位浮点数(1位符号 + 4位指数 + 3位尾数)为例:
- 数字从最大到最小依次排列为:
- 非规格化数在接近0的邻域内提供了更高的精度。这也是我们要在指数位为0时进行非规格化的原因。以下是笔者回课时用以说明的例子,可以看出使用非规格化数时(左侧长条图),能在0周围分布得更为均匀,不会出现“突变”的情况。

浮点型的比较
- 浮点数的比较可以参考无符号整型的规则。
- 注意特殊情况:
- NaN与任何数比较结果均为
False
,特殊情况:NaN != NaN
返回True
。
浮点型的运算
基本思路
- 加法时,需要首先将两个数的指数位对齐
- 执行运算后,进行规格化。
- 若结果超出范围,则处理溢出。
- 按照规则进行舍入。
运算结果的尾数精读超出了该精度浮点数的尾数位精度范围时,需要进行舍入。舍入的规则有:
- 向0舍入。
- 向下舍入。
- 向上舍入。
- 向最近的偶数舍入(这是IEEE的默认方法)。它的规则是:
- 如果要舍入的值位于两个可表示值的中间(即恰好是中间值),则舍入到最接近的偶数。
- 否则,舍入到最接近的可表示值。
以下例子中数的表示均为二进制。例如,假设运算结果的尾数位为
10001
,需要舍入到3位尾数位。可以把前3位看成是“整数部分”,后面看成“小数部分”,则问题转化为100.01
舍入到整数。.01
不处在两个整数的中间值,遵循四舍五入原则,舍入为0,因此最终结果为100
再如,假设运算结果的尾数位为
10001
,需要舍入到4位尾数位。通过上面的方法,可以看成是1000.1
舍入到整数。此时.1
处在两个整数正中间,因此向最近的偶数,也就是1000
舍入。同样地,
10011
舍入到4位尾数位,结果应该是1010
。乘法规则
- 符号位s相加并对2取模。
- 实际指数e相加,若溢出则结果为无穷。
- 小数部分f相乘,并按指定精度舍入。
加法规则
- 首先对齐尾数:将指数较小的一方尾数右移。
- 根据符号位s执行加减运算。
- 规范化、舍入,并处理特殊情况。
C语言中的浮点型规则
- 浮点转整型:向0舍入。
- 整型转浮点:
int → double
:精确转换。这是由于int
的上限(2147483647)可以被double
精确表示,不需舍入。int → float
:非精确转换,需舍入。
Prev
ICS01 概述
Next
ICS03 汇编语言
Loading...