新闻详情

新闻详情

首页 / 资讯中心 / 详情

彻底搞懂进制转换:从整数到小数,精度问题一文讲透

发布时间:2026/10/1 18:57:04来源:尧图网络
彻底搞懂进制转换:从整数到小数,精度问题一文讲透
我记得刚工作那会儿第一次接手网络协议解析模块看到满屏的0x45 0x00 0x02 0x58这种数据整个人是懵的。后来硬着头皮把十六进制、二进制、十进制来回倒腾才慢慢找到感觉。可以说进制转换是程序员的基本功但你真把它掰开揉碎了去理解会发现里面藏着不少容易被忽视的细节——尤其是小数部分的转换和精度舍入问题几乎每个人都在上面踩过坑。这篇文章我想从本质出发把我这些年做进制转换的心得、代码实现和排查经验完整梳理一遍希望能帮你一次性把“十进制与任意进制互转”这件事彻底搞明白。如果你刚接触进制转换可能觉得这不过是个“除基取余、按权展开”的机械过程如果你已经写了不少代码也许遇到过0.1 0.2不等于0.3的怪事或者手写过转换函数却在负数、大数、非法输入上翻了车。无论你处于哪个阶段这篇内容都值得你对“进制”这个东西重新审视一下。1. 进制转换的本质与整体设计思路1.1 位权与基数为什么“进制”本质上只是一套计数约定要理解任意进制之间的转换先得想清楚一个底层问题进制到底是什么。日常我们数数用的是十进制底数为10每一位上的数字范围是0到9。所谓的“十进制的10”在二进制里是1010在十六进制里是0xA——同一个数量换了一套记法而已。这套记法的核心就是两个概念基数base和位权weight。基数决定了一个数位上能放多少个符号。二进制的基数是2所以每位的符号只有0和1八进制是0到7十六进制是0到15用A到F表示10到15。位权则决定了同一个数字在不同位置上代表多大的值。十进制数123实际上是1×10^2 2×10^1 3×10^0。这里的10^2、10^1、10^0就是位权。所以进制转换的本质是把同一个数在不同“计数约定”之间的表达方式做映射。这和用不同语言描述同一个事物是类似的中文说“水”英文说“water”东西还是那个东西叫法不同而已。我常用一个生活类比帮人理解进制就像时钟。时钟上60秒等于1分钟这是六十进制24小时等于一天是二十四进制。你既可以说“1.5小时”也可以说“90分钟”两个数表述不同但时间长短完全一样。进制转换要解决的就是这种“表述互换”的一致性问题。1.2 任意进制互转的核心思路以十进制为“中转站”理解了进制是计数约定接下来就是方法论问题任意进制之间怎么转原则上任意两个进制都可以直接转换比如从二进制直接转八进制、从十六进制直接转二进制因为2^3 8、2^4 16存在天然的整数倍关系可以用“分组法”直接转。但如果你让我把一个二十七进制的数转成五十三进制直接转就非常痛苦因为你得专门为这两个进制设计一套算法。所以实际工程里更通用的做法是把十进制当作“中转站”。任意进制数先按位权展开转换成十进制再把十进制通过短除法转换成目标进制。提示以十进制为中间层代码实现上只需要写两个函数任意进制转十进制和十进制转任意进制就能解决所有进制组合的互转问题。这样逻辑统一、代码可复用也便于维护。那为什么不干脆对所有进制组合都写直接转换函数很简单假设你支持2到36进制可能的组合有35×34 1190种每对写一个函数绝对是一场灾难。中转法牺牲了一点性能多了一次中间转换的开销换来了代码规模的大幅缩减和逻辑的清晰。对绝大多数应用场景来说这点性能开销完全可以忽略。不过这里有一个例外二进制、八进制、十六进制之间因为进制基数是2的整数次幂直接分组转换不仅快写法也简单。面试中如果转型题目能快速用分组法处理这几种进制会是很加分的表现。2. 整数部分的转换短除法与位权展开法2.1 十进制转任意进制短除法的完整步骤与原理先看最常用的场景十进制整数转成其他进制。核心方法叫短除法division by base也叫“除基取余法”。操作步骤非常固定用十进制数除以目标进制记录余数用商继续除以目标进制再记录余数重复直到商为0把记录的余数从下往上排列就是转换结果。我用一个具体例子演示把十进制200转成二进制200 ÷ 2 100 余 0 100 ÷ 2 50 余 0 50 ÷ 2 25 余 0 25 ÷ 2 12 余 1 12 ÷ 2 6 余 0 6 ÷ 2 3 余 0 3 ÷ 2 1 余 1 1 ÷ 2 0 余 1余数从下往上读11001000。所以十进制的200就是二进制的11001000。为什么余数要倒着读我第一次学的时候也疑惑为什么先算出来的余数放最后关键在于第一次200 ÷ 2求的是“200里包含多少个2以及还剩多少”这个余数就是二进制最低位的值。每次除法都在剥离最低位先得到的余数当然是最低位必须放到最右边。如果目标进制是十六进制过程一样只是余数范围变成0到15超过9的需要映射成A到F。比如把200转十六进制200 ÷ 16 12 余 8 12 ÷ 16 0 余 12余数从下往上C8其中12映射为C。所以200的十六进制是C8。我还想提醒一个细节转换目标基数的选择对短除法效率有影响。基数越大除的次数越少这个从上面的例子能直接看出来。但基数大也意味着字符集复杂后续处理字符串时要格外注意大小写。2.2 任意进制转十进制位权展开法反向转换用位权展开法positional notation expansion核心公式是value Σ (digit[i] × base^(len-1-i))其中len是数字字符串的长度digit[i]是第i位对应的数值大小。说白了就是每一位乘上它的位权再累加。用二进制的11001000反推十进制1×2^7 1×2^6 0×2^5 0×2^4 1×2^3 0×2^2 0×2^1 0×2^0 128 64 0 0 8 0 0 0 200完全对上了。这里最容易出错的地方是字符到数值的映射。对十进制输入直接字符 - 0即可对含字母的进制十六进制及以上要判断字符范围0-9减48A-Z减55或者用toupper统一成大写再处理。我曾经在写一个解析器时因为忘了处理小写字母导致0xab和0xAB结果不同排查了半天。后来养成习惯凡是处理进制字符串进函数第一步就统一大小写。下面这张表整理了0到15在4种常见进制下的表示算是转换时的手边参考十进制二进制八进制十六进制000011112102231133410044510155611066711177810001089100111910101012A11101113B12110014C13110115D14111016E15111117F2.3 大整数转换的细节与语言类型上限整数转换在原理上不难但落到具体代码里最大的坑是数据类型溢出。C语言里int通常只有4字节最大能表示约21亿2147483647。如果你转换的十进制数超过这个范围比如500000000050亿直接用int乘法就会溢出结果莫名其妙。long long能到19位数左右9223372036854775807能应对大部分场景但还是有边界。我建议在实际工程里至少用unsigned long long来存中间计算结果并且在做累加前判断一下会不会溢出超过范围就报错。如果需要转换任意大的整数比如身份证号、超长订单号就得用字符串模拟大数运算或者借助现成的大数库如OpenSSL的BIGNUM、GMP。这类需求在底层网络协议解析和加密场景里很常见。在JavaScript里还要额外注意Number类型的整数安全上限是2^53 - 19007199254740991超过这个值就会出现精度丢失。我在浏览器控制台里见过太多因为大数转换导致的诡异bug根源都是JavaScript的浮点表示方式。后来凡是处理超大整数字符串我直接用BigInt省心很多。3. 小数部分的转换精度与舍入3.1 十进制小数转任意进制乘基取整法整数转换解决了小数部分才是真正让很多人栽跟头的地方。十进制小数转二进制用的是乘基取整法multiplication by base。操作步骤把小数部分乘以目标基数取乘积的整数部分作为当前位的结果把乘积的小数部分继续乘以目标基数重复直到小数部分为0或者达到所需精度结果的顺序是从上往下读和短除法相反。用十进制小数0.625转二进制演示0.625 × 2 1.25 → 整数部分1 0.25 × 2 0.5 → 整数部分0 0.5 × 2 1.0 → 整数部分1从上往下读0.101。验证一下1×2^-1 0×2^-2 1×2^-3 0.5 0 0.125 0.625完全正确。为什么乘出来的整数部分正好就是二进制的一位因为小数部分乘以2本质上是把这一位“挤”到整数位上来看看它是0还是1。这和整数短除法剥离最低位是镜像操作。整数除法看余数小数乘法看进位。如果转成八进制或十六进制方法一样只是把乘数换成8或16。比如0.625转八进制0.625 × 8 5.0 → 整数部分5结果就是0.5八进制。验证5×8^-1 5×0.125 0.625果然吻合。3.2 为什么二进制无法精确表示所有十进制小数这个问题的关键在于分母的因子。一个十进制小数能不能被二进制精确表示要看它转化后的小数部分是否能在有限步内变成0。数学上二进制分数能精确表示的数其分母只能是2的幂如1/2、1/4、3/8。而十进制小数0.1是1/10分母10含有因子5不是2的幂所以二进制无法在有限位内精确表示它。演示一下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 × 2 1.2 → 1 0.2 × 2 0.4 → 0 0.4 × 2 0.8 → 0 0.8 × 2 1.6 → 1 ...看到没有从0.2开始无限循环0.00011001100110011...永远写不完。你可以把它类比成十进制里的1/3你在纸上用十进制写1/3得到0.33333...永远写不完。同理0.1在二进制里也永远写不完。这是进制本身的表达限制不是计算机的bug。3.3 小数点后保留多少位精度限制与舍入策略既然有些小数二进制写不完那么计算机里到底怎么存答案就是——截断并考虑舍入。float单精度浮点数的尾数位有23位加上隐藏位实际精度约7位十进制有效数字double双精度有52位尾数约15到16位十进制有效数字。超出这些位数的部分就会被舍入掉。IEEE 754标准默认采用就近舍入round to nearest, ties to even也就是“四舍六入五成双”处于中间值时舍入到最近的偶数位。这正是所有计算机语言里0.1 0.2不等于0.3的根本原因0.1和0.2在二进制里都是近似值两者相加后再舍入结果在十进制下显示为0.30000000000000004而不是数学上的0.3。注意如果你在做金融、会计、计费这类对十进制精度极其敏感的业务千万不要用二进制浮点数来存金额。这就是为什么数据库里有DECIMAL/NUMERIC类型Java有BigDecimal、C#有decimal、Python有Decimal——它们本质是用十进制字符串或整数如按分存储来规避二进制误差。具体到“十进制小数转换为二进制有精度限制时需要考虑舍入吗”这个问题我的回答是任何时候都需要。哪怕你只是手写了一个转换函数当转换结果超出既定精度时也要明确告诉调用方这里发生了截断并提供舍入策略。我习惯让函数接受一个precision参数默认做四舍五入或者更严谨地用银行家舍入调用方能清楚地知道转换不是无损的。4. C语言实现通用进制转换从原理到代码4.1 十进制整数转任意进制完整代码与剖析理论讲得再明白不落地写代码都是虚的。我直接用C语言实现一个通用的转换工具代码风格偏工程带输入校验和错误处理。#include stdio.h #include string.h #include ctype.h // 数字转字符 char num_to_char(int num) { if (num 0 num 9) { return 0 num; } if (num 10 num 35) { return A (num - 10); } return ?; } // 十进制整数转任意进制 // 参数: value - 十进制数, base - 目标进制(2~36), out - 输出缓冲区, out_size - 缓冲区大小 // 返回: 0成功, -1参数错误, -2缓冲区不足 int dec_to_base(unsigned long long value, int base, char *out, size_t out_size) { if (base 2 || base 36) return -1; if (out NULL || out_size 2) return -1; if (value 0) { if (out_size 2) return -2; strcpy(out, 0); return 0; } char temp[128]; int pos 0; while (value 0 pos (int)sizeof(temp) - 1) { temp[pos] num_to_char(value % base); value / base; } if (value 0) return -2; // 溢出说明缓冲区不够 if ((size_t)(pos 1) out_size) return -2; // 倒序拷贝 for (int i 0; i pos; i) { out[i] temp[pos - 1 - i]; } out[pos] \0; return 0; }关键点有两个一是余数倒序。代码里先存进temp数组最后倒序放到out对应短除法的“从下往上读”。二是缓冲区边界。这类转换函数最常见的bug就是缓冲区溢出。我在代码里先用一个128字节的临时数组存中间结果再检查目标缓冲区是否足够最后才拷贝。虽然多了一次内存拷贝但换来的是安全性值得。4.2 任意进制字符串转十进制完整代码与剖析反向函数稍微复杂一些因为要多处理字符到数值的映射和非法字符校验。#include stdio.h #include string.h #include ctype.h // 字符转数字 int char_to_num(char c) { if (c 0 c 9) return c - 0; if (c A c Z) return c - A 10; if (c a c z) return c - a 10; return -1; } // 任意进制字符串转十进制 // 参数: str - 输入字符串, base - 输入的进制(2~36) // 返回: 成功返回转换结果, 失败返回-1注意如果输入合法-1只可能表示溢出或参数错误 long long base_to_dec(const char *str, int base) { if (str NULL || base 2 || base 36) return -1; long long result 0; int len strlen(str); for (int i 0; i len; i) { int digit char_to_num(str[i]); if (digit 0 || digit base) { printf(非法字符 %c 在当前进制 %d 下无法解析\n, str[i], base); return -1; } // 溢出检查 if (result (9223372036854775807LL - digit) / base) { printf(溢出转换结果超出 long long 范围\n); return -1; } result result * base digit; } return result; }这里有三个工程上的细节值得说。第一字符校验。char_to_num返回-1表示非法字符且digit base的检查也必不可少——比如在二进制字符串里出现2虽然2是合法数字字符但在二进制里却是非法数据。第二溢出保护。如果不做检查result * base digit一旦超过long long上限行为是未定义的通常会绕回最小值产生匪夷所思的结果。我先用不等式的变形检测溢出再去计算避免了乘法溢出本身。第三大小写兼容。char_to_num对A和a都返回10避免调用方大小写混用导致解析错误。4.3 小数部分转换的C语言实现思路整数转换都做完了小数部分才是加分项。我给出一个通用思路用double作为输入转换后输出指定精度的字符串。#include stdio.h #include math.h // 十进制小数转任意进制最多支持到36进制 // 参数: frac - 小数部分(0 frac 1), base - 目标进制, precision - 保留多少位 void frac_to_base(double frac, int base, int precision) { if (base 2 || base 36 || precision 1) return; printf(0.); while (precision 0) { frac * base; int digit (int)frac; printf(%c, num_to_char(digit)); frac - (double)digit; // 如果小数部分变成0后面的位自然全是0 if (frac 1e-12) { while (precision 1) { printf(0); precision--; } break; } precision--; } printf(\n); }这个写法有两点值得打磨一是double的精度噪声。double本身精度有限当frac小到一定程度比如小于1e-12继续乘下去只是在放大噪声不如直接补零终止。二是没有做舍入。上面的代码是直接截断严格来说不算“四舍五入”。如果要做舍入可以在输出最后一位前多取一位判断。这个留给你作为练手扩展。4.4 代码实现里容易踩的坑总结一下代码层面的高频坑都是我实际见过的坑点错误示例正确做法忘记处理0的情况while (value 0)直接跳过输出空字符串提前判断value 0输出0字符映射错误直接用char - 0处理A先判断数字还是字母大小写不做兼容0xAB能解析0xab报错统一转成大写或都处理缓冲区不够char buf[10]转超长字符串越界用临时大数组拷出前检查长度负数未处理直接把负数传给无符号类型函数入口先处理符号位基数不校验base1或base100崩溃或死循环函数入口统一校验范围特别是基数为0的情况我自己就踩过某次用户误传base0除法直接触发浮点异常。所以所有函数入口我都加了base 2 || base 36的保护。5. 常见问题与排查技巧实录5.1 hex转十进制时最容易犯的低级错误十六进制转十进制是高频操作。新手最容易犯的错误有这么几类一是把前缀也当数字解析。输入0xFF时如果整个字符串传给转换函数第一个0能解析是0第二个x就崩了。所以解析前要去掉0x前缀或者说调用方明确传入不带前缀的字符串。二是**A的ASCII码混淆**。0的ASCII码是48A的ASCII码是65a的ASCII码是97。如果要手写映射A要减55才行65 - 10 55不是减64。一个字母之差结果偏10倍排查起来非常别扭。三是位权算错。把最右边那位当成最高位去乘。记住最右边是base^0向左依次递增。我见过有人用循环从左边开始但指数递减错了结果整个数完全不对。这里强力推荐在纸上用符号法推导两遍再写代码——花两分钟省两小时。我平时最快的验证方法就是开终端用printfprintf %d\n 0xFF # 输出 255或者反过来printf %x\n 255 # 输出 ff这些小命令做快速验证非常方便但真正的程序里还是要靠完整的转换函数。5.2 为什么0.1 0.2不等于0.3这大概是进制转换领域最出名的“悬案”了。绝大多数编程语言里直接算0.1 0.2结果都不是0.3double a 0.1; double b 0.2; double c a b; printf(%.20f\n, c); // 输出 0.30000000000000004441原因前面说过0.1和0.2转二进制后都是无限循环小数存进double时被舍入成了近似值。两个近似值相加误差累积显示出来就是0.30000000000000004。碰到这种问题时排查思路不是去“修0.1”而是去理解浮点数的误差边界。工程上有几个常用策略比较浮点数时不要用改用fabs(a - b) 1e-9这类误差范围判断对精度要求高的场景不用二进制浮点改用Decimal类型或按整数分存储如果只是显示问题输出时控制格式化的精度比如printf(%.2f, c)。5.3 会计场景下的十进制特例前面提到“会计十进制”这个热搜词金融和会计领域确实对十进制格外敏感。在电商计费、财务报表、银行利息结算里如果用double存储金额累积到一定量级就会产生“分”级别的误差这在审计上是没法接受的。所以主流做法是数据库用DECIMAL(10,2)这类精确定点小数类型应用程序里用BigDecimal/decimal类型或者干脆把金额乘以100转成整数“分”存储只在展示时还原成元。这个问题的本质就是二进制无法精确表示所有十进制小数在实际业务里的真实代价。理解进制转换的人一眼就能明白为什么会计系统里不能用float计钱——这不是敷衍这是数学规律。5.4 面试与笔试里的进制转换套路最后说说面试。进制转换基本上是技术面试的基础题但考察点各不相同初级岗位手写十进制转二进制验证基本的短除法掌握程度。中级岗位二进制转十进制有时要求不能直接用库函数需自己实现字符映射。高级岗位和底层岗位可能会遇到“实现任意进制互转”核心考点就是能否想到以十进制为中转的算法设计。也有考位运算的比如“判断一个整数二进制表示里有多少个1”有个经典技巧int count_bits(unsigned int x) { int count 0; while (x) { x (x - 1); // 清除最低位的1 count; } return count; }这个技巧的前提还是对二进制的深入理解x (x-1)这一行代码只要有二进制位权的基础就能看出它是在消除最低位的1。写在最后的实战建议如果你问我掌握了进制转换的原理后最值得带走的是什么我的答案是永远不要迷信“精确”二字。整数转换在有限位内是精确的小数转换天然带有精度损失。写代码时明确标注函数的精度行为读协议时留意字段的长度和字节序做账务时坚持用定点数或整数。把这些问题想清楚远比背熟一套转换公式更有价值——因为你在面对的不是“怎么转”而是“转完之后丢了什么该怎么补”。这些年在底层网络包解析、嵌入式开发、数据处理里我靠这套认知躲过不少暗坑希望也能帮到你。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Microsoft Edge主页被劫持?四层控制机制深度解析 2026/10/1 20:36:33

