☰
十进制与二进制转换全解析:手算、代码、小数精度与补码
2026/10/1 3:47:52 网站建设 项目流程

上周帮同事排查一段串口回传的位图日志,报文里全是 0 和 1 组成的串,他盯着屏幕看了十几分钟,也没算出第三段八位到底代表几。我接过纸笔,把 11011001 拆成 128、64、16、8、1,报出 217,他愣了两秒说,原来你能心算十六进制。其实哪有什么天赋,无非是二进制和十进制的互相转换做得多了,把 8421 这几个权值刻进了脑子里,看到一串 bit 就能条件反射地分块。进制转换这个东西,课本上讲两页就翻过去了,但真到了干活的时候,它是很多问题的第一道门槛。

写这篇的起因很直接:不管是看协议文档、调寄存器配置、读内存 dump,还是算校验和、对浮点误差,你总会撞上“这串 0 和 1 到底是几”或者“这个数写成二进制长什么样”的问题。很多人对整数转换还算有手感,一碰到小数、负数补码、十六进制混着来的场景就开始心虚,只能打开计算器一个数一个数地按。这篇就是想把十进制与二进制互相转换这件事,从手算到写代码、从小数精度到补码表示,完整地捋一遍,顺带把几个特别容易踩的坑摊开讲清楚。新手能照着往下走,有基础的可以挑小数精度和补码那两节对一下自己的理解。

1. 进制转换到底在解决什么问题

1.1 十进制和二进制各自的主场

先说清楚一件事:十进制和二进制不是两种“对错”的表示方法,它们只是同一堆数量的两套衣服。我们平时习惯十进制,是因为人有十根手指,数起来顺手;机器用二进制,是因为电路只有通和断两种稳定状态,用高低电平表示 1 和 0 最省事、最不容易出错。两套衣服之间来回换,就是进制转换要干的事。

十进制的主场在“给人看”的环节:报表、金额、日志里的计数、界面上的数值,除了极少数场景,全都是十进制。二进制的主场在“给机器用”的环节:内存里存的、总线上传的、位域里塞的标志位,本质都是二进制。所以进制转换的真实含义,往往不是一道数学题,而是“人机之间的翻译”。

我见过不少刚入行的朋友,把进制转换理解成一种考试技能,觉得工作里用不上。等到要读一份寄存器手册,发现人家写的是“bit7..bit4 表示分频系数”,或者要手动算一个 IP 掩码、一个 CRC 表,才反应过来这玩意儿是日常工具。理解这两种表示各自的适用场景,你才知道什么时候该换算、换算完要给谁看。

1.2 位权是所有转换方法的统一底色

不管是整数还是小数、正数还是负数,转换方法看起来花样很多,但底层只有一条规则:位权。一个二进制数,从右往左数,第 n 位的权重是 2 的 n 次方,n 从 0 开始。十进制同理,第 n 位权重是 10 的 n 次方。所谓“转换”,本质就是把一个数按一套权展开,再按另一套权重新打包。

举个最直观的例子:二进制 1101,从右往左分别是 2^0=1、2^1=2、2^2=4、2^3=8,把有 1 的位挑出来相加,8+4+0+1=13。反过来,要把 13 写成二进制,就是看 13 能由哪些 2 的幂拼出来:13 里有一个 8,剩 5;5 里有一个 4,剩 1;1 正好是 2^0。于是对应位填 1,得到 1101。

把这条规则记住,后面所有的除 2 取余、乘 2 取整、补码取反加一,都能找到出处,而不是一堆孤立的操作口诀。我个人建议学进制转换的时候,先把“位权”这两个字在心里过一遍,每次不确定方法对不对,就用一个简单数(比如 5 或者 10)手工验证一下,比死记步骤靠谱得多。

2. 十进制整数转二进制:两种主流手法

2.1 除2取余法:从低位到高位的稳定流程

十进制整数转二进制,最标准的方法是“除 2 取余,逆序排列”。步骤说起来简单:用这个数不断除以 2,每次记下余数,直到商为 0 为止,最后把余数从下往上读出来,就是二进制结果。它的正确性来自位权——每一次除以 2,相当于把最低位挤出来当余数,剩下的商就是去掉最低位之后的数字。

