C语言数制转换实战:栈与除基取余的完整实现
简介针对初学数据结构的C语言学习者这份资源以顺序栈为核心演示如何将十进制数转换为八进制等任意进制正好补足严蔚敏教材中伪代码不易直接运行的痛点给出可直接调试的完整实例。压缩包内仅1个PDF文件大小47KB正文包含SqStack结构体、初始化、入栈、出栈、栈空判断以及conversion()转换函数的完整源码并对算法3.1的执行流程逐段注解同时呈现了动态扩容与越界处理等关键细节。已有894人学习下载适合正在复习栈与进制转换、希望对照教材伪代码进行实操验证的读者。通过阅读PDF可掌握利用栈后进先出特性保存余数并逆序输出结果理解n % m入栈、n n / m迭代更新的核心思路若需转换为二进制或十六进制只要修改目标进制输入m即可能够举一反三扩展应用。1. 数制转换实例代码为什么数据结构课第一道栈实验题就能拦住一批人身边不少人在第一次上机时被这道题卡住十进制25转二进制手算明明是11001代码一跑却输出10011十进制100万转二进制结果位数少了一截转十六进制倒是出来了字母却是小写考试偏偏要求大写。其实数制转换的核心就是一句「除基取余、余数入栈、出栈逆转」十行逻辑真正拉开分差的是边界条件、进制校验和溢出处理。这篇文章用一个可运行的C语言实例代码集合把整数转换、小数转换、负数处理、任意进制互转和五个高频翻车场景一次说清适合正在写数据结构实验报告、刷考研数据结构题或者刚学完栈想找练习的人直接照着改。2. 除基取余法完整实现十进制转二、八、十六进制的代码与三个必调参数除基取余是数制转换的地基也叫「除N取余法」十进制整数转N进制不断用N去除目标数记下每次的余数最后把余数序列倒序输出。第一次除出来的余数是结果的最低位最后一个余数是最高位。这个「后进先出」的顺序和栈的语义完全吻合所以数据结构与算法教材几乎都把数制转换放在栈那一章的课后题里连《大话数据结构》讲栈的应用时也拿它当第一个例子。实际开发里我们常用数组直接逆序输出未必每次都建一个正经栈结构但理解栈在这里扮演的角色对你后面学表达式求值、递归转非递归都有帮助。这一章给三个从简到繁的版本数组模拟栈的最小代码、标准顺序栈接口、带字符映射的十六进制转换。你按自己实验报告的进度挑一个用。2.1 数组模拟栈能跑通的最小可运行版本先看最直观的版本。一个一维数组、一个栈顶指针top入栈就是stack[top] value出栈就是return stack[top--]不需要结构体一个stdio.h就能跑。#include stdio.h #define MAX_STACK 32 // 32位int二进制最多31位留一位给结束符/边界 int stack[MAX_STACK]; int top -1; void push(int value) { if (top MAX_STACK - 1) { return; // 栈满直接丢弃调用方自行判断 } stack[top] value; } int pop(void) { if (top 0) { return -1; // 空栈返回-1正常余数范围是0~15不会撞 } return stack[top--]; } void decToBin(unsigned int n) { if (n 0) { printf(0\n); return; } while (n 0) { push(n % 2); n / 2; } while (top 0) { printf(%d, pop()); } printf(\n); } int main(void) { decToBin(25); // 期望输出 11001 decToBin(0); // 期望输出 0 return 0; }代码逻辑分两段while (n 0)负责把余数一个个压进去第二个while (top 0)负责弹栈输出。中间不要混写混写的后果就是顺序错乱这个问题在后面避坑章节还会专门讲。这里有两个参数值得按需改MAX_STACK和取模基数。MAX_STACK设成32是因为32位无符号整数转二进制最多31位转八进制最多11位转十六进制最多8位32足够。你要是转long long范围的大数就把这个宏改成64或再加一位。取模基数改成8或16printf的%d不用动就能直接输出八进制或十六进制的数字串——因为0到15这十六个数字正好是十进制符号的子集。这个版本的特点就是短适合考试手写和实验报告第一版交差。缺点是top是全局变量你在一个程序里同时跑多个转换函数栈状态会互相串。这也是下一步引出顺序栈结构体的原因。2.2 顺序栈接口InitStack到DestroyStack的标准写法数据结构实验报告通常要求体现「抽象数据类型」意识也就是把栈的操作封装成函数集。常见做法是把栈定义成结构体让每个调用方持有一份独立实例下面这套接口和严蔚敏《数据结构C语言版》里的写法基本一致。#include stdio.h #include stdlib.h #define STACK_INIT_SIZE 16 #define STACK_GROW_FACTOR 2 typedef struct { int *base; // 栈底指针malloc出来的堆内存 int *top; // 栈顶指针始终指向下一个可用位置 int capacity; // 当前容量按元素个数计 } SqStack; void initStack(SqStack *s) { s-base (int *)malloc(STACK_INIT_SIZE * sizeof(int)); if (!s-base) exit(EXIT_FAILURE); s-top s-base; s-capacity STACK_INIT_SIZE; } void push(SqStack *s, int value) { if (s-top - s-base s-capacity) { int *newBase (int *)realloc(s-base, s-capacity * STACK_GROW_FACTOR * sizeof(int)); if (!newBase) exit(EXIT_FAILURE); s-base newBase; s-top s-base s-capacity; // 关键realloc后必须重算top s-capacity * STACK_GROW_FACTOR; } *s-top value; } int pop(SqStack *s) { if (s-top s-base) { return -1; // 空栈 } return *--s-top; } void destroyStack(SqStack *s) { free(s-base); s-base NULL; s-top NULL; s-capacity 0; }这套代码的注意点有两个。第一STACK_INIT_SIZE设成16理由是单次十进制转二进制的余数最多31个16不够用会自动扩容一次不至于一直在realloc。第二扩容后必须重新计算toprealloc可能把整块内存搬到新地址旧指针全部失效把top重设成新base 旧容量指向旧数据末尾之后*s-top value才能接着写。这是我见过动态栈最常见的野指针来源。至于扩容因子为什么选2而不是1.5纯属C语言的现实考虑realloc的扩容语义由内存分配器决定不存在「原地扩容」保证每次扩容都可能搬运。因子越大搬运次数越少最坏浪费的空间越多因子越小空间利用率越高但搬运越频繁。数据结构C语言版课程里用2倍是约定俗成因为它实现简单、均摊复杂度容易算。2.3 十进制转十六进制字符映射表与大小写约定的取舍十六进制和二进制有个本质区别二进制余数只有0和1直接打印数字即可十六进制余数范围0到15其中10到15必须映射成A到F。这时候需要一张映射表常见做法是预定义一个字符串当查表字典。#include stdio.h #include string.h static const char HEX_DIGITS[] 0123456789ABCDEF; void decToHex(unsigned int n, char *out, size_t outSize) { char tmp[11]; // 32位整数十六进制最多8位10足够 int idx 0; if (n 0) { tmp[idx] 0; } else { while (n 0) { tmp[idx] HEX_DIGITS[n % 16]; n / 16; } } if ((size_t)idx 1 outSize) { return; // 输出缓冲区不够宁可失败也不要越界写 } for (int i 0; i idx; i) { out[i] tmp[idx - 1 - i]; // 逆序拷贝 } out[idx] \0; } int main(void) { char buf[12]; decToHex(255, buf, sizeof(buf)); printf(%s\n, buf); // 期望输出 FF return 0; }HEX_DIGITS把余数直接当下标用比一串if-else简洁得多。这里有个独立的决策点题目要求小写输出时最稳的做法是把表改成0123456789abcdef而不是在输出阶段用tolower()统一转。因为tolower会把数字字符0到9也过一遍结果虽然碰巧一样逻辑上却不干净实验报告里容易被老师划问号。如果你要同时支持大小写就再加一个int uppercase参数查表前判断一次。注意decToHex里的outSize检查不能省。实验报告里常见的问题是输出缓冲区开在栈上函数内部不知道边界一旦目标数超过预设位数就是越界写轻则乱码重则段错误。宁可多写三行防御也不要花两小时查一个内存越界。3. 数制转换的小数与负数乘基取整的精度控制与符号位处理整数转换写完实验报告第二问往往是「把十进制小数转成二进制小数」。原理叫「乘基取整」小数部分乘以2取出整数位作为二进制小数的第一位剩下的小数部分继续乘2直到小数部分归零或达到指定精度。这句话里藏着一个浮点数的黑匣子——很多初学者用while (frac ! 0)判断循环结束结果发现double里的0.1转二进制永远除不尽程序直接死循环。这一章把小数和负数一次说透两条都是考试和上机的常客。3.1 乘基取整法循环终止条件不能只靠「小数部分为0」#include stdio.h void decFracToBin(double frac, int precision) { printf(0.); for (int i 0; i precision; i) { frac * 2.0; int bit (int)frac; // 取出整数位 printf(%d, bit); frac - (double)bit; // 保留小数部分继续乘 if (frac 1e-12) { // 足够接近0就提前结束 break; } } printf(\n); } int main(void) { decFracToBin(0.625, 16); // 期望输出 0.101 decFracToBin(0.1, 16); // 期望输出 0.0001100110011001... return 0; }precision是输出精度必须显式传入。为什么不能靠frac 0退出因为0.625在二进制下是精确的(0.101)它能结束是因为0.625恰好是2的负一次方加2的负二次方加2的负三次方而0.1在二进制下是无限循环小数double只能存近似值永远到不了0。实际工程里小数转进制基本都限定位数二进制给16位八进制和十六进制给6到8位就够还原精度了。判断frac 1e-12是因为double的机器精度大约在1e-15量级阈值放松两个量级既能避免死循环又不会在还有效的小数位时提前截断。还有一个影响输出的细节(int)frac是向零取整如果frac恰好在0.9999999999附近取整后得到0可能丢一位有效数字。严谨的做法是先frac 1e-12再取整。我在自己代码里一般不加因为16位精度下误差会被后面位吃掉但如果要求输出20位以上就得加上这个修正。3.2 负数怎么转符号位单独处理比补码直转更符合题意负数转换是高频考点。最常见、最稳的做法是把符号单独拿出来先输出一个-再对绝对值做除基取余。不建议把负数直接强转成无符号数再套算法因为那样转出来的是补码的二进制表示和数学意义上的「数制转换」是两回事而且容易把结果搞成一大串1。void decToBaseWithSign(long long n, int base, char *out, size_t outSize) { unsigned long long absVal; char tmp[80]; int idx 0; int sign 0; if (n 0) { sign 1; absVal -(unsigned long long)n; // 先转无符号再取负避免LLONG_MIN溢出 } else { absVal (unsigned long long)n; } if (absVal 0) { tmp[idx] 0; } while (absVal 0) { tmp[idx] 0123456789ABCDEF[absVal % base]; absVal / base; } if ((size_t)(sign idx 1) outSize) { return; } int pos 0; if (sign) { out[pos] -; } for (int i idx - 1; i 0; i--) { out[pos] tmp[i]; } out[pos] \0; }这段代码里最值得圈出来的是一行absVal -(unsigned long long)n;。为什么不能直接写absVal -n因为当n恰好等于LLONG_MIN时-n在long long范围内直接溢出未定义行为结果还是负数。必须先转成无符号型再取负借无符号整型的回绕语义拿到正确的绝对值。这个边界值你可能一年都碰不到一次但碰到就是查半天的疑难bug。符号位单独输出的另一个好处是代码结构清晰实验报告里老师一眼能看到你的逻辑层次正数负数共用转换核心符号只是前缀。3.3 整数与小数合一的完整转换实例double型入参的两个精度参数前面两节各自独立实际实验题会要求你把它们拼成一个函数输入一个double输出它的二进制表示整数部分和小数部分都要处理。#include stdio.h void decToBinFull(double value, int intPrecision, int fracPrecision) { long long intPart (long long)value; double fracPart value - (double)intPart; if (value 0) { printf(-); intPart -(long long)intPart; fracPart -fracPart; } char tmp[64]; int idx 0; if (intPart 0) { tmp[idx] 0; } while (intPart 0) { tmp[idx] 0 (intPart % 2); intPart / 2; } for (int i idx - 1; i 0; i--) { printf(%c, tmp[i]); } if (fracPrecision 0) { printf(.); for (int i 0; i fracPrecision; i) { fracPart * 2.0; int bit (int)fracPart; printf(%d, bit); fracPart - (double)bit; if (fracPart 1e-12) break; } } printf(\n); } int main(void) { decToBinFull(-9.625, 32, 16); // 期望输出 -1001.101 return 0; }这里的intPrecision其实没有参与循环控制因为除基取余到0自然结束不需要位数上限。真正影响输出的是fracPrecision同样是0.18位精度输出0.0001100116位输出0.0001100110011001后者的还原误差小一到两个数量级。考试题如果写「精确到8位二进制小数」你就传8别多也别少。(long long)value这一步隐含了一个前提——这个函数只处理绝对值小于long long上限的数超出部分会被截断真遇到超大数要用后面的十进制字符串方案。4. 任意进制互转实例代码从字符串输入到进制校验的一条龙实现课程设计比实验题再进一步往往是「输入一个N进制数转换成M进制」。这要求程序能处理字符串输入而不只是拿内存里的整数转换。经典思路是「任意进制→十进制→任意进制」中间用unsigned long long当桥梁。这一章的代码可以直接抄进数据结构实验报告也可以改造成命令行小工具。它同时覆盖了多数C语言基础练习里没细讲的进制校验问题。4.1 任意进制字符串转十进制逐位累乘与非法字符校验把一个字符串按指定进制解析成数值用的是「多项式累乘法」从高位往低位每读一位把已有结果乘以进制再加上当前位的值。这个算法本质上就是霍纳法则二进制转十进制时你会觉得眼熟——1011就是((1*20)*21)*21。#include stdio.h #include string.h #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 anyBaseToDec(const char *num, int base) { unsigned long long result 0; int len strlen(num); for (int i 0; i len; i) { int val charToVal(num[i]); if (val 0 || val base) { return 0; // 非法字符或该字符超出进制上限 } result result * base (unsigned long long)val; } return result; }charToVal把字符映射成数值同时支持大小写字母这是「进制不分大小写」这个约定带来的便利。校验放在累乘的同一轮循环里不需要先单独扫描一遍省一趟遍历。base合法范围是2到36超过36字符映射表就不够用了。调用方必须注意返回值0可能表示合法输入0也可能表示非法输入所以4.3节的主程序里会先strcmp判断再决定是否报错。这属于接口设计的取舍——用返回0当错误码省一个指针参数代价是调用方多一行判断。4.2 十进制转任意进制查表编码与输出缓冲区的安全边界void decToAnyBase(unsigned long long n, int base, char *out, size_t outSize) { const char *digits 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ; char tmp[80]; int idx 0; if (n 0) { tmp[idx] 0; } while (n 0) { tmp[idx] digits[n % base]; n / base; } if ((size_t)idx 1 outSize) { return; } int pos 0; for (int i idx - 1; i 0; i--) { out[pos] tmp[i]; } out[pos] \0; }这段和2.3的十六进制转换几乎一样区别只在映射表从16个字符扩展到36个。所以「任意进制」本质上就是「给映射表加长」这么简单。tmp[80]对unsigned long long来说绝对够64位整数转二进制也只要64个字符加上结束符65字节80留足了余量。输出缓冲区的outSize检查依然不能省我把它看成数组操作的「安全带」。如果你把out指向一个栈上开好的char buf[16]却想塞进一个32位的十六进制结果这个检查能拦住一次越界写。4.3 组合运行支持2到36进制互转的完整主程序#include stdio.h #include string.h int main(void) { char input[100]; int fromBase, toBase; printf(输入数值: ); scanf(%99s, input); printf(输入源进制(2-36): ); scanf(%d, fromBase); printf(输入目标进制(2-36): ); scanf(%d, toBase); if (fromBase 2 || fromBase 36 || toBase 2 || toBase 36) { printf(进制必须在2到36之间\n); return 1; } unsigned long long dec anyBaseToDec(input, fromBase); if (dec 0 strcmp(input, 0) ! 0) { printf(输入包含非法字符或超过进制范围\n); return 1; } char result[80]; decToAnyBase(dec, toBase, result, sizeof(result)); printf(结果: %s\n, result); return 0; }这段主程序把4.1和4.2串起来也是我推荐的「最小完整工具」结构输入→校验→转换→输出四步各干各的。scanf(%99s, input)的99是故意留的防线防止超长输入写爆数组这是C语言基础里反复强调的scanf宽度限制。strcmp(input, 0)把「合法零」和「非法输入」分开解决了4.1留下的歧义。你把它编译跑一遍输入FF、源进制16、目标进制2输出11111111。想拿去当课程设计交差再加个while循环让用户连续转换加个exit或quit指令就算完整题目了。5. 数制转换避坑指南逆序、溢出、字符映射的5个翻车现场这一章全是踩坑记录来源是我自己调试和帮人改代码的血泪经验。每条按「现象→原因→解决」写你在实验或考试碰到同类问题直接对号入座。5.1 结果少一截还带乱码数组容量不够的越界写现象十进制2147483647转二进制输出的字符串明显比预期短尾部还跟着一串乱码。原因char result[32]只留了32字节。32位int二进制最多31位加上结束符\0正好32字节勉强够一旦你处理的是long long64位二进制需要65字节数组直接越界写入相邻内存把别人的数据踩了。解决临时字符数组统一开成char tmp[80]并在逆序拷贝前检查idx 1 outSize。宁可多开几十字节也别省这块栈空间。5.2 二进制转十进制出来负数int装不下的整数溢出现象输入111111111111111111111111111111131个1转十进制得到负数或和手算不一致的值。原因int范围是-2147483648到214748364731个1正好是2147483647贴着边界32个1直接溢出成-1符号位被占用。解决累乘累加的临时变量用unsigned long long最多装64位整数的二进制表示。如果目标数本身超过64位那就得手写大数除法用数组模拟长除法每一步记录商和余数——这是考研数据结构里的进阶题型数制转换只是引子。5.3 输出顺序永远是反的忘了栈的后进先出现象25转二进制输出10011手算是11001。原因除基取余得到的余数序列低位在前必须逆序才是高位在前。很多人把输出直接写在第一次余数产生的循环里那等于先打印低位顺序必反。解决让入栈和出栈各占一个循环中间别混写。判断标准很简单第一个产生的余数是结果的最低位一定最后打印。这个错误在多进制转换里同样出现属于数制转换最经典也最冤的扣分点。5.4 scanf吃换行导致第二轮输入为空格式化匹配的坑现象程序支持连续转换跑完第一轮第二轮连输入提示都没正常走或者scanf(%s)读到空串直接退出。原因上一轮scanf(%d, base)之后输入缓冲区里残留一个\n下一轮scanf(%s, input)不会跳过空白字符一下子把空行当输入读了。解决在字符串输入前加while (getchar() ! \n);清掉残留或把格式串写成 %99s前面加个空格scanf会跳过分隔符。很多入门C语言题库和在线练习系统里这类症状都指向同一个原因排查顺序放在第一位。5.5 LLONG_MIN取负溢出64位边界的符号陷阱现象对-9223372036854775808做abs()或直接取负结果还是负的后续转换全部错乱。原因LLONG_MIN的绝对值是9223372036854775808超出long long正数上限取负行为未定义。解决先强转成unsigned long long再取负写法是-(unsigned long long)n借无符号类型的回绕语义拿到正确绝对值。这条和5.2本质一样都是「有符号整数边界」在数制转换里的具体表现。我建议你在写转换函数时只对无符号整数做转换运算符号单独挂在外层能同时绕开这两个坑。6. 进阶验证用位运算重写转换函数并跑一个自测用例二进制转换还有一种更贴近机器语义的写法n % 2等价于n 1n / 2等价于n 1。位运算版本在不开优化时比除法版本快开-O2后编译器会把除以常数2优化成移位两者几乎持平。所以位运算的意义更多在于帮助你理解二进制在计算机里的真实形态以及面试时应对「不用除法实现数制转换」这类题。void decToBinBitwise(unsigned int n, char *out) { char tmp[33]; int idx 0; if (n 0) tmp[idx] 0; while (n 0) { tmp[idx] 0 (n 1); n 1; } int pos 0; for (int i idx - 1; i 0; i--) { out[pos] tmp[i]; } out[pos] \0; }验证方法建议写一个对照测试对0到100000之间的每个整数分别用除法和位运算法生成二进制串strcmp逐一比对再用clock()统计总耗时。我自己常加一组自我校验数据覆盖0、1、base-1、base、base*base、UINT_MAX这几个边界值任意进制互转则做「往返测试」——decToAnyBase之后立刻anyBaseToDec看结果是否还原。这套做法我沿用多年比肉眼检查输出可靠得多也是我这些年翻车明显变少的主要原因。希望帮到你。本文还有配套的精品资源点击获取