Microsoft Edge主页被劫持?四层控制机制深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
macOS安装Redis实操:配置、持久化与避坑指南 2026/10/1 20:36:32

macOS安装Redis实操:配置、持久化与避坑指南

先说个我自己的经历。刚转行做后端那会儿,领导扔给我一台Mac电脑,第一件事就是"把Redis装上,配好,项目要用了"。我当时觉得很简单,brew install redis一条命令跑完,再brew services start redis&…

阅读更多 →
openrig:基于4040铝型材的DIY模拟驾驶舱搭建全攻略 2026/10/1 20:36:32

openrig:基于4040铝型材的DIY模拟驾驶舱搭建全攻略

1. 为什么会有 openrig:从“想玩但又买不起成品”开始先交代一下背景。我长期玩模拟类游戏,从模拟飞行到拉力赛到 F1 赛季模式都会碰,家里原来一直用办公椅配入门方向盘,玩久了腰疼、踏板乱滑、显示器距离怎么调都不对。看了一圈市…

阅读更多 →
飞书实习面试全流程复盘:从简历到HR面的关键经验 2026/10/1 20:36:32

飞书实习面试全流程复盘:从简历到HR面的关键经验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
一键开关机芯片选型全攻略:功耗、时序、拓扑与调试 2026/10/1 20:36:19

一键开关机芯片选型全攻略:功耗、时序、拓扑与调试

1. 一键开关机芯片到底在解决什么问题 1.1 一键开关机芯片是个什么东西 先聊一个最基础的认知。很多人第一次接触"一键开关机芯片"这个分类的时候,都会以为它就是一个电子版的机械自锁开关:按一下导通、再按一下断开。实际完全不是这么回事。…

阅读更多 →
Xvisor设备虚拟化三要素:区域、模拟器与MMIO陷出 2026/10/1 20:36:05

Xvisor设备虚拟化三要素:区域、模拟器与MMIO陷出

1. 项目概述:从裸机视角看设备虚拟化的底层逻辑Xvisor 是一个开源的 Type-1(裸金属)虚拟机监控器(Hypervisor),它的设计哲学非常硬核——不依赖任何宿主操作系统,直接运行在物理硬件之上。当你看…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