拿热词里高频出现的 217 走一遍完整流程:

步骤被除数除以2的商余数
12171081
2108540
354270
427131
51361
6630
7311
8101

把最右边一列从下往上读:1 1 0 1 1 0 0 1,也就是 11011001。回头用位权验证一下:128+64+16+8+1=217,对上了。这个方法的优点是机械、不容易出错,缺点是位多了之后要写一长串除法,八位还好,三十二位就有点烦。

顺带说一句,很多资料会把“除 2 取余”和“除 2 取整”混着叫,其实没区别,关键动作是取余、逆序。我见过有人把余数按正序抄下来,写成 10011011,结果差了十万八千里,这是最常见的错。养成习惯:每写完一次结果,用两三个高位的权值反算一下,几秒钟的事。

2.2 二进制扩展法:从高位往下凑数

除 2 取余适合“照着算”,但如果你像我一样经常需要心算,可以换一套更快的路子,圈里一般叫“二进制扩展法”或者“权值拼凑法”。思路是:先把比目标数小的 2 的幂按从大到小排好(比如 512、256、128、64、32、16、8、4、2、1),然后从最大的能装下的权值开始往下减,能减就记 1,减不动就记 0,直到减到 0。

还是用 217:先排好 128、64、32、16、8、4、2、1。128 能装下,记 1,余 89;64 能装,记 1,余 25;32 装不下,记 0;16 能装,记 1,余 9;8 能装,记 1,余 1;4 装不下,记 0;2 装不下,记 0;1 能装,记 1,余 0。连起来就是 11011001,和除 2 取余的结果一致。

这个方法练熟之后速度非常快,尤其是你只需要知道“第几位是 1”的时候。比如要判断某个数落在哪个区间,你甚至不用算完整结果,只看最高的那个 2 的幂就够了。实际工作中我处理位域配置,基本都是用这套方法,因为硬件手册给的往往也是“这个位控制什么”,而不是完整的数值。

2.3 两种方法的取舍与操作上的注意事项

这两套方法不是谁替代谁的问题,而是场景不同。除 2 取余胜在流程固定、可以无脑执行,适合写进程序、适合位数多又不想动脑的时候;扩展法胜在快、容易心算、还能顺便告诉你最高有效位在哪,适合现场调试、读手册、估算范围。我自己的习惯是:小于 256 的数直接扩展法口算,大于 256 或者需要保证零失误的场合就老老实实除 2 取余,或者直接交给代码。

注意:扩展法排权值的时候,一定要把比目标数大一档的权值也列出来,否则容易漏位。比如算 217,如果你只排到 128,就会下意识觉得“不够用”,实际上 128 已经能装下了,只是剩下的部分还需要右移几位继续凑。

还有一个非常容易踩的坑:补零。很多时候我们按字节操作,要求结果凑够 8 位、16 位、32 位。217 算出来是 8 位刚好,但如果算出来是 1101(13),放到一个字节里就得写成 00001101。我在读嵌入式日志的时候被这个坑过:上位机打印的是十进制 13,我手算成 1101 写进配置,结果程序按 8 位解析,高位补零倒是没错,但我另一次把 00001101 和 11010000 的字节顺序搞反了,导致一整块配置错位。字节序和补零,是两个必须分开确认的东西,别混在一起想。

3. 二进制转十进制:按权展开与速算技巧

3.1 按权展开求和的规范做法

二进制转十进制是反方向的位权操作:从右往左,第 n 位是 1 就把 2 的 n 次方加进去,是 0 就跳过,最后求和。这套流程没什么可解释的,但它是后面一切的基础,包括小数、补码、校验计算,用的都是同一个思路。

拿 11011001 举例:从右往左逐位看,第 0 位是 1,加 1;第 1 位是 0,跳过;第 2 位是 0,跳过;第 3 位是 1,加 8;第 4 位是 1,加 16;第 5 位是 0,跳过;第 6 位是 1,加 64;第 7 位是 1,加 128。总计 1+8+16+64+128=217,和前面的结果完全闭合。

