1. 为什么值得花时间把数制转换彻底搞明白
刚入行那几年,我一直觉得数制转换是学校里应付考试的东西,工作中用到再查一下就行。直到有次调试一个传感器驱动,数据手册上写着寄存器地址0x4002 3800,偏移量0x1C,而我在代码里用十进制写偏移量,怎么算都不对,白白折腾了一下午。那次之后我才真正把二进制、八进制、十进制、十六进制这套东西系统梳理了一遍。
数制本质上就是同一堆数量的不同表达方式。十进制之所以成为日常默认,是因为人有十根手指;计算机用二进制,是因为电路只有通和断两种稳定状态;八进制和十六进制被发明出来,纯粹是因为二进制写起来太长,人眼扫过去容易数错。理解这一点,后面的转换就不再是死记硬背,而是顺理成章的换算。
这篇文章主要面向三类人:一是刚接触编程或者嵌入式的新手,看到0x、0b就发怵;二是做网络、运维、逆向的朋友,经常要在不同进制之间来回跳;三是准备考试或者面试,需要把转换过程讲清楚的人。我会把原理、手算方法、代码实现、常见坑都过一遍,尽量做到看完就能上手。文中涉及参数和步骤的地方,我会把推导过程写出来,而不是只丢一个结论。
2. 数制的本质:位权、基数与表示法
2.1 位权到底是什么,一个生活化的类比
先说一个最核心的概念:位权。你手里有一叠钱,面额分别是 100、10、1,那数字 345 就表示 3 张 100、4 张 10、5 张 1,加起来 345。这里的 100、10、1 就是位权,而 10 是基数,因为每往左一位,代表的量就乘以 10。
计算机的世界里,基数变成了 2。二进制数字 1011,从右往左的位权依次是 1、2、4、8,所以它等于 1×1 + 1×2 + 0×4 + 1×8 = 11。这个"乘基数、加结果"的动作,就是所有进制转十进制的基本方法,没有例外。
把这个思路推广开:
- 八进制基数 8,位权是 8 的幂:1、8、64、512……
- 十进制基数 10,位权是 10 的幂:1、10、100、1000……
- 十六进制基数 16,位权是 16 的幂:1、16、256、4096……
提示:位权是理解一切进制转换的钥匙。凡是搞不清某个数怎么来的,先把它每一位的位权列出来,问题基本就迎刃而解。
2.2 基数决定了几套符号够用
基数决定了这个进制需要多少个不同的数字符号。十进制需要 0 到 9 共十个符号,够用。二进制只要 0 和 1 两个。八进制要 0 到 7 八个。十六进制最特殊,需要十六个符号,可阿拉伯数字只有 0 到 9 十个,怎么办?于是借用了字母 A 到 F,分别代表 10、11、12、13、14、15。
这也是为什么你在代码里经常看到0xFF这种写法,F 就是 15,两个 F 拼起来就是 255。刚接触的人容易卡在"A 为什么等于 10",其实没有为什么,就是约定,就跟扑克牌里 J 代表 11 一样,用起来方便而已。
不同语言和工具对进制的书写约定大同小异,下面这张表建议存一下:
| 进制 | 常见前缀 | 示例 | 典型场景 |
|---|---|---|---|
| 二进制 | 0b | 0b1010 | 位运算、掩码、寄存器标志位 |
| 八进制 | 0或0o | 0o17或017 | Linux 文件权限、早期 C 代码 |
| 十进制 | 无 | 255 | 日常计算、业务逻辑 |
| 十六进制 | 0x | 0xFF | 内存地址、颜色值、字节流 |
需要特别留个心眼的是八进制。C 语言里以0开头的数字会被当成八进制,所以int x = 010;里 x 实际是 8,不是 10。这个坑我踩过,当时排查一个数组下标越界,最后发现是某个常量前面手滑多了个零。
2.3 十六进制为什么和二进制是"亲兄弟"
十六进制在计算机领域地位特殊,原因就一个:16 是 2 的 4 次方。这意味着每一个十六进制位,刚好对应四个二进制位,不多不少。
具体对应关系是固定的,背下来收益极高:
| 十六进制 | 二进制 | 十六进制 | 二进制 |
|---|---|---|---|
| 0 | 0000 | 8 | 1000 |
| 1 | 0001 | 9 | 1001 |
| 2 | 0010 | A | 1010 |
| 3 | 0011 | B | 1011 |
| 4 | 0100 | C | 1100 |
| 5 | 0101 | D | 1101 |
| 6 | 0110 | E | 1110 |
| 7 | 0111 | F | 1111 |
有了这张表,二进制和十六进制之间的转换就变成了"四换一、一拆四"的拼图游戏,完全不需要做乘法和除法。比如1101 1010,直接对着表拆成D和A,结果就是0xDA。反过来0x3F7,拆成3F7,各自展开成0011 1111 0111就行了。
八进制同理,只不过它是 2 的 3 次方,所以是"三位一组"。这也是为什么八进制在现代编程里用得越来越少——四位一组比三位一组更贴合字节的边界(一个字节 8 位,刚好两个十六进制位)。
3. 四类进制的核心转换方法全拆解
3.1 任意进制转十进制:按位权展开求和
这是最基础也最通用的方法。把每一位数字乘以它对应的位权,然后全部加起来。
以二进制11010为例,从右往左标位权:
位数: 1 1 0 1 0 位权: 16 8 4 2 1 计算: 1×16 + 1×8 + 0×4 + 1×2 + 0×1 = 16 + 8 + 2 = 26八进制237转十进制:
7×1 + 3×8 + 2×64 = 7 + 24 + 128 = 159十六进制2AF转十进制,注意 A=10、F=15:
F×1 + A×16 + 2×256 = 15 + 160 + 512 = 687这套方法对小数部分同样适用,只不过位权变成负幂次。比如二进制0.101:
1×2⁻¹ + 0×2⁻² + 1×2⁻³ = 0.5 + 0 + 0.125 = 0.625注意:手算时遇到很长的数,建议从右往左数,把位权写在数字上方,这样不容易错位。错位是手算最常见的失误,一旦某一位偏了,后面全错。
3.2 十进制转其他进制:整数除基取余,小数乘基取整
整数部分用除基取余、逆序排列。以十进制 156 转二进制为例:
156 ÷ 2 = 78 ... 余 0 ← 最低位 78 ÷ 2 = 39 ... 余 0 39 ÷ 2 = 19 ... 余 1 19 ÷ 2 = 9 ... 余 1 9 ÷ 2 = 4 ... 余 1 4 ÷ 2 = 2 ... 余 0 2 ÷ 2 = 1 ... 余 0 1 ÷ 2 = 0 ... 余 1 ← 最高位把余数从下往上读:10011100。验证一下:128+16+8+4=156,对的。
转八进制和十六进制思路完全一样,只是除数换成 8 和 16。156 转十六进制:
156 ÷ 16 = 9 ... 余 12(C) 9 ÷ 16 = 0 ... 余 9逆序读是9C,验证:9×16+12=156。
小数部分则相反,用乘基取整、顺序排列。以十进制 0.6875 转二进制为例:
0.6875 × 2 = 1.375 → 取整 1,保留 0.375 0.375 × 2 = 0.75 → 取整 0,保留 0.75 0.75 × 2 = 1.5 → 取整 1,保留 0.5 0.5 × 2 = 1.0 → 取整 1,保留 0取整结果按顺序读:0.1011。验证:0.5+0.125+0.0625=0.6875,吻合。
这里就要说到那个热搜问题了:十进制小数转换为二进制有精度限制时需要考虑舍入吗?答案是必须考虑,而且这是很多人踩过的真实坑。因为二进制只能精确表示那些分母是 2 的幂次的分数,像 0.1、0.2、0.3 这种十进制小数,转成二进制是无限循环的。用有限位数表示就必然要截断或者舍入。
举个实际例子,很多语言里0.1 + 0.2 != 0.3,就是因为这个。如果你在做金额计算、科学计算这类对精度敏感的场景,别用浮点数,改用整数(以分为单位)或者专门的十进制库(比如 Python 的decimal、Java 的BigDecimal)。至于舍入方式,常见的有四舍五入、银行家舍入(round half to even)、向上/向下取整,金融场景优先考虑银行家舍入,能减少系统性偏差。
3.3 二进制与八进制、十六进制的快捷转换
前面说过,十六进制一位对四位,八进制一位对三位。利用这个特性可以跳过十进制直接互转,速度飞快。
二进制转十六进制:从小数点开始,往左往右各四位一组,不足补零。
二进制: 101 1010 1111 分组: 0101 1010 1111 (最左边不足四位,补个零) 结果: 5 A F → 0x5AF二进制转八进制:同样分组,但是三位一组。
二进制: 101101011 分组: 101 101 011 (从右往左三位一组) 结果: 5 5 3 → 0o553提示:最左边和最右边不足位时补零,补的是数值位方向的零,不要补错位置。左边补零不改变数值,右边补零会改变数值,所以小数部分右侧补零要慎重——实际上转换时小数部分是从左往右分组,不足的在最右补零,这是为了凑齐分组,数值上是等价的。
3.4 负数与补码:绕不开的一道坎
热搜里"负数的二进制""二进制补码"出现频率很高,说明这是大家的痛点。计算机里表示负数,主流方案是补码。
以 8 位为例,正数 5 是0000 0101。要得到 -5,步骤是:先取反得到1111 1010,再加 1 得到1111 1011。这个1111 1011就是 -5 的补码表示。
为什么要这么麻烦?因为用补码可以让减法和加法共用一套电路。5 + (-5)用补码算:
0000 0101 + 1111 1011 ----------- 1 0000 0000 ← 最高位的进位溢出丢弃结果是 0,完美。如果不用补码用原码,减法电路和加法电路就得分开设计,硬件成本高得多。
补码还有个好处是 0 只有一种表示,而原码和反码里 +0 和 -0 是两种,判断起来麻烦。
实操中要注意的是溢出。8 位有符号数范围是 -128 到 127,129 就溢出了。当你看到1000 0000这个值,如果是无符号就是 128,有符号就是 -128,临界点要格外小心。
4. 代码和工具里的数制转换实操
4.1 用 Python 和 C 快速验证转换结果
Python 内置了转换函数,调试时非常方便:
# 十进制转其他进制 print(bin(156)) # 0b10011100 print(oct(156)) # 0o234 print(hex(156)) # 0x9c # 其他进制转十进制 print(int('10011100', 2)) # 156 print(int('234', 8)) # 156 print(int('9C', 16)) # 156 # 处理小数 from decimal import Decimal, getcontext getcontext().prec = 30 print(Decimal(0.1)) # 展示浮点数的真实存储值C 语言里,printf的格式说明符可以直接输出不同进制:
#include <stdio.h> int main() { int n = 156; printf("十进制: %d\n", n); // 156 printf("八进制: %o\n", n); // 234 printf("十六进制: %x\n", n); // 9c printf("十六进制大写: %X\n", n); // 9C return 0; }这里有个细节:printf没有二进制格式符,因为 C 标准没纳入。要输出二进制得自己写循环或者用非标准扩展。另外%o输出八进制时前面不会自动加0,%#o才会加,%#x会加0x。
注意:C 里
scanf读十六进制用%x,但如果输入超过int范围,结果未定义。处理大数要用strtoul之类的函数,并检查errno。
4.2 十六进制编辑器和二进制分析场景
做逆向或者排查文件格式问题时,十六进制编辑器是必备工具。像 HxD 这类工具,打开任意文件都能看到每个字节的十六进制值,右侧配上对应的 ASCII 字符显示。
举个例子,PNG 文件开头固定是89 50 4E 47 0D 0A 1A 0A。其中50 4E 47对应 ASCII 的 "PNG",89是高位字节,用来检测传输过程有没有被错误处理成文本模式。看懂这些,你就能快速判断一个文件是不是伪装了扩展名。
CTF 里常见的二进制题目,比如栈溢出、rip控制这类,本质也是要计算出精确的偏移量和地址。这些地址在反汇编器里都是十六进制,你要能快速把十六进制地址和内存布局对应起来。字节序(大端小端)也是这里的关键,0x12345678在小端机器上内存里是78 56 34 12,顺序反的。
4.3 数据库和其他工具中的进制玩法
通达信这类软件里,十六进制代码有时会出现在数据文件或者公式编码里。处理这类数据时,关键是把十六进制字节流正确地解码成有意义的数值或字符串,注意编码和字节序。
容器运维场景里也会遇到二进制相关的东西,比如某些组件的二进制分发包部署,用 systemd 管理时监听本地 socket 文件。这些路径配置在服务单元里通常以绝对路径给出,排查连接问题时要确认进程实际监听的路径和权限是否符合预期。
颜色领域更是十六进制的天下。网页和设计工具里#RRGGBB是标配,前两位是红、中间两位是绿、后两位是蓝,每个通道 0 到 255。像陶土白这种色号,查出来的 hex 值就是设计师和前端之间交接的通用语言。理解十六进制的位权,你就能心算出不同颜色值的亮度大概在什么区间。
5. 常见问题速查与避坑经验
5.1 数制转换常见问题排查表
实际工作中遇到的问题,很多是细节没处理干净。我把最常见的几类整理成表,方便对照:
| 问题现象 | 可能原因 | 排查方法 | 解决方法 |
|---|---|---|---|
| 数值大了一倍 | 误把八进制当十进制 | 检查前缀是否有前导 0 | 去掉前导零或改成十进制 |
| 负数结果异常 | 忽略了补码表示 | 确认是有符号还是无符号 | 明确类型和位宽 |
| 小数转换不精确 | 二进制无法精确表示 | 用高精度库观察真实值 | 改用整数或十进制库 |
| 十六进制地址算错 | 位权错位或字节序搞反 | 逐位列出位权重新算 | 确认大小端,重新分组 |
| 颜色值对不上 | 遗漏 # 或长度不对 | 检查是否为 6 位完整格式 | 补齐六位,区分大小写 |
5.2 手算提速的几个实用技巧
第一个技巧是先转十六进制再转二进制。比如要算十进制 5000 的二进制,直接除 2 要算十几次,但如果先算出十六进制1388,再一位拆四位,快得多,也不容易错。
第二个技巧是记住几个常用位权值。二的 1 到 16 次方背下来,转换时可以直接套:2、4、8、16、32、64、128、256、512、1024、2048、4096、8192、16384、32768、65536。这几个数在编程里出现频率极高,尤其 1024、65536 这类,看到就条件反射。
第三个技巧是善用分组。超过 10 位的二进制数,用人眼直接读很容易数错,从右往左每四位加个空格或者分隔符,瞬间清晰。很多编辑器和终端也支持高亮显示分组,用起来很顺手。
5.3 那些文档里不会写的坑
坑一:八进制的权限表示。Linux 里chmod 755里的 755 是八进制,7 表示读+写+执行(4+2+1),5 表示读+执行(4+1)。如果你用十进制思维理解 755,算出来完全不对。这种场景下,理解数制不是学术问题,是直接关系到权限是否配置正确。
坑二:浮点比较别用等号。前面说过 0.1 的二进制表示有误差,所以0.1 + 0.2 == 0.3在某些语言里返回 false。正确做法是比较绝对差值是否小于一个极小的阈值,比如abs(a - b) < 1e-9。金融计算则干脆不要用浮点。
坑三:位运算的符号扩展。对负数做移位或者与运算时,有符号数的符号位会扩展。比如-1 >> 1在很多语言里还是 -1,而不是一个大正数。做位掩码时,优先用无符号类型,避免符号位捣乱。
坑四:字符串和二进制数据的边界。像 C 里的strstr()这类字符串函数是用来处理以\0结尾的文本的,中间一旦出现\0就会提前截断。二进制内存里随时可能包含\0,所以查找二进制数据要用memmem或者带长度的比较函数,不能直接套字符串函数。这个坑在做协议解析时特别容易翻车。
坑五:字节序的隐蔽性。网络协议规定用大端,x86 是小端。当你手工拼接多字节数值时,如果不做转换,得到的会是反的。十六进制看起来都是一样的字符,但在内存里的排列顺序才是真相。
6. 硬件与进阶场景里的数制应用
6.1 键盘扫描与编码中的十六进制
用 Verilog HDL 设计一个十六进制键盘的扫描和编码电路,这个场景把数制用到了硬件层面。矩阵键盘通常是 4 行 4 列,扫描时逐行拉低,读列线状态,判断哪个键被按下。
十六进制键盘有 16 个键,从 0 到 F,每个键对应一个唯一的四位二进制编码。比如按下 "A",编码器输出1010。这个输出可以直接用于后续的显示驱动或者数据总线。
设计要点在于消抖。机械按键按下和释放时会有几十毫秒的抖动,扫描电路需要用计数器做延迟确认,或者用移位寄存器做状态过滤。如果扫描频率太高又没消抖,一个按键会被识别成多次,这是新手常见的问题。
另外编码器的优先级也要考虑。如果多个键同时按下,得有明确的优先级规则,通常按矩阵位置(低位优先或者高位优先)来确定输出。这是组合逻辑设计中容易忽略的细节。
6.2 从数制角度看数据存储与传输
磁盘、内存、网络,底层都是字节流。理解数制让你能"看见"这些字节。比如一个 32 位整数0x01020304,在内存里按小端存储是04 03 02 01。你在十六进制编辑器里看到的就是这个顺序。
网络抓包时,协议头的字段长度、标志位都定义到比特级别。TCP 头里那些标志位(SYN、ACK、FIN)各占一位,本质上是同一个字节里不同位置上的二进制位。理解位权,你就能自己算出标志位的组合值,也能从十六进制值反推出哪些标志位被置位了。
机器学习和人工智能领域也离不开数制。模型权重常见的是 FP16、BF16 这类浮点格式,它们的指数位和尾数位分配不一样,直接影响到模型的精度和显存占用。BF16 指数位和 FP32 一样是 8 位,尾数只留 7 位,牺牲精度换范围;FP16 指数 5 位、尾数 10 位,范围小但精度高一些。选择哪种格式,本身就是数字表示原理在实际工程里的取舍。
7. 写在最后的一点个人体会
数制转换这东西,看起来简单,但要真正用得顺,靠的是反复练和踩坑。我的建议是不要只背结论,而是每次遇到一个进制问题,都花三十秒在心里过一遍位权,时间长了就成了本能反应。
具体练习方式上,可以每天随手拿几个数做转换,比如看看时间戳的十六进制,看看文件大小的二进制,坚持一两周,基本就刻进肌肉记忆了。工具方面,手头常备一个十六进制编辑器和一个带多进制显示的计算器,遇到不确定的立刻验证,别靠猜。
另外提醒一句,涉及金额、坐标、传感器读数这类场景,一定要在动手前想清楚用什么数据类型和精度。数制选择错一次,排查成本可能远超预期。这是我最想传达的一条经验。