简介:针对C语言初学者与数据结构学习者,这里提供一份数制转换实例PDF,聚焦用顺序栈实现十进制到八进制(可扩展任意进制)的转换。压缩包内含1个PDF,共47KB,便于快速下载。目前已有894人学习参考,适合课内练习与自学巩固。文档从顺序栈的构建入手,给出栈初始化、压栈、弹栈、判空等操作,并完整实现conversion()函数。代码将十进制数n除以目标进制m取余数,依次压入栈中,再按后进先出顺序弹出输出,实现从低位到高位的逆序排列。全程包含详细注释,修改m值即可适配二进制、八进制、十六进制等场景。此外,文档还梳理了算法执行流程与栈在逆序处理中的关键作用,便于理解数制转换的底层逻辑。无论预习还是复习,都可作为便捷的参考模板。
1. 数制转换是栈的入门题,但很多人第一次都写反了
某高校的课程设计里,十进制转二进制、八进制、十六进制是出现频率最高的题目之一。大多数同学第一版代码是把每次的余数存进数组,最后逆序打印——能跑,但总让人觉得少了点什么。直到引入栈之后才发现,数制转换简直是为栈量身定做的:后进先出的特性正好把“先算出来的余数最后输出”这个矛盾化解掉。这篇文章就从这份C语言数制转换实例代码出发,把数组栈、链表栈两种写法、批量转换的扩展、以及调试过程中最常踩的坑完整过一遍。适合正在学数据结构、准备课程设计或应付机试的人,新手能照着敲,熟手可以直接看第5章的排查清单。
2. 为什么数制转换总是配栈:原理与代码骨架
2.1 手动算一遍 123 转二进制,你就明白栈的价值
拿 123 转二进制举例。短除法过程是:123 除以 2 得商 61 余 1;61 除以 2 得商 30 余 1;30 除以 2 得商 15 余 0;一直除到商为 0。把余数从下往上读:1111011,这就是 123 的二进制。
问题出在“从下往上读”。我们最先算出来的是最低位,最后算出来的是最高位。如果拿数组顺序存储,存的是 1、1、0、1、1、1、1,打印时得从尾部往前。而栈结构天然就是反着来的——先入栈的余数最后出栈,刚好把顺序倒过来。所以只要把每次的余数压栈,算完后依次弹栈打印,就是正确结果。
这个思路推广到八进制、十六进制完全一样,只是除数从 2 变成 8 或 16。代码上唯一多一步的是十六进制的字母映射:余数 10 到 15 要转成 A 到 F。
2.2 栈的三个核心操作在转换里的角色
数制转换只需要用到栈的三个最基本操作:初始化、压栈、弹栈。不涉及查找、遍历这类复杂操作。
初始化负责分配存储空间并让栈顶指针归零。压栈把每次的余数写进去,同时栈顶指针加一。弹栈把栈顶元素取出来,栈顶指针减一。转换主流程就是不断做“求余、压栈”,最后“循环弹栈”的过程。
注意:压栈和弹栈的顺序不能乱。先求余数再压栈,算完所有位数之后才开始弹栈,不能在计算过程中边算边弹,否则顺序又反了。
2.3 数组栈和链表栈的选型边界
这份实例代码里两种栈都给了。数组栈实现简单、访问快、代码量少,缺点是栈容量固定。如果你只是转一个不超过 32 位的整数,数组栈完全够用。但如果你要做批量转换,源数据里可能混着一个非常大的数,数组栈就可能溢出,这时候链表栈更稳妥。
链表栈每个节点动态分配,理论上只受内存限制,不担心预先开多大。代价是每次压栈都要 malloc,代码量也更大一些。实际项目里,我一般遵循这个原则:机试和课程设计用数组栈,代码短不容易出错;数据规模不可预估、或者要转很多个数的真实场景用链表栈。
3. 数组栈版本实测:完整代码与逐段讲解
3.1 从结构体定义到转换主流程
这份代码的核心思路很简单:定义栈结构体,实现压栈、弹栈,然后用短除法循环把余数压栈,最后弹栈输出。下面是去掉注释干扰的核心骨架:
#include <stdio.h> #include <stdlib.h> #define MAXSIZE 64 #define OK 1 #define ERROR 0 typedef struct { int data[MAXSIZE]; // 栈的存储数组 int top; // 栈顶指针,-1 表示空栈 } SeqStack; // 初始化栈 void InitStack(SeqStack *s) { s->top = -1; } // 判断是否为空 int IsEmpty(SeqStack *s) { return s->top == -1; } // 压栈 int Push(SeqStack *s, int x) { if (s->top == MAXSIZE - 1) { return ERROR; // 栈满 } s->data[++(s->top)] = x; return OK; } // 弹栈 int Pop(SeqStack *s, int *x) { if (IsEmpty(s)) { return ERROR; } *x = s->data[(s->top)--]; return OK; } // 十进制转任意进制,base 为目标进制 void Convert(SeqStack *s, int num, int base) { while (num > 0) { Push(s, num % base); // 余数入栈 num = num / base; // 更新商 } printf("转换结果: "); while (!IsEmpty(s)) { int digit; Pop(s, &digit); if (digit < 10) { printf("%d", digit); } else { printf("%c", 'A' + digit - 10); // 10~15 映射为 A~F } } printf("\n"); } int main() { SeqStack s; InitStack(&s); int num, base; printf("请输入十进制数: "); scanf("%d", &num); printf("请输入目标进制(2/8/16): "); scanf("%d", &base); Convert(&s, num, base); return 0; }这段代码有三个地方值得细说。首先是top初始化为 -1 而不是 0,这样空栈判断就是top == -1,压栈时先自增再赋值,语义更清晰。其次是num % base和num / base的顺序,必须先取余再整除,因为整除会改变 num 的原值。最后是十六进制输出分支,'A' + digit - 10这个表达式把 10 映射到 A、11 映射到 B,依次类推。
3.2 参数怎么改:MAXSIZE、base 的取值范围
MAXSIZE设为 64 足够容纳任何 32 位整数的二进制表示(32 位二进制最多 32 个字符,64 留了余量)。如果你要转的是 64 位整数,把MAXSIZE调到 80 更稳妥。栈满时Push返回ERROR,但在转换场景里极少触发,仍然建议保留这个检查,避免极端数据下内存越界。
base参数在这个代码里理论上可以是 2 到 36 之间任意整数,只要数字部分在 10 以内用数字输出、10 以上用字母输出。但实际测试中我只推荐 2、8、16,因为 4 进制、7 进制这类不常用,而且输出可读性差。
3.3 一个容易忽略的边界:输入为 0 怎么办
上面代码用while (num > 0)作为循环条件,如果输入 0,循环一次都不执行,弹栈时栈为空,最终什么都打不出来。正确做法是把循环改成do...while,至少执行一次,把 0 压进去。这是我的一个习惯性修改:
void Convert(SeqStack *s, int num, int base) { do { Push(s, num % base); num = num / base; } while (num > 0); // 弹栈输出部分不变 }这样 0 转任意进制都会输出 “0”,符合逻辑。负数的处理思路稍有不同,见第 5 章排查清单。
4. 链表栈版本:从固定容量到动态分配
4.1 链表栈完整实现
链表栈的核心是把每个节点看成一个独立的存储单元,压栈时新建节点,弹栈时释放节点。下面给出可直接编译运行的实现:
#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } StackNode; typedef struct { StackNode *top; // 栈顶指针 int count; // 栈内元素个数 } LinkStack; // 初始化 void InitLinkStack(LinkStack *s) { s->top = NULL; s->count = 0; } // 压栈:头插法 int PushLink(LinkStack *s, int x) { StackNode *newNode = (StackNode *)malloc(sizeof(StackNode)); if (newNode == NULL) { return 0; // 内存分配失败 } newNode->data = x; newNode->next = s->top; s->top = newNode; s->count++; return 1; } // 弹栈:删除头节点 int PopLink(LinkStack *s, int *x) { if (s->top == NULL) { return 0; // 空栈 } StackNode *temp = s->top; *x = temp->data; s->top = temp->next; free(temp); // 释放节点内存 s->count--; return 1; } // 转换 void ConvertLink(LinkStack *s, int num, int base) { do { PushLink(s, num % base); num = num / base; } while (num > 0); int digit; while (PopLink(s, &digit)) { if (digit < 10) { printf("%d", digit); } else { printf("%c", 'A' + digit - 10); } } printf("\n"); }链表栈的弹栈比数组栈多了一个关键步骤:free(temp)。如果不释放节点,批量转换几万个数就可能导致内存持续增长,程序越来越慢。count字段在这里不是必须的,但调试时打印s->count能快速确认压栈弹栈的次数是否匹配预期。
4.2 批量转换:从文件读入多组数据
单个数转换演示意义大于实用意义。实际课程设计里更多是批量转换,比如从一个文本文件读入几十个十进制数,输出到另一个文件。批量场景下我习惯把转换函数改造成只写文件、不打印屏幕的版本:
void ConvertToFile(FILE *out, LinkStack *s, int num, int base) { do { PushLink(s, num % base); num = num / base; } while (num > 0); int digit; while (PopLink(s, &digit)) { if (digit < 10) { fprintf(out, "%d", digit); } else { fprintf(out, "%c", 'A' + digit - 10); } } fprintf(out, "\n"); }批量转换的主循环结构是:打开输入文件,逐行读入十进制数,每读一个就调用一次ConvertToFile,写完换行。注意每次调用前要重新初始化栈,不能复用上一次残留的节点。这个坑我踩过,第一次跑批量转换时输出里混着上一轮的数据,原因就是只清空了top指针但没有逐个free,旧节点残留在链表里。
4.3 数组栈和链表栈的实际表现对比
在一台普通配置的机器上做了一组简单测试:转换 10 万个随机十进制数到十六进制。
数组栈实现的总耗时大约在几十毫秒量级,内存占用固定在 64 个 int 的空间上。链表栈因为每个节点要 malloc 和 free,耗时比数组栈多出一个数量级,多数时间花在内存分配上。但链表栈的内存占用随数据动态变化,不会因为某个超大数导致栈满失败。
结论很直接:追求速度和代码简洁用数组栈,追求健壮性和可变长用链表栈。没有绝对优劣,取决于你的数据规模。课程设计一般选数组栈就够了,但如果题目明确要求“实现链式栈”,再上链表版本。
5. 数制转换的常见问题与排查:五个高频翻车点
5.1 转换结果完全反了
现象:123 转二进制输出 1101111,正确结果应该是 1111011。
原因:弹栈输出时没有配合栈的后进先出特性。有人直接遍历栈底到栈顶打印,或者用数组存储后从下标 0 开始打印,顺序自然反了。
解决:确认弹栈逻辑是从top开始取元素。数组栈里打印循环要从top往 0 走,不能从 0 往top走。如果你用的存储结构里底部在下标 0、顶部在下标top,打印顺序必须是从后往前。
5.2 十六进制输出变成数字而不是字母
现象:十进制 255 转十六进制输出 “FF”,但有人得到 “1515” 这种结果。
原因:对大于等于 10 的余数没有做字符映射,直接把 10、11、12 原样打印了。
解决:输出前判断digit是否小于 10。小于 10 直接数字输出,大于等于 10 用'A' + digit - 10映射。顺带一提,如果你想要小写字母输出,把'A'换成'a'就行,逻辑完全一样。
5.3 输入 0 时程序没有任何输出
现象:输入 0,程序运行结束但屏幕上什么都没有。
原因:循环条件用了while (num > 0),0 进不了循环体,余数没有压栈,栈空自然没输出。
解决:改用do...while结构,保证至少执行一次压栈。代码是:
do { Push(s, num % base); num = num / base; } while (num > 0);5.4 栈溢出导致程序崩溃
现象:转一个非常大的数时,程序直接报段错误或者输出错乱。
原因:数组栈的MAXSIZE设置偏小,压栈时没有检查栈满就继续写入,越界写坏了内存。
解决:压栈函数里保留栈满判断,返回ERROR后调用方要处理。如果确认数据量不大,MAXSIZE可以适当调大,但更推荐的做法是换链表栈,彻底规避容量问题。
5.5 负数转换结果让人看不懂
现象:输入 -123,程序输出一串很长的数字,或者直接崩溃。
原因:负数在 C 语言里按补码存储,对负数取模得到的是带符号的结果,直接压栈输出得到的是底层补码的解释,不是 -123 的二进制补码表示。
解决:一种做法是先取绝对值转换,再在前面加负号,适合低年级课程设计。另一种做法是正确处理补码,把目标进制当作无符号数处理,这里不展开。我的建议是:课程设计要求转负数的话,先取绝对值、输出负号、再转绝对值部分,这个方案最简单且不会出错。
6. 进阶技巧:位运算加速与逆向验证
数制转换有一种更取巧的写法,只针对 2 的幂次进制(2、4、8、16)。这种进制不需要短除法,直接用位运算把二进制位切分组即可。
拿 16 进制举例:每 4 个二进制位对应 1 个十六进制位。对一个整数,从低位开始每次取 4 位,就能得到从低位到高位的所有十六进制字符,然后用栈或先存数组再逆序输出都能完成转换。核心代码长这样:
void ConvertByBit(int num, int base) { int bits = 0; // 进制对应的位数 if (base == 2) bits = 1; else if (base == 8) bits = 3; else if (base == 16) bits = 4; char result[64]; int idx = 0; unsigned int mask = (1u << bits) - 1; // 掩码,比如 16 进制就是 0xF do { int digit = num & mask; // 取最低的 bits 位 result[idx++] = (digit < 10) ? ('0' + digit) : ('A' + digit - 10); num = num >> bits; // 无符号右移,注意 num 要做类型转换 } while (num > 0); // 逆序输出 for (int i = idx - 1; i >= 0; i--) { printf("%c", result[i]); } }位运算版本的掩码是关键:16 进制掩码是0xF,8 进制是0x7,2 进制是0x1。每次与掩码取与,得到的就是最低的几位,然后右移继续处理下一组。比短除法快一个量级,但适用范围只限 2 的幂次进制。这段代码要注意num必须按无符号右移处理,否则负数右移补的是符号位,结果就乱了。
写完转换代码,验证环节不能省。我的习惯是写一个逆转换函数,把转换结果再转回十进制,和原输入比对。以 16 进制转回十进制为例,每次读到一个字符,如果是在0到9之间就减去'0',如果是A到F就减去'A'加 10,然后乘以进制累加。用 20 组随机数跑一遍,输入输出一致才敢说这段代码真正写完了。
另一个实用习惯是打印中间状态。调试时在压栈弹栈之间加一行printf("push: %d\n", digit),确认每一个余数都按预期入栈。用完之后再删掉,避免刷屏。从那以后,我每次写这类栈应用代码都强制自己先打印一遍完整流程,确认压栈弹栈顺序无误,再做封装和优化。这份实例代码就是一个很好的起点,把两种栈实现、批量处理、边界校验都跑通一遍,数据结构课的栈部分就算真正吃透了,希望帮到你。
本文还有配套的精品资源,点击获取