我建议新手在练习阶段老老实实把过程写出来,别跳步,尤其是位数超过 8 位的时候。人脑同时记住六七个数字相加很容易串位,我自己的做法是分高低两半算:低四位一个结果,高四位一个结果,最后相加。11011001 拆成 1101 和 1001,前者是 13,后者是 9,按权重高四位要乘 16,所以 13×16+9=217。这种方法位数越多越省心,八位、十六位都适用。

3.2 8421法、分组法与十六进制的桥梁作用

八位以内的二进制转十进制,最实用的速算工具是8421 法。因为四位二进制的权值刚好是 8、4、2、1,而一位十六进制的取值是 0 到 15,两者完全对应。所以你可以把二进制每四位切一刀,每一刀直接读成一位十六进制,再把十六进制转十进制,比直接数位要快得多。

具体做法:二进制从右往左四位一组,左边不够四位就补零。11011001 切成 1101 和 1001,查 8421:1101=8+4+1=13,对应十六进制 D;1001=8+1=9。于是 11011001 就是 0xD9,再换算成十进制,13×16+9=217。这套“二进制—十六进制—十进制”的三级跳,是我日常用得最多的链路,因为十六进制在手册、日志、内存 dump 里到处都是,见得多就熟。

再补一个速查表,平时手算可以直接对:

二进制十进制十六进制
000000
000111
001022
001133
010044
010155
011066
011177
100088
100199
101010A
101111B
110012C
110113D
111014E
111115F

这张表我建议直接背下来,它带来的效率提升是复利式的。你看内存里的一串字节,能瞬间知道每个字节大概是多大,不需要按计算器。

3.3 hex转十进制的实操步骤

单独把 hex 转十进制拎出来说,是因为它的实际出现频率比纯二进制高得多。做法有两条路:一条是逐位按权展开,第 n 位(从右数,从 0 开始)的权是 16 的 n 次方,把每一位的数字乘上权值再相加;另一条是借道二进制,先把每位十六进制展开成四位二进制,再合并成二进制数,按二进制的方法算十进制。

以 0x1A3 为例。逐位展开:3×16^0=3,A×16^1=10×16=160,1×16^2=256,合计 256+160+3=419。借道二进制的话,1 是 0001,A 是 1010,3 是 0011,拼成 000110100011,去掉前导零是 110100011,按权展开:256+128+32+2+1=419,结果一致。

我的经验是:小数字用逐位展开,大数字用分组法先折算成十六进制再算。比如给你一串 32 位二进制 10110101111000010000000000000011,直接按权加会疯掉,先分组转成 0xB5E10003,再逐位展开:B 是 11,乘 16^7;5 是 5,乘 16^6;E 是 14,乘 16^5;1 乘 16^4;后面三个字节都是 0 和 3。这样虽然还是要算高次幂,但心理负担小得多,而且分组过程顺便完成了校验。

4. 小数部分:乘2取整、精度与舍入

4.1 十进制小数转二进制:乘2取整的完整流程

小数部分转二进制,口诀是“乘 2 取整,顺序排列”。把小数不断乘 2,每次记下整数部分(0 或 1),用剩下的小数部分继续乘,直到小数部分为 0 或者达到要求的位数,最后把整数部分按从上到下的顺序读出来。这个方法和整数的除 2 取余是对称的,只不过方向反了。

以 0.625 为例:0.625×2=1.25,整数部分 1,余下 0.25;0.25×2=0.5,整数部分 0,余下 0.5;0.5×2=1.0,整数部分 1,余下 0。停止,结果按顺序读是 101,所以 0.625 的二进制是 0.101。验证一下:1/2 + 0/4 + 1/8 = 0.5+0.125=0.625,闭合。

再换一个不那么“干净”的例子:0.1。0.1×2=0.2 取 0;0.2×2=0.4 取 0;0.4×2=0.8 取 0;0.8×2=1.6 取 1,余 0.6;0.6×2=1.2 取 1,余 0.2……你会发现余数开始循环,0.1 在二进制里是一个无限循环小数,写作 0.0001100110011...(0011 循环)。这就是为什么浮点数存 0.1 永远存不准的根本原因,不是电脑算错了,是 0.1 这个数在二进制里本来就表示不完。

