
1. 先搞明白进制转换题在考什么思维我在刷题初期对进制转换四个字是有点轻视的觉得这题太基础了考的就是十进制转二进制、八进制、十六进制这种套路。结果第一次认真在在线评测系统上做进制转换C语言题时连续翻车一道题要求把输入的十六进制字符串转成二进制输出我用printf(%x)配合整数变量一把梭看起来没毛病可一旦输入是大到1000000000000000000000的字符串时程序直接溢出运行结果形如乱码。从那一刻我就意识到进制转换题考的不是会不会背转换步骤而是你有没有真正理解进制本身、字符串和数字之间如何互相转化、C语言里那些隐含的边界行为以及大数场景下的处理能力。这篇文章我想把这些东西拆开讲透给同样准备机考、笔试或者单纯想练 C 语言基本功的朋友一个可以直接照着练的参考。1.1 出题人到底在考什么先给进制转换题分个类。见到的题再多归纳起来基本都是这几类题型典型描述核心考点十进制转 N 进制把一个十进制整数转成 2/8/16 进制输出除基取余、逆序输出N 进制转十进制读入一个 N 进制字符串转成十进制数按权展开、字符转数字N 进制转 M 进制输入一个 N 进制数要求以 M 进制输出通常借助十进制中转或直接长除法大数进制转换数字长度可达几百上千位不能用整数类型保存数组模拟除法、字符串运算小数进制转换十进制小数转二进制等乘基取整、浮点误差控制负数与补码负数转二进制补码表示补码概念、无符号类型、溢出边界从这张表能看出来单纯写一个while (n 0) { result n % base result; n / base; }只是最基础的功力。真正能区分水平的地方在于你能否处理字符串形式给的超长数字能否在结果为 0 时保持正确能否在被要求输出 36 进制时正确映射 A-Z以及是否知道 C 语言对负数的除法和取模规则其实和数学上的预期不一样。这些细节才是进制转换题拉开分数的原因。1.2 为什么 C 语言特别喜欢拿这题当练习有人会问Python 里一句int(s, base)就完成了进制转换C 语言却要写一堆循环为什么考试还总爱用 C 语言出进制转换题我的观点是正因为 C 语言没有内置的任意进制转换 API这个题目才变得有训练价值。C 语言只提供了%d、%o、%x这类格式化输出它们最多让你换个显示格式并不能帮你完成一个任意大数字从 A 进制换到 B 进制的计算。你必须亲手写循环、管理字符串缓冲区、处理进位和借位这一整套流程下来你对指针、数组、位运算和整数边界的理解都会被调动起来。换句话说进制转换题是机考里性价比很高的一道基本功体检它不涉及复杂算法却能暴露你 C 语言基础到底扎不扎实。2. 手写转换的三条主线除基取余、按权展开、位运算无论题型怎么变手写转换的核心思路其实只有三条主线把它们吃透90% 的常规进制题都能迎刃而解。2.1 十进制到任意进制除基取余的完整实现十进制转 N 进制的经典方法是除基取余不断用原数除以目标进制记下余数最后把所有余数逆序排列。比如十进制 13 转二进制13 / 2 6 余 1 6 / 2 3 余 0 3 / 2 1 余 1 1 / 2 0 余 1余数从下往上读是 1101所以十进制 13 等于二进制 1101。写成 C 语言函数时有几个细节要注意先看完整代码#include stdio.h void decToBase(unsigned int n, int base, char *out) { static const char digits[] 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ; char tmp[64]; int i 0, j 0; if (n 0) { out[0] 0; out[1] \0; return; } while (n 0) { tmp[i] digits[n % base]; n / base; } while (i 0) { out[j] tmp[--i]; } out[j] \0; } int main() { char buf[64]; decToBase(255, 16, buf); printf(%s\n, buf); // 输出 FF return 0; }这段代码里有几个关键点第一n 0必须单独处理因为如果直接进入循环tmp里一个字符都没有输出会变成空串而正确结果应该是0。第二先用数组tmp临时存放从低位到高位的余数最后逆序转移到out这是所有实现里最容易踩坑的地方忘掉逆序是经典错误。第三base大于 10 时余数可能超过 9所以需要digits[]这个查表数组完成数字到字符的映射如果不查表10 0会得到:而不是A。2.2 任意进制回十进制按权展开与字符校验N 进制转十进制用按权展开每一位乘以权重 N 的幂次再累加。比如十六进制字符串FF转换成十进制就是15 * 16 15 255。实际写代码时不需要真的去计算每个幂次可以用霍纳法则也就是从最高位开始不断乘以进制再加下一位效率高代码也短#include ctype.h int charToVal(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; } unsigned long long baseToDec(const char *s, int base) { unsigned long long res 0; for (int i 0; s[i] ! \0; i) { int val charToVal(s[i]); if (val 0 || val base) { return 0; } res res * base val; } return res; }这个版本比直接写pow()再累加要稳得多而且可以顺便完成非法字符校验。注意两个容易出问题的地方charToVal返回-1表示字符不合法同时如果某个字符的数值大于等于base比如用二进制却输入了2必须判为非法否则结果会错得莫名其妙。这种细节在线评测里常用来构造边界用例。2.3 二进制特供用位运算代替除法虽然除基取余能处理任意进制但二进制有个更贴近计算机本质的做法位运算。一个数的二进制表示本质就是它的所有比特位逐位提取即可。下面这段代码把unsigned int的所有位按每 4 位一组打印出来#include stdio.h void printBinary(unsigned int n) { int bits sizeof(n) * 8; for (int i bits - 1; i 0; i--) { putchar((n i) 1 ? 1 : 0); if (i % 4 0 i ! 0) { putchar( ); } } putchar(\n); } int main() { printBinary(13); // 0000 0000 0000 0000 0000 0000 0000 1101 return 0; }这里(n i) 1的意思是把第i位右移到最低位然后和1做按位与得到该位是 0 还是 1。位运算比除法和取模更快而且在处理位图、哈希值、网络地址这类二进制场景时几乎是首选。除了打印还可以用__builtin_popcount统计 1 的个数用__builtin_ctz找低位连续的 0 的个数这些都是二进制类题目的常用工具。提示printf(%x)只能做低级格式化输出不要把它当作任意进制的万能转换器。真正的转换题尤其是字符串输入输出的题还是需要自己写上面这些核心函数。3. 真正拉开差距的是大数、负数和带小数的转换如果说上面那些是常规题那接下来这三个场景就是典型的高分拦路虎大数、负数、小数。它们不是你会不会除基取余的问题而是你对语言特性和数值表示理解有多深的问题。3.1 大数进制转换用数组模拟长除法进制转换题一旦出现长度上几十位甚至上千位的数字任何标准整数类型都不够用因为unsigned long long最大也只能表示到约1.8 * 10^19。这个时候还不换思路写出去的程序就是溢出之后再循环结果完全不可控。正确的思路是把十进制大数当成字符串模拟手算长除法从最高位开始每一位取出上一位余数 × 10 当前位计算出商和余数余数传给下一位商的每一位重新拼成字符串一轮扫描结束后收集到的余数就是目标进制的一位低位。反复执行直到被除数变成全 0。参考实现#include stdio.h #include string.h void bigDecToBase(char *dec, int base, char *out) { static const char digits[] 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ; int len strlen(dec); int idx 0; while (1) { int rem 0; int i; for (i 0; i len; i) { int cur rem * 10 (dec[i] - 0); dec[i] (char)(cur / base 0); rem cur % base; } out[idx] digits[rem]; for (i 0; i len dec[i] 0; i) { } if (i len) { break; } } out[idx] \0; for (int i 0; i idx / 2; i) { char t out[i]; out[i] out[idx - 1 - i]; out[idx - 1 - i] t; } } int main() { char num[] 255; char res[128] {0}; bigDecToBase(num, 16, res); printf(%s\n, res); // FF return 0; }注意这个函数会直接修改传入的dec字符串所以调用前如果需要保留原始数据一定要先复制一份。每次外层循环本质上是对整个十进制字符串做一次长除法目标进制位数越多外层循环次数越多时间复杂度约在 O(位数 × 目标进制位数) 的量级对常规题目的数据范围完全够用。如果你在机考里遇到这种大数题能把这个模板默写出来基本就稳了。3.2 负数转换向零取整的坑与补码处理负数转进制是另一个经典陷阱。很多人下意识写void decToBase(int n, int base)然后照搬除基取余结果测试时发现-7转二进制输出了一堆奇怪的数字。问题不在公式而在 C 语言的除法语义。C 语言规定整数除法向零取整而取模结果满足(a / b) * b a % b a这个等式。于是-7 / 2 -3-7 % 2 -1余数出现了负数。如果按tmp[i] digits[n % base]去下标访问字符数组digits[-1]就是未定义行为程序直接乱套。处理负数有两种常见思路。第一种如果题目只要求把负号保留在结果前面后面跟绝对值转换就先记录符号再基于绝对值的无符号形式计算#include stdio.h void decToSigned(int n, int base, char *out) { unsigned int un (unsigned int)n; int negative 0; char tmp[64]; int i 0, j 0; static const char digits[] 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ; if (n 0) { negative 1; un (unsigned int)(-(n 1)) 1; // 安全求绝对值避免 INT_MIN 溢出 } if (un 0) { out[0] 0; out[1] \0; return; } while (un 0) { tmp[i] digits[un % base]; un / base; } if (negative) { out[j] -; } while (i 0) { out[j] tmp[--i]; } out[j] \0; }第二种如果题目要求输出补码位模式就不能简单加负号了。补码本质上是无符号整数取模2^bits后的位模式可以统一按unsigned int处理再逐位转二进制。无论哪种关键都是先想清楚题目要的是带符号字符串还是要补码位序列想清楚了再动手能避免大量无效调试。3.3 小数进制转换乘基取整与浮点精度陷阱小数转进制用的方法是乘基取整小数部分不断乘以目标进制每次取整数部分作为下一位直到小数部分为 0 或达到要求精度。举一个十进制 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结束所以 0.6875 写成二进制是 0.1011。这个例子刚好有限但很多人第一次用double写小数转换会惊讶地发现 0.1 怎么乘都乘不到 0。原因很简单十进制 0.1 在二进制中是无限循环小数double只能保存近似值。于是实际代码里常见精度尾差问题明明该在某个位置停止程序却因为浮点误差永远停不下来。处理办法有三种一是限制循环位数达到指定小数位数就直接截断二是把小数转换成整数 放大倍数用定点数思想避免浮点误差三是题目如果明确给的是类似0.6875这样的字符串就通过字符串运算做精确转换。机考里面最稳妥、最不容易出边界问题的做法是第一种加上一个最大迭代次数保护防止死循环。4. 五个高频易错点和排查思路我在带新人练题和帮朋友调代码时发现进制转换题犯错的地方非常集中下面这五个细节值得专门花时间做一组针对练习。4.1 0 和空串的处理不一致n 0的情况是出现频率最高的遗漏点。很多人写好除基取余函数后用 255、128 这种正数测试全通过一提交就有一两个用例报错大概率就是题目给了0。另一个相关坑是输入字符串本身是空串的情况如果题目没有说明输入保证非空转十进制前应该先判s[0] \0输出 0 或者在题目要求下输出空串。我的习惯是写一个统一的边界判断if (n 0 || s[0] \0)再决定走0还是别的逻辑。4.2 字符串与数字互转的边界字符0对应 ASCII 码 48数字 0 就是整数 0这两个经常被混用。最典型的问题出现在缓冲区没有清空char buf[64];声明之后没有初始化往里面写字符串时最后忘了写\0printf(%s)就会从缓冲区一直读到随机内存输出后面跟着一串乱码。所以我在所有手写转换函数里约定函数出口必须执行out[j] \0或out[idx] \0一秒钟都不能偷懒。另外字符转换时c - 0和c - A 10这两套偏移也必须分开算混在一起结果必然错位。4.3 字母大小写与逆序缓冲区十六进制和三十六进制转换涉及字母输出。有些题明确要求大写有些要求小写还有些不区分大小写输入但输出必须统一。这里最容易踩的坑是输入函数只处理了大写A-Z结果测试数据给了小写a-z程序直接返回非法字符错误。稳妥做法是在字符转数值时大小写都支持而在输出时用一个统一数组限定大小写比如上面代码里的digits[]用大写如果题目要求小写最后再做一次tolower转换或者干脆准备两套映射表。逆序缓冲区同理用两个下标变量i和j做对称交换的代码建议封装成独立函数别在多个地方重复写重复写就意味着多一个出错位置。4.4 类型溢出与非法输入常规题里N 进制字符串转十进制时如果原始数据接近unsigned long long上限累加过程中可能溢出。事先估算数据范围很重要unsigned int最大约 42 亿unsigned long long最大约 1844 亿亿如果题目的进制数和位数导致结果可能超出就要直接走大数方案而不是硬用整数类型硬扛。非法输入也是常见坑比如题里保证输入合法的时候你可以不判断但如果不保证就必须像baseToDec那样检查每一个字符的val是否在[0, base-1]区间内。很多排行榜上WA 到怀疑人生的提交就是败在非法字符没拦截。4.5 多组测试数据的输入写法在线评测的输入格式经常是多组测试数据每组一行处理不好就会少读组数或留下换行符。推荐的模板是char s[1024]; while (scanf(%s, s) ! EOF) { // 处理一组合法数据 }不要用gets也不要先scanf(%d)再getchar()读字符串除非你非常清楚整个输入流里换行符的位置。如果一行里既有进制数又有字符串用scanf(%d%s, base, s)一次性读完比手动跳过空白字符更省心。5. 一道综合练习字符串大数转十六进制完整代码前面都是拆开讲最后用一个稍微综合的练习把知识点串起来。这是我在练习时觉得很有代表性的题目适合作为进制转换训练的一个收尾。5.1 题目要求与解题思路题目描述大概是输入一个长度不超过 1000 的十进制非负整数用字符串表示要求输出它对应的十六进制大写字符串禁止使用printf(%x)或sprintf直接格式化完成。这个题目如果直接sscanf(dec, %llu, num)再printf(%llX, num)遇到长字符串立刻溢出显然不是正确解法。正解就是前面 3.1 节的大数转进制模板把十进制字符串按位拿出来反复做长除法每次除 16余数映射成十六进制字符最后把所有余数逆序输出。这个过程不依赖任何整数类型能容纳原始大数所以长度 1000 也照样能算。5.2 参考实现与测试用例完整实现如下我把它整理成一个可以直接运行的 C 程序#include stdio.h #include string.h void trimLeadingZeros(char *s) { int i 0; while (s[i] 0 s[i 1] ! \0) { i; } if (i 0) { int j 0; while (s[i] ! \0) { s[j] s[i]; } s[j] \0; } } void decimalStringToHex(const char *decIn, char *hexOut) { static const char digits[] 0123456789ABCDEF; char dec[1024]; char tmp[2048]; int len; int idx 0; strcpy(dec, decIn); trimLeadingZeros(dec); len (int)strlen(dec); while (1) { int rem 0; int i; for (i 0; i len; i) { int cur rem * 10 (dec[i] - 0); dec[i] (char)(cur / 16 0); rem cur % 16; } tmp[idx] digits[rem]; for (i 0; i len dec[i] 0; i) { } if (i len) { break; } } hexOut[idx] \0; for (int i 0; i idx / 2; i) { char t tmp[i]; tmp[i] tmp[idx - 1 - i]; tmp[idx - 1 - i] t; } strcpy(hexOut, tmp); } int main() { char hex[2048]; decimalStringToHex(0, hex); printf(%s\n, hex); // 0 decimalStringToHex(255, hex); printf(%s\n, hex); // FF decimalStringToHex(4096, hex); printf(%s\n, hex); // 1000 decimalStringToHex(18446744073709551615, hex); printf(%s\n, hex); // FFFFFFFFFFFFFFFF return 0; }额外加了一个trimLeadingZeros它的作用是去掉前导 0避免000255这种输入导致长除法提前被误判为全 0。测试用例里我特意放了四组0验证特判255验证常规转换4096验证十六进制进位的输出1000最后一个18446744073709551615是unsigned long long的最大值验证大数路径。这四组如果全过这个函数基本可以放心提交。说句实在话进制转换题不像动态规划那样有特别高的思维门槛它更像是一场细节大考。我把这套代码和易错点整理出来之后自己刷题的速度明显提高了一些因为我每次写完都会先跑一遍那组固定边界用例再去看题目里有没有特别刁钻的输入限制。如果你也在为机考刷题我的建议是不要只背模板而是把除基取余、按权展开、大数模拟除法这三板斧亲手各写三遍写成肌肉记忆再遇到任何进制转换题都能在一两分钟内把框架铺开把精力留给真正的边界判断。