很多人第一次被进制转换绊住不是因为数学难而是因为教材把它讲成了一张需要背下来的表。十进制、二进制、八进制、十六进制四个词摆在一起加上“逢几进一”的定义看着像常识真到用的时候——比如读一段内存数据、看一个颜色值、分析一个权限位掩码——脑子里那根线就断了。我自己刚接触底层调试的时候对着一个十六进制地址反复数位数数了三遍三个结果最后发现是把0x前缀也算进了有效位。这种错误不丢人但它说明一件事进制转换不是靠记忆能稳住的技能得靠一套能自我校验的方法。这篇内容想解决的就是这个问题。它面向的是需要和底层数据打交道的开发者、正在补计算机基础的学生以及任何被“0b”“0o”“0x”这些前缀搞晕过的人。我会把二进制、八进制、十六进制各自的表示方式、它们之间的转换逻辑、以及实际工作中真正会用到它们的场景拆开讲重点放在“为什么这样转”和“怎么转不出错”上而不是罗列一堆换算公式。读完之后你应该能做到看到一个数就能判断它的进制、心算常见的转换、并且知道在代码里怎么安全地处理这些表示。1. 进制这件事先搞清楚它在表示什么1.1 位权所有进制共用的底层规则不管几进制一个数的本质都是“每一位上的数字乘以这一位的权重再全部加起来”。这个权重就是位权等于基数的位置次方。十进制里352拆开是3×10² 5×10¹ 2×10⁰二进制里1011拆开是1×2³ 0×2² 1×2¹ 1×2⁰逻辑完全一样只是基数从 10 换成了 2。理解这一点之后很多“转换技巧”就不需要死记了。比如为什么二进制转十进制可以“从右往左遇到 1 就加对应的 2 的幂”因为它就是位权展开的简化写法。为什么十六进制一位能对应二进制四位因为 16 是 2 的 4 次方一位十六进制的取值范围 0 到 15 恰好能被四位二进制完整覆盖。这些关系不是巧合是基数之间的幂次关系决定的。我建议在动手转换之前先把 2 的幂次表在脑子里过一遍2⁰ 到 2¹⁰ 分别是 1、2、4、8、16、32、64、128、256、512、1024。这张表是二进制心算的地基后面所有的快速转换都建立在它上面。十六进制同理记住 16⁰1、16¹16、16²256、16³4096处理地址和颜色值时够用了。1.2 四种进制的分工不是随便定的十进制是给人看的因为人有十根手指这是历史选择没什么技术必然性。二进制是给硬件看的因为数字电路里一个开关只有通和断两种稳定状态用两个符号表示最可靠。八进制和十六进制则是给“人看二进制”做折中的产物——二进制太长11010110这种串一眼看不出值但每三位或每四位归一组读起来就轻松多了。八进制每三位二进制对应一位十六进制每四位对应一位。为什么现在十六进制更常见、八进制相对少见因为现代计算机的字长普遍是 8 的倍数8 位、16 位、32 位、64 位8 能被 4 整除但不能被 3 整除所以用十六进制分组时字节边界天然对齐一个字节正好两位十六进制。八进制在早期 12 位、36 位字长的机器上更自然如今主要出现在文件权限这类特定场景里。进制基数每位取值范围典型用途二进制20-1硬件状态、位运算、掩码八进制80-7文件权限、部分旧式表示十进制100-9人类日常读写十六进制160-9、A-F内存地址、颜色值、字节数据这张表值得记牢尤其是“每位取值范围”这一列。很多人转换出错根源是写出了0b102或者0o78这种非法表示——二进制里不该出现 2八进制里不该出现 8。写之前先扫一眼有没有越界字符能挡掉一大半低级错误。1.3 前缀约定代码里怎么区分一个数是几进制纯数字写在纸上11到底是十一还是三二进制还是十七十六进制光看字面分不出来。所以编程语言普遍用前缀来标记进制这套约定跨语言基本一致0b或0B开头表示二进制如0b10110o或0O开头表示八进制如0o17注意不是0开头那是老式写法容易和十进制混淆0x或0X开头表示十六进制如0x1F不带前缀默认十进制这里有个历史坑要提一句C 语言早期用前导0表示八进制010其实是八进制的 8 而不是十进制的 10。这个设计后来被很多语言继承又逐渐弃用因为太容易看错。现在写代码八进制一律用0o显式标注别用裸0开头这是能省掉无数调试时间的习惯。提示十六进制里的字母 A-F 大小写不敏感0x1f和0x1F是同一个值。但团队协作时建议统一大小写风格混用虽然语法合法读起来费劲。2. 二进制与其他进制之间的转换逻辑2.1 十进制转二进制除二取余为什么从下往上读十进制转二进制最标准的做法是“除二取余逆序排列”。拿156举例156÷278 余 078÷239 余 039÷219 余 119÷29 余 19÷24 余 14÷22 余 02÷21 余 01÷20 余 1。把余数从最后一次往前读得到10011100。为什么是逆序因为第一次除出来的余数是最低位2⁰ 位最后一次除出来的余数是最高位。除法的过程是从低位往高位剥离读的时候自然要反过来。这个道理讲清楚之后就不容易在“正着读还是倒着读”上犯迷糊了。验证结果有个快速办法把得到的二进制按位权加回去看是否等于原数。10011100 1281684 156对上了。养成“转完必验算”的习惯比任何技巧都可靠。心算熟练之后还可以用“凑幂次”的方式反向操作156 最接近的 2 的幂是 128减去剩 2828 里能放 16剩 1212 里能放 8剩 44 正好是 2²。所以 156 1281684对应位是 2⁷、2⁴、2³、2²写出来10011100。这个方法比除法快适合口算。2.2 二进制转十进制位权展开的两种心算路径二进制转十进制本质就是位权展开求和。101101从右往左数第 0 位是 1第 2 位是 1第 3 位是 1第 5 位是 1其余是 0所以值是 2⁰2²2³2⁵ 14832 45。心算时有两条路径可选。一条是“从左往右累加翻倍”从最高位开始每读一位就把当前结果乘 2 再加这一位的值。101101的过程是1→(1×20)2→(2×21)5→(5×21)11→(11×20)22→(22×21)45。这条路径不需要记 2 的幂次表适合位数多、幂次大的情况。另一条就是前面说的“认 1 的位置加对应幂次”适合位数少、1 比较稀疏的情况。两条路都练熟遇到什么数都能挑快的走。2.3 二进制与八进制、十六进制的分组对应二进制转八进制从右往左每三位一组不足三位左边补零。1011010分组为1 011 010补零成001 011 010每组对应八进制一位0011、0113、0102结果是132。反过来八进制转二进制每位拆成三位即可132拆成001 011 010去掉前导零得1011010。二进制转十六进制同理每四位一组。1011010分组为101 1010补零成0101 1010对应5A。十六进制每位拆四位5A拆成0101 1010。这里的关键是分组方向必须从右往左因为低位对齐是数值表示的基本要求从左边分组会把位权搞错。二进制八进制十六进制000000011101022011331004410155110661117710001081001119101012A101113B110014C110115D111016E111117F这张对照表不用全背但 0 到 F 这十六个值对应的四位二进制要能条件反射。实际工作中最常查的就是这张表尤其是调试时看到0xF要立刻反应出是1111看到0xA反应出1010。2.4 十进制与八进制、十六进制的互转十进制转八进制或十六进制思路和转二进制一样只是除数换成 8 或 16叫“除基取余”。156转十六进制156÷169 余 12即 C9÷160 余 9逆序读得9C。验证9×1612 14412 156正确。转八进制156÷819 余 419÷82 余 32÷80 余 2逆序读得234。验证2×643×84 128244 156。反过来八进制或十六进制转十进制就是位权展开。9C 9×16¹ 12×16⁰ 14412 156。234 2×8² 3×8¹ 4×8⁰ 128244 156。这里要注意十六进制里的字母要换算成数值再参与计算C 就是 12别直接拿字母去乘。实际转换时如果要在十进制和十六进制之间来回倒中间借道二进制往往更快因为十六进制和二进制是四位对齐的拆起来没有除法。156先转二进制10011100再四位一组1001 1100得9C比除法少一步心算负担。这个技巧在处理大量数据时特别有用。3. 代码里处理多进制的实操细节3.1 各语言的字面量写法与解析函数不同语言对进制的支持程度不一样写代码时得心里有数。下面这张表是我常用的对照覆盖了主流场景语言二进制字面量八进制字面量十六进制字面量字符串转数值Python0b10110o170x1Fint(s, base)JavaScript0b10110o170x1FparseInt(s, base)Java0b1011017前导00x1FInteger.parseInt(s, base)C/C无标准字面量017前导00x1Fstrtol(s, NULL, base)Go0b10110o170x1Fstrconv.ParseInt(s, base, 64)Python 的int(s, base)是最灵活的base 传 2、8、16 都行甚至传 0 会自动识别前缀。JavaScript 的parseInt有个坑不传第二个参数时以0开头的字符串在某些历史环境下会被当成八进制现代规范虽然改成默认十进制了但显式传 base 永远是更稳的写法。Java 的八进制用前导0017是十五这个和 C 一致但和 Python、JS 的0o风格不同跨语言切换时容易写错。# Python 里的进制转换示例 value 156 print(bin(value)) # 0b10011100 print(oct(value)) # 0o234 print(hex(value)) # 0x9c print(int(10011100, 2)) # 156 print(int(9c, 16)) # 156// JavaScript 里的进制转换示例 let value 156; console.log(value.toString(2)); // 10011100 console.log(value.toString(8)); // 234 console.log(value.toString(16)); // 9c console.log(parseInt(10011100, 2)); // 156 console.log(parseInt(9c, 16)); // 156注意toString(16)输出的是小写字母而有些场景比如颜色值习惯大写需要自己toUpperCase()。这类细节不影响数值正确性但影响输出的一致性做数据比对时要留意。3.2 位运算与掩码十六进制真正的主场十六进制在代码里出现频率最高的地方不是“转换练习”而是位运算和掩码。一个 32 位整数用十六进制写出来是 8 位每一位对应 4 个二进制位看掩码的时候一目了然。比如要取一个数的低 8 位掩码写0xFF比写255或0b11111111都直观——FF两个字符就说明了“两个字节里的低字节全 1”。// 取低 8 位、高 8 位的典型写法 uint32_t value 0x12345678; uint8_t low value 0xFF; // 0x78 uint8_t high (value 24) 0xFF; // 0x12这里0xFF的作用是“只保留最低 8 位其余清零”。为什么右移之后还要再与一次0xFF因为右移会把高位补进来对有符号数是符号扩展再与一次能确保结果干净。这个细节在调试时经常被忽略导致取出来的值带着高位垃圾。权限系统也是十六进制的经典场景。Linux 文件权限755是八进制拆开是7读写执行、5读执行、5读执行每位八进制对应三个二进制位正好是 rwx 三个权限位。这就是为什么权限用八进制而不是十进制——三位一组天然对应三个权限标志用十进制493表示755就完全看不出结构了。3.3 颜色值与字节序十六进制的日常面孔前端开发里#FF8800这种颜色值本质是三个字节的十六进制表示红FF、绿88、蓝00。每个分量 0 到 255正好一个字节。理解这一点之后做颜色计算就顺了想把红色减半把FF右移一位得7F约 127而不是去算十进制的 255÷2。字节序是另一个绕不开的点。一个 32 位整数0x12345678存到内存里小端序机器上低字节在前内存里看到的是78 56 34 12大端序则是12 34 56 78。调试时从内存 dump 里读出来的字节顺序和代码里写的十六进制顺序可能相反这不是数据错了是字节序在起作用。判断方法很简单写一个已知值进去看内存里的字节排列就能确定当前平台的字节序。注意做跨平台数据交换比如网络协议、文件格式时协议通常会规定统一的字节序网络字节序是大端收发两端都要做转换别假设对方和自己一样。3.4 转换中的边界与溢出进制转换本身不改变数值但落到具体的数据类型上就有边界了。一个 8 位无符号数最大是0xFF即 255你写0x1FF赋给 8 位变量高位会被截断结果是0xFF。这种截断在编译期可能只给个警告运行时悄无声息是很多诡异 bug 的来源。处理大数时还要注意精度。JavaScript 的Number是双精度浮点安全整数范围到 2⁵³-1超过这个范围用十六进制表示再转回来可能丢精度。需要精确处理 64 位整数时用BigIntBigInt(0xFFFFFFFFFFFFFFFF)能正确表示而parseInt会丢。Python 的整数是任意精度没这个问题但转成其他语言的定长类型时同样要检查范围。// 大整数的正确处理 let big BigInt(0xFFFFFFFFFFFFFFFF); console.log(big.toString(16)); // ffffffffffffffff // 用 Number 会丢精度 console.log(Number(0xFFFFFFFFFFFFFFFF)); // 18446744073709552000末位已不准4. 转换出错时怎么一步步排查4.1 先查字符合法性再查分组方向转换结果不对第一步不是重算而是检查输入里有没有非法字符。二进制串里出现 2 到 9、八进制串里出现 8 或 9、十六进制串里出现 G 以后的字母这些都是直接错误。写个简单的正则扫一遍比盯着数字看半天快得多。第二步查分组方向。二进制转十六进制必须从右往左四位一组如果从左往右分1011010会分成1011 010补零成1011 0100得B4而正确答案是5A差得离谱。这个错误在手工计算时特别常见因为人习惯从左往右读。养成“先在最右边画一条基准线往左分组”的习惯能避免这类错误。4.2 用反向转换做交叉验证算完之后把结果转回原进制看是否等于输入。这是最可靠的验证手段成本也低。十进制转十六进制得到9C就把9C按位权展开算回十进制看是不是 156。二进制转八进制得到132就把132每位拆三位拼回二进制看是不是1011010。正反两次结果一致基本可以放心。如果两次不一致错误通常出在“取余后读的顺序”或者“分组补零”上。除基取余要从下往上读分组补零要补在左边。这两个点是最容易翻车的地方排查时优先怀疑。4.3 前缀和符号位的陷阱带前缀的字符串转数值时解析函数的行为要确认清楚。parseInt(0x1F)不传 base 时能识别0x前缀返回 31但parseInt(0b1011)不传 base 时可能只解析到0就停了因为老版本规范不认0b。稳妥做法是剥掉前缀再传明确的 base或者用语言提供的专用解析方法。负数在二进制里的表示更是个大坑。-1的 8 位补码是11111111即0xFF但如果你把它当无符号数读就是 255。做位运算和进制转换时先明确“这个数是有符号还是无符号”再决定怎么解释最高位。同一个字节0xFF有符号解读是 -1无符号解读是 255两种都对取决于上下文。现象可能原因排查动作结果差一个数量级分组方向反了检查是否从右往左分组结果高位多出垃圾右移后未掩码补 0xFF之类的掩码大数末位不准浮点精度丢失改用 BigInt 或任意精度整数负数变成大正数有符号/无符号解读不一致确认类型和补码解释解析提前终止前缀未被识别剥前缀后显式传 base4.4 一个真实踩坑的完整链路早些年我处理一段设备上报的数据协议文档说某个字段是“2 字节十六进制大端序”。我拿到原始字节0A 1F按大端拼成0x0A1F得 2591和预期对不上。排查过程是这样的先确认字节没读错dump 出来确实是这两个字节再确认字节序协议写的大端我拼的顺序没错然后怀疑字段偏移往前挪了一个字节重新读还是不对最后翻协议附录才发现这个字段实际是“小端序”文档正文写错了。把0A 1F按小端拼成0x1F0A得 7946和预期吻合。这个坑的教训是字节序不能只信文档正文要以实际数据特征和附录为准必要时用已知值做验证。另外十六进制字符串和字节数组是两回事0A1F是四个字符[0x0A, 0x1F]是两个字节转换时别把字符数当字节数。5. 把进制转换练成条件反射的几个方法5.1 建立自己的速查锚点不需要背完整的转换表但要有几个锚点值。我常用的锚点是0x1016、0x2032、0x4064、0x80128、0xFF255、0x100256、0x10004096。这几个值覆盖了字节边界和常见的内存块大小看到就能反应。二进制这边101010、110012、111115 这几个要熟因为它们是十六进制 A、C、F 的二进制形态。锚点的作用是在心算时提供参照。比如看到0x2A先想到0x2032再加0xA10得 42。看到0b110101拆成110和101分别是 6 和 5八进制是65再转十进制 6×8553。有锚点做跳板比从零开始算快得多。5.2 在日常场景里刻意练习进制转换是肌肉记忆光看不动手没用。我自己的练法是把它嵌进日常操作里看颜色值时心算 RGB 分量、读文件权限时把755拆成二进制想一遍 rwx、调试时看到内存地址先猜一下大概范围再看实际值。这些练习不需要专门抽时间顺手就做了坚持一段时间反应速度会明显不一样。另一个有效的练法是“双向默写”随便写一个十进制数转成三种进制再转回来全程不查表。错了就记下错在哪一步是分组方向、取余顺序还是字符换算。错误记录比正确结果更有价值因为它暴露的是思维习惯里的漏洞。5.3 工具要用但不能依赖计算器和在线转换工具该用就用尤其是处理长串数据的时候手工算既慢又容易错。但工具的作用是“验证”和“批量处理”不是“替代理解”。我的习惯是手工算出结果再用工具核对不一致就查原因。长期下来手工计算的准确率会逼近工具这时候工具就退化成纯粹的效率手段而不是拐杖。写代码时也一样语言内置的转换函数要用但要知道它内部大概怎么做的、边界在哪。比如parseInt遇到非法字符会截断而不是报错这个行为在数据校验时可能放过脏数据需要额外检查。理解工具的行为边界比记住它的调用方式更重要。进制表示和转换这套东西说到底是一层“人和机器之间的翻译”。机器只认高低电平人需要可读的符号进制就是这中间的桥。桥怎么搭、为什么这么搭、哪里容易塌搞清楚之后剩下的就是熟练度问题。我在实际使用中最大的体会是别追求“一眼看穿”追求“每一步都能验证”。心算再快也有出错的时候但只要有反向验证的习惯错误就漏不过去。这个习惯比任何速算技巧都值钱。