注意:整数部分和小数部分必须分开转换,最后拼起来。很多人图省事把 13.625 当成整体去除 2,结果整数部分和小数部分全乱套。正确做法是 13 转成 1101,0.625 转成 101,拼起来 1101.101。

4.2 精度限制与舍入策略怎么定

热词里有人问“十进制小数转换为二进制有精度限制时需要考虑舍入吗”,答案是必须考虑,而且这不是可选项。因为绝大多数十进制小数在二进制下都是无限循环的,你只能截断到某个位数,截断就意味着误差。问题不是“要不要舍入”,而是“舍入到哪里、用什么规则、误差能不能接受”。

工程上的常见做法有这么几种。第一种是定点数,把小数放大成整数处理,比如金额统一用“分”为单位存整数,1.23 元存成 123,这样全程不涉及小数,也就没有二进制表示误差。这就是会计、支付系统里普遍不用浮点数做金额计算的原因,业内有时候开玩笑叫“会计十进制”。第二种是指定精度的二进制浮点,接受 IEEE 754 的误差范围,但比较时用容差而不是等号,比如判断 |a-b| < 1e-9。第三种是十进制定点库或字符串十进制,直接从根上避开二进制表示的问题。

舍入规则本身也有讲究。单精度浮点的有效精度大约是 7 位十进制,双精度大约是 15 到 16 位。如果你把十进制小数截断到 24 位二进制有效位(单精度的尾数长度),最后一位几乎必然需要舍入。常见规则是“就近舍入,遇到正好一半看末位是否偶数”,也就是 IEEE 754 默认的 round-to-nearest-even。这个规则的好处是大量运算时误差不容易单向累积。

我踩过的坑是:用浮点数做累加统计,几百次加法之后结果和预期差了一点几,追溯原因就是每次加法各自的小误差被累积起来了。后来改成整数累加(把数值乘以 1000 存成整数),最后再除回去,问题消失。这件事让我养成了一个习惯:凡是涉及“必须精确”的数值计算,先问一句能不能用整数表示。

4.3 二进制小数转十进制

反方向就是把每一位按 2 的负次幂展开。小数点后第一位权重是 1/2,第二位是 1/4,第三位是 1/8,依次类推。0.101 就是 1×1/2 + 0×1/4 + 1×1/8 = 0.625。位数多的时候,先把它当整数展开,最后除以 2 的位数次方,速度更快。

举个例子:0.0001100110011,假设我们截取到 13 位小数,那么把这 13 位当成整数看,是 0001100110011,去掉前导零是 1100110011,十进制等于 512+256+32+16+2+1=819。总位数是 13,所以结果是 819/8192≈0.0999755859375。原本是 0.1,误差约 2.4e-5,这就是截断带来的误差量级。

这个方法在排查浮点问题时特别有用。当你在代码里打印一个 float 得到 0.100000001490116,你可以用这套方法倒推它实际存的二进制是多少、误差有多大,从而判断这个误差是不是正常范围。很多人看到浮点打印出长尾巴就慌,其实这是正常现象,关键是误差有没有超出你的业务允许范围。

5. 负数与补码:最反直觉的一块

5.1 原码、反码、补码的由来

纸面上表示负数,最直觉的做法是“前面加个负号”,但在二进制电路里没有负号这个符号,只有 0 和 1。早期有人提出原码:最高位当符号位,0 表示正,1 表示负,剩下的位表示绝对值。这方案看着合理,但有两个致命问题:一是 0 有两个表示(正零 0000 和负零 1000),二是加减法要分开处理正负号,电路会复杂得多。

于是有了反码:正数不变,负数在符号位为 1 的前提下,其余位按位取反。反码解决了部分问题,但零还是有两个表示,而且负数运算的进位处理还是有边界情况。最终胜出的是补码:负数按位取反之后再加 1。补码的妙处在于,它让加减法可以用同一套电路完成,a 减 b 直接变成 a 加 b 的补码,符号位自然参与运算,零也只有一种表示。

补码之所以成立,核心逻辑是“用模运算把减法变加法”。在固定位宽下,最高位的进位会溢出丢弃,相当于做了一次取模。以 8 位为例,模是 256,-1 就等于 256-1=255,而 255 的二进制是 11111111,正好就是 1 取反加一的结果。这个思路一旦想通,补码就不再是死记硬背的东西了。

5.2 负数二进制转换的实操与验证

具体操作:先把这个负数的绝对值转成二进制,按要求的位宽写全,然后全部按位取反,最后加 1,得到的就是补码表示。以 -27 举例,位宽 8 位:27 的二进制是 00011011;按位取反得到 11100100;加 1 得到 11100101。所以 -27 的 8 位补码是 11100101。

反过来,看到 11100101 怎么知道它是负几?最高位是 1,说明是负数,先减 1 得 11100100,再按位取反得 00011011,也就是 27,加负号得 -27。或者用位权直接算:最高位权值是 -128,其余位正常相加,-128+64+32+4+1=-27。两种方法我都用,前者更机械不容易错,后者更快能看出大小。

数值8位补码验证
270001101116+8+2+1
-2711100101-128+64+32+4+1
-111111111-128+64+32+16+8+4+2+1
-12810000000只有最高位为1

这里有个必须记住的边界:位宽决定了取值范围。8 位有符号数的范围是 -128 到 127,不对称。为什么负数多一个?因为 00000000 表示 0,占用了正数那半边的一个位置,所以负数比正数多一个。同理,16 位有符号范围是 -32768 到 32767,32 位是约 -21 亿到 21 亿。写代码的时候如果搞错了位宽,算出来的负数会完全不对。

5.3 二进制加减法与除法的实际操作

补码解决了减法,于是二进制四则运算里,加减法是同一套逻辑:逐位相加,逢二进一,带符号位一起算,最高位的进位丢弃。举个 8 位例子:13-11 等价于 00001101 + (-11 的补码 11110101) = 100000010,8 位截断是 00000010,也就是 2,正确。

二进制除法比其他运算麻烦一些,本质上和十进制竖式除法一模一样,只是判断商位的时候不是试 1 到 9,而是看“够不够减一个除数”。每次从被除数的高位往下取足够的位数,和除数比较,够减就商 1 并做减法,不够就商 0 并把下一位落下来。以 1100100(100)除以 101(5)为例:从高位取 110,够减 101,商 1,余 1;落下一位变成 10,不够减,商 0;再落下一位变成 100,不够减,商 0;落下一位变成 1000,够减,商 1,余 11;落下一位变成 110,够减,商 1,余 1;落下一位变成 10,不够减,商 0。商是 10100,也就是 20,正是 100/5 的结果。这个过程和十进制长除法完全同构。

实际写代码的时候,整数的乘除 2 一般不用真做除法,而是用移位代替:左移一位等于乘 2,右移一位等于除以 2(对正数是整除,对负数是向下取整,这点要小心)。位运算比乘除指令快,也是很多性能敏感代码里常见的优化手段。不过编译器现在基本会自动帮你做这个优化,手写移位更多是为了对齐硬件寄存器操作,比如解析位域时的移位或掩码,那是另一回事。

6. 把转换写成代码:C语言实操与工程要点

6.1 手写十进制与二进制互转函数

手工算得再熟,量大了还是得靠代码。下面这两个函数是我平时调试用的最小实现,一个负责十进制转二进制字符串,一个负责二进制字符串转十进制,逻辑简单、没有依赖,直接拷进临时程序就能跑。

#include <stdio.h> /* 十进制无符号整数转二进制字符串,buf 至少 33 字节 */ int dec_to_bin(unsigned int n, char *buf) { char tmp[33]; int i = 0, j = 0; if (n == 0) { /* 0 是最容易漏的特例 */ buf[0] = '0'; buf[1] = '\0'; return 1; } while (n > 0) { tmp[i++] = (char)('0' + (n & 1u)); /* 等价于 n % 2 */ n >>= 1; /* 等价于 n / 2 */ } while (i > 0) buf[j++] = tmp[--i]; /* 逆序写出 */ buf[j] = '\0'; return j; } /* 二进制字符串转十进制,遇到非 0/1 字符停止 */ unsigned int bin_to_dec(const char *s) { unsigned int v = 0; while (*s == '0' || *s == '1') { v = (v << 1) | (unsigned int)(*s - '0'); /* 左移一位再加当前位 */ ++s; } return v; } int main(void) { char buf[33]; dec_to_bin(217, buf); printf("217 -> %s\n", buf); /* 期望 11011001 */ printf("%s -> %u\n", "11011001", bin_to_dec("11011001")); /* 期望 217 */ return 0; }

这两个函数里有两个细节值得说。第一,n & 1和n >> 1分别是取余和除 2 的位运算写法,对无符号数完全等价,而且更快,这也是为什么位运算在底层代码里到处都是。第二,二进制转十进制用的是“边读边算”,每读一位就把已有结果左移一位,再加上当前位,这样只需要一趟遍历,不用先存字符串再回头展开。这个套路在处理任意进制字符串时都通用。

如果目标平台支持 C23,标准库已经可以直接打印二进制了:

#include <stdio.h> int main(void) { unsigned int n = 217; printf("%b\n", n); /* C23 新增,输出 11011001 */ printf("0b%b\n", n); /* 带前缀 */ return 0; }

不过现实里很多项目的工具链还没跟上,编译不过别怀疑自己,换个手写函数就行。C++ 的话可以用std::bitset<8>(217).to_string(),一行搞定;Python 里bin(217)得到'0b11011001',int('11011001', 2)转回来,hex()和int('d9', 16)同理。脚本语言胜在省事,但你要是想知道底层发生了什么,还是得回到上面那两个循环。

6.2 标准库的用法与printf的坑

C 语言里printf的格式符对应关系是必须记牢的:%d十进制有符号,%u十进制无符号,%o八进制,%x小写十六进制,%X大写十六进制。除了 C23 的%b,标准里没有直接输出二进制的格式符,很多人到这一步就卡住了,以为要自己写,其实自己写也确实更可控。

真正容易踩的坑在类型匹配上。%u对应的是unsigned int,如果你传了一个int或者unsigned long,在某些平台上会打印出莫名其妙的值,尤其是 64 位平台上long和int宽度不一样。我在排查一个位掩码打印错误的时候,最后发现就是%x配了long类型,改成%lx才对。这类问题是“编译能过、运行不对”,最难查。

另一个坑是符号扩展。把一个 8 位有符号数提升到 32 位时,如果最高位是 1,编译器会按符号位扩展,得到一个很大的数;如果你本意是把它当无符号字节处理,就得先强转成unsigned char再提升。处理二进制协议里传过来的字节时,这个细节能把人坑到怀疑人生。

6.3 strstr() 为什么不能用来查找二进制内存

热词里有人问strstr()能不能用于查找二进制内存,这个问题非常典型,答案是不能,除非你确定数据里没有 0 字节。原因是strstr()是字符串函数,它靠'\0'(也就是 0 字节)判断字符串结束。二进制内存里出现 0 字节是家常便饭,一旦遇到,strstr()就认为字符串到头了,后面的内容它根本不会看。

那二进制内存里找子串用什么?POSIX 环境有memmem(),它接收长度参数,不依赖结束符。如果是跨平台代码,或者不想引入平台相关的函数,就自己写一个带长度的搜索循环,逐字节比较。这里的原则是:凡是带长度、可能含 0 字节的数据,一律用长度驱动的接口,不要用字符串接口。

我踩过的坑是在解析一段包含 0x00 的报告时,用strstr()找分隔符,结果前面几个字段都正常,一遇到某条记录中间带 0 就整条解析失败。后来换成手动按长度遍历,问题立刻消失。这件事之后我给自己定了条规矩:二进制数据的处理路径上,不允许出现任何字符串处理函数,除非先经过转义。

6.4 工程里常见的进制使用场景

散着说了不少,这里集中列几个实际工作里高频遇到的场景,方便你对号入座。

第一个是位域解析。硬件寄存器、协议标志位基本都是按位定义的,比如某 32 位寄存器第 0 到 3 位是版本号,第 8 到 15 位是状态码。解析方式就是先右移把目标位段对齐到低位,再用掩码取出。这两步背后的原理就是位权和二进制表示,理解了这个,写起来不需要翻书。

第二个是校验和与哈希。很多校验算法本质是在固定位宽下做加法、异或、取反,位宽决定了进位怎么丢弃。你如果不清楚补码和位宽的关系,看校验代码会一头雾水。

第三个是地址与掩码。网络里算子网掩码,或者内存里算对齐偏移,都是按位运算。掩码 255.255.255.0 写成二进制就是一串 1 后面跟一串 0,这个“连着多少个 1”的直觉,就是二进制转换教给你的。

第四个是十六进制与二进制的互转。日志、抓包、内存 dump 默认都是十六进制显示,你要在脑子里把它拆成二进制再按位理解,这个链路越短,排查速度越快。我现在的习惯是看到十六进制就自然分组,比如 0xB5E1 就知道前八位是 10110101,后八位是 11100001,中间不需要停顿。

7. 容易被搞混的“二进制”:数字 vs 文件

7.1 二进制数和二进制文件不是一回事

搜索这个词的时候,经常能看到两类完全不同的结果混在一起:一类是二进制数、进制转换、补码;另一类是二进制包、二进制文件、二进制部署。这两类里的“二进制”含义完全不同,前者指的是二进制数制,用 0 和 1 表示数值;后者指的是已编译的可执行文件或程序包,相对于“源码”而言,是编译器把代码翻译成机器指令之后的产物。

为什么叫“二进制文件”?因为这类文件的内容是机器指令和数据的原始字节流,不是给人读的文本,你用文本编辑器打开会是一堆乱码。它和进制转换的唯一交集是:机器指令本身就是二进制编码,但你在部署和运维的时候基本不需要逐位分析它。搞清楚这个区别,能省下不少检索时的困惑。

部署方面,预编译的程序包通常被称为二进制包,好处是目标机器不需要装编译器和依赖源码,解压或安装就能跑。代价是包和平台强绑定,不同 CPU 架构、不同系统库版本之间往往不通用。这一点在做边缘设备部署时特别明显,同一个工具,x86 机器上的包拿到 64 位 ARM 设备上直接跑不起来。

7.2 和架构绑定有关的几个实际问题

热词里出现的“下载适配你平板 CPU 架构的二进制包”,说的就是上面这个绑定问题。程序包的名字里带arm、arm64、amd64这类标识,指的就是它编译时面向的指令集架构。架构不匹配,内核连加载都不会让你加载,直接报格式错误。所以拿到一个包,第一步不是急着运行,而是先确认uname -m之类命令输出的架构和包的标识对不对得上。

另一个常见问题是动态库依赖。二进制包跑不起来,很多时候不是架构错了,而是目标机器缺某个共享库,或者共享库版本差了。用ldd看一眼这个程序依赖哪些库,把缺的补上,比一遍遍猜要快得多。还有就是文件权限,下载下来的包默认可能没有可执行权限,需要手动加上,这个属于新手最常遇到的一类“明明文件在那儿却提示找不到命令”的问题。

至于用编辑器直接查看二进制文件,VS Code 需要装十六进制查看扩展,或者用“以十六进制方式打开”的选项,普通的文本打开模式会把不可打印字节显示成乱码甚至破坏文件。如果你只是想看一眼文件头、确认魔数和架构标识,用file命令或者hexdump -C更省事,不需要打开图形编辑器。

顺带提一句,有些容器环境的客户端和守护进程之间通过本机的一个特殊文件通信,这个文件是 Unix 域套接字,不是二进制文件,也不是网络端口。排查连接问题时看的是这个套接字文件存不存在、权限对不对,方向别搞错。

8. 常见问题速查与踩坑记录

8.1 高频问题速查表

把前面讲到的问题和排查方向整理成一张表,遇到问题可以直接对号。

现象可能原因处理方向
算出的二进制结果位数对但顺序反了余数按正序写了除2取余必须逆序读,写完用高位权值反算验证
小数转换算了几步不收敛十进制小数在二进制下是循环小数明确目标位数,按就近舍入截断,不要追求精确
金额计算出现几分钱的误差用了浮点数改用整数或定点数表示,以分为单位存储
负数算出来完全不对位宽不对或忘了取反加一先确定位宽,再按取反加一流程走,用位权验证
8位有符号数溢出取值超出 -128 到 127换更宽的位宽,或加溢出检查
二进制数据查找失败用了字符串函数遇到 0 字节截断改用带长度的内存查找接口
printf 输出十六进制不对格式符和实际类型不匹配按类型选对格式符,注意 long 需要长度修饰
预编译包运行报格式错误架构不匹配先确认目标架构再选包
包存在却提示命令找不到缺可执行权限或路径不对检查权限位和 PATH
程序启动报缺库动态库依赖不满足用依赖检查命令逐一补齐

这张表里我最想强调的是前两条。顺序反了和追求精确,是新手最容易犯也最难自己发现的两个错,因为它们都“看起来能跑通”,只是结果不对。

8.2 我自己的几条实操经验

第一条经验是永远用两个方向互相验证。算完十进制转二进制,就用位权展开算回去,对上了才算完。手算一次不验证,后面越算越没底,反而不如花五秒查一遍。我到现在手算 32 位地址的时候还是会拆成四段,每段都验证一次,习惯了就不觉得慢。

第二条是拿不准的时候回到位权。所有口诀、技巧、快捷方法,出问题的时候都可以扔掉,回到“第 n 位权值是 2 的 n 次方”这条根本规则上,慢慢展开一定不会错。口诀是提速用的,不是救命用的。

第三条是位宽先定,再动手。负数、补码、溢出、符号扩展,这一串问题的根源都是位宽没想清楚。写代码也好,手算也好,先问一句“这是几位”,能省掉一大半返工。我在处理协议字段的时候会把每个字段的位宽写在注释里,看似啰嗦,实际省事。

第四条是涉及精确数值就别用浮点。前面说过金额的例子,其实不只是金额,任何有“必须精确相等”要求的比较,都应该优先考虑整数或定点表示。浮点适合科学计算和图形计算,不适合账本。

第五条是二进制数据的处理路径上禁用字符串函数。这是被 0 字节坑过之后养成的肌肉记忆,凡是长度已知、内容可能含 0 的数据,接口一定选带长度的那一套。这个习惯帮我避掉过好几次很难查的解析错误。

8.3 手算练习的几个小方法

如果你想让转换速度再上一个台阶,我的建议是从反向练起。大多数人练的是十进制转二进制,其实二进制转十进制才是更高频的场景,因为现实里你更多是拿到一串 bit 要读懂它。可以随手翻一段日志里的十六进制,把它展开成二进制,再按位读一遍每个字段的含义,这个过程既练了转换,又练了业务理解。

第二个方法是背权值表到 2 的 16 次方。65536、32768、16384、8192、4096、2048、1024、512、256、128、64、32、16、8、4、2、1,这串数字背下来之后,十六位以内的转换基本可以心算完成。我当初是每天通勤路上默念一遍,两周就形成条件反射了。

第三个方法是用代码给自己出题。写个小程序随机生成一个数,输出它的十六进制和二进制,自己在纸上转成十进制,再让程序验证。这种练习比看书快得多,而且错在哪一目了然。练到一定程度,你会发现很多看起来复杂的数值问题,其实卡住你的不是逻辑,而是进制没转明白。

最后分享一个我常用的小技巧:遇到不确定的数值,先换算成十六进制看一眼字节结构。十六进制每两位刚好一个字节,字节边界一目了然,比一长串二进制好读得多,也比十进制更容易看出高位低位。很多人在二进制和十进制之间来回换算,其实中间隔一层十六进制,两边都轻松。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询