news 2026/10/22 6:31:06

C语言数制转换实战:用数组模拟栈实现十进制到任意进制转换

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言数制转换实战:用数组模拟栈实现十进制到任意进制转换

简介:这是一份面向C语言初学者与数据结构学习者的代码实例PDF,重点演示如何借助顺序栈实现十进制到八进制等任意进制的转换。资源共1个PDF文件,压缩包约47KB,内容紧凑,便于直接阅读与对照练习。代码完整覆盖栈的初始化、压栈、出栈、判空等核心操作,并配有详细的转换流程说明,能帮助读者理解LIFO结构在逆序输出中的应用。已有894人学习下载。通过该实例,读者不仅可以掌握数制转换的实现思路,还能加深对顺序栈这一基础数据结构的实际运用能力,适合作为链表、栈等章节的课后补充材料。

1. 数制转换实例代码:从输入 82 到输出 1010010,栈的第一次实战

数制转换几乎是每个学 C 语言的人都会手写一遍的经典小题目,可大多数写法只是套公式,并没有真正用上数据结构。C语言数据结构中的数制转换实例代码,核心价值不在“会除会取余”,而在于用一个恰好长度的数组重现栈的压入与弹出过程:短除法算出一串余数,输出时却必须倒着来,这种“先生成、后倒序”的节奏,正是栈最典型的工作场景。读完并照着敲一遍这份代码,你会同时搞懂三件事:栈顶指针怎么动、压栈和出栈各发生在哪一步、为什么后进先出能解决进制换算里的顺序冲突。适合刚学到栈结构、准备课程设计或笔试算法题的人,也适合想从“会用递归”跨到“会用数据结构”的初学者参考。

2. 数制转换在 C 语言里的实现选型:为什么数组模拟栈是默认答案

2.1 短除法产生的余数顺序,天然对应后进先出

先说清楚数学过程。把十进制 82 转成二进制,短除法一路除下去:82 除以 2 商 41 余 0,41 除以 2 商 20 余 1,20 除以 2 商 10 余 0,10 除以 2 商 5 余 0,5 除以 2 商 2 余 1,2 除以 2 商 1 余 0,1 除以 2 商 0 余 1。余数生成顺序是 0、1、0、0、1、0、1,可 82 的二进制写出来却是 1010010,正好把余数序列倒过来。

这个“倒过来”不是一次性反转就能简单解释的,它背后有一个规律:最先算出来的是最低位,最后算出来的是最高位。如果边算边处理,就要求“晚到的最先被取走”,这是典型的后进先出。数据结构里只有栈满足这个语义,所以数制转换常被写进教材的栈章节当入门实例。你当然可以先把余数全部存进数组,再写个循环反向输出,效果一样,但这就绕过了对栈的理解——题目要训练的是数据结构思维,不是“用数组硬存”。

用文字描述算法流程是这样:输入非负整数 n 和目标进制 base,循环里反复做 n % base 得到余数并压栈,n /= base 缩小规模,直到 n 变成 0;随后只要栈不为空,就弹出栈顶元素,映射成字符拼接结果。这个流程里栈总共承担两件事:暂存中间结果,以及自动倒序。用 C 语言写出来,核心循环短到十行以内。

while (n > 0) { push(n % base); /* 余数进栈 */ n = n / base; /* 缩小规模 */ } while (!is_empty()) { putchar(digits[pop()]); /* 字符映射后出栈输出 */ }

逻辑说明:第一段循环负责生成余数并压栈,第二段循环负责把栈里的余数逐个弹出。余数本身是数值,比如 base 为 16 时余数可能是 10 到 15,不能直接打印,必须先通过 digits 字符数组映射成 A 到 F 再输出。参数说明里最需要注意的是n % base与n / base必须成对出现,漏掉/会导致死循环,漏掉%则栈里存的全是同一数值,这两个操作在 C 语言里对非负整数可以直接用,不需要额外修正。

2.2 三种常见实现方式对比:栈、反向数组、递归

同一个小题目在 C 语言里有三种主流写法:数组模拟栈、固定数组两遍写、递归直接输出。数组模拟栈是最符合教学目标的写法,定义一个数组和栈顶下标,压栈就是stack[++top] = value,出栈就是stack[top--],栈的每一步都看得见。固定数组两遍写是先算出总位数,再从数组尾部往前填字符,代码最少,但对“栈到底干了什么”的展示几乎为零。递归的做法最讨巧:先递归调用自身再打印当前位,天然倒序,不需要任何容器,但递归深度受系统调用栈限制,用在大数转换上有爆栈风险。

实现方式核心思路优点风险
数组模拟栈余数入数组,用 top 指针倒序弹出逻辑直白,完整呈现栈的三个核心机制需要额外维护 top 变量,易忘记初始化
固定数组两遍写先统计位数,再反向填充字符代码最短,不需要栈概念对理解数据结构帮助有限
递归转换函数先递归后打印当前位代码极简,天然倒序深度大时可能爆系统调用栈

选择数组模拟栈作为实例代码的默认方案,还有一个实际原因:它封装性好。你可以把栈定义成int stack[64]; int top = -1;这样的局部变量,也可以包成结构体,方便后续扩展成任意进制转换工具。递归和固定数组方案在演示之外都缺少这个扩展空间。熟手可能会说这题用递归三行就写完,但课设和笔试题要考察的恰恰是“你知不知道栈是用来倒序的”,所以递归方案只适合口头讨论,不适合当实例代码交出去。

2.3 栈容量设计的推导:几个字节够用,为什么定成 64

栈容量是最容易随手写错的地方。假设目标进制是 base,原始整数为 long 类型,转换结果的最大位数出现在 base 等于 2 时。在常见 64 位系统里 long 占 8 字节,正数最多 63 位,加上可能输出的负号以及字符串结尾的'\0',64 个数组元素已经足够覆盖最坏情况。如果只针对 int 来写,32 位正数最多 31 个二进制位,栈开 32 个也行,但代码换个平台或换成长整型输入就可能在栈顶越界。

#define MAX_STACK 64

逻辑说明:MAX_STACK 是栈数组的容量,同时也是输出缓冲区的容量,因为压栈的余数和最终输出的字符一一对应。参数说明:栈顶从 -1 开始,表示空栈;压栈时先++top再写数据,保证 top 永远指向最后一个有效元素;循环条件用top >= 0,能精确控制弹出次数。容量宁可多留也绝不要“刚刚好”,尤其后面要扩展成通用转换工具时,一个到处传 MAX_STACK 的硬编码会让所有调用方都跟着紧张。

3. 可抄作业的 dec2base 完整代码:数组栈存余数、主程序驱动、编译验证

3.1 dec2base 函数本体:压栈、出栈、字符映射一次写完

下面这份代码是可以直接保存编译的完整版本。函数设计成int dec2base(long n, int base, char out[]),输入一个十进制整数 n、目标进制 base、输出缓冲区 out,返回 0 表示成功,返回 -1 表示进制参数非法。代码刻意把栈定义成局部数组,这样一次函数调用结束,栈空间自动释放,不需要额外写销毁逻辑。

#include <stdio.h> #define MAX_STACK 64 /* 十进制非负整数转任意 2~36 进制 */ int dec2base(long n, int base, char out[]) { int stack[MAX_STACK]; /* 用数组模拟栈 */ int top = -1; /* 栈顶指针,-1 表示空栈 */ int i = 0; const char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; if (base < 2 || base > 36) { return -1; /* 非法进制直接失败 */ } if (n == 0) { out[i++] = '0'; /* 0 不做任何除法,单独处理 */ out[i] = '\0'; return 0; } while (n > 0) { stack[++top] = n % base; /* 余数入栈 */ n /= base; /* 缩小规模 */ } while (top >= 0) { out[i++] = digits[stack[top--]]; /* 出栈并通过映射表转字符 */ } out[i] = '\0'; return 0; }

逻辑说明:函数前半部分做参数校验和零值保护,后半部分是两个对称的 while 循环,第一个循环负责生成余数并压栈,第二个循环负责弹出并写入输出缓冲区。digits 数组是关键,它把余数数值 0 到 35 映射成 0 到 9、A 到 Z,省去了一段 if-else 判断。参数说明:base 的有效范围是 2 到 36,超过 36 时 digits 数组下标越界,所以必须在入口拦截;输出缓冲区 out 的长度至少是 MAX_STACK,这个容量对应 long 类型二进制的最大位数加上结尾符,前面已经推导过。

3.2 主程序驱动:输入、调用、打印完整流程

有了转换函数,主程序只需要负责三件事:读入十进制数和目标进制,调用 dec2base,打印结果。这里建议用scanf("%ld")读整数而不是scanf("%d"),因为转换函数接受 long 类型,输入范围更大;用%d时如果用户输入超过 int 范围,值会先被截断再进入函数,转换出的结果是错的。

int main(void) { long n; int base; char buf[MAX_STACK]; /* 输出缓冲区,容量和栈一致 */ printf("请输入十进制整数 n: "); scanf("%ld", &n); printf("请输入目标进制 base (2~36): "); scanf("%d", &base); if (dec2base(n, base, buf) == 0) { printf("转换结果: %s\n", buf); } else { printf("进制参数不合法\n"); } return 0; }

逻辑说明:buf 长度与栈数组保持一致,避免函数内部写越界。函数返回值做了分支,只有成功时才打印结果,失败时给出提示。参数说明:目前这个版本只对非负整数有效,输入负数时while (n > 0)循环一次都不执行,结果会输出空字符串,这是刻意留给下一章的坑。建议把scanf的返回值也检查一下,生产环境里用户可能输入非数字字符,导致 n 和 base 处于未初始化状态。

3.3 编译与运行验证:用 82 和 255 两个用例检查输出

把以上两段代码合并保存为convert.c,用 gcc 编译时建议把告警全开,捕获隐藏的类型问题:

gcc -std=c11 -Wall -Wextra -o convert convert.c ./convert

参数说明:-std=c11指定使用 C11 标准,代码里没有任何依赖新标准特性的写法,换成-std=c99也一样能过;-Wall -Wextra会在编译期提示未使用变量、类型转换隐患等问题,是排查小错误的第一道防线。运行后依次输入 82 和 2,预期输出1010010;再输入 255 和 16,预期输出FF。如果看到输出顺序颠倒或数字 10 出现在十六进制结果里,对照第 5 章的内容检查余数方向与字符映射两步。

4. 把实例扩展成通用进制转换利器:负数处理、2~36 进制互转与边界参数

4.1 负数与 LONG_MIN 的边界:用 unsigned long 绕开取负溢出

第 3 章的 dec2base 只处理非负整数,这是多数教材例子的默认前提。实际使用中负数躲不掉,而负数转换只有一个小技巧:先记住符号,对绝对值做转换,最后在结果前面补一个-。听起来简单,可LONG_MIN是个例外——它取负后仍然超出 long 能表示的范围,直接n = -n属于未定义行为。常规的做法是在检测到负数后先转成无符号类型再取负。

int dec2base_v2(long n, int base, char out[]) { int stack[MAX_STACK]; int top = -1; int i = 0; int negative = 0; unsigned long u; const char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; if (base < 2 || base > 36) { return -1; } if (n < 0) { negative = 1; u = -(unsigned long)n; /* 先转无符号再取负,避开 LONG_MIN 溢出 */ } else { u = (unsigned long)n; } if (u == 0) { out[i++] = '0'; out[i] = '\0'; return 0; } while (u > 0) { stack[++top] = u % (unsigned long)base; u /= (unsigned long)base; } if (negative) { out[i++] = '-'; /* 负号加在结果最前面 */ } while (top >= 0) { out[i++] = digits[stack[top--]]; } out[i] = '\0'; return 0; }

逻辑说明:负数分支先设置 negative 标记,再用-(unsigned long)n把 n 的绝对值变成无符号数。以 LONG_MIN 为例,先转成 unsigned long 得到 2 的 63 次方,取负后仍是 2 的 63 次方,恰好是 LONG_MIN 的绝对值,全程没有有符号溢出。参数说明:循环里的 n 换成了 u,余数计算也随之强转成 unsigned long,避免无符号与有符号混用带来的隐式转换告警。输出负号的位置必须在字符序列之前而不是之后,这一点最容易写反。

4.2 反向转换:从任意进制字符串还原十进制整数

进制转换一体两面,既然能把十进制转出去,就得能把字符串转回来。C 标准库的 strtol 自带这个功能,但实例代码的价值在于看清内部机制。手写一个to_decimal函数,逐字符解析并累积结果,同时做合法性校验。

#include <ctype.h> #include <limits.h> long to_decimal(const char *s, int base) { long result = 0; int i = 0; int sign = 1; while (s[i] == ' ' || s[i] == '\t') i++; /* 跳过空白 */ if (s[i] == '-') { sign = -1; i++; } else if (s[i] == '+') i++; while (s[i] != '\0') { int digit; char c = s[i]; if (c >= '0' && c <= '9') digit = c - '0'; else if (c >= 'A' && c <= 'Z') digit = c - 'A' + 10; else if (c >= 'a' && c <= 'z') digit = c - 'a' + 10; else return -1; /* 非法字符 */ if (digit >= base) return -1; /* 该位超出进制允许范围 */ if (result > (LONG_MAX - digit) / base) return -1; /* 防溢出 */ result = result * base + digit; i++; } return sign * result; }

逻辑说明:每个字符先转成数值,大小写字母都接受,然后检查digit >= base,例如 base 为 2 时字符串里出现'2'必须判错。溢出检查放在乘法之前,通过比较(LONG_MAX - digit) / base提前发现累加是否越界。参数说明:这个函数用返回 -1 表示非法输入,但 -1 也可能是一个合法的负数转换结果,区分两者需要额外增加错误码参数,当前版本适合教学场景,做工程时建议改成int to_decimal(const char *s, int base, long *out)的接口形式。

4.3 边界参数速查:base 范围、缓冲区长度、零值和符号处理

把散落在代码里的边界条件汇总成一张表,开发时对照检查能少踩一半坑。这张表也适合直接写进代码注释,方便后来人维护。

边界项建议值 / 处理方式原因
base 范围2 ~ 36,入口校验非法值字符映射表只有 36 个可用字符
缓冲区长度MAX_STACK 64long 型二进制最多 63 位 + 符号 + 结束符
输入 0单独判断并输出'0'while (u > 0)循环天然跳过 0
负数 LOMG_MIN用-(unsigned long)n取绝对值直接对 long 取负是未定义行为
反向转换溢出累乘前用(LONG_MAX - digit) / base预判防止转换结果超出 long 范围

逻辑说明:表中每一项都能在上面的代码段里找到对应实现,这不是理论约定,而是每个函数被真实输入逼出来的硬边界。参数说明:MAX_STACK之所以统一用在栈和输出缓冲区两处,是为了让容量决策只出现在一个宏定义里,后续想改成支持 long long 输入,只需同步调整宏和函数签名,不需要翻遍全文件改数字。

5. 避坑与排查:数制转换代码最常见的四个翻车点

5.1 余数方向反了:输出结果整体颠倒却找不到原因

现象:输入 82 转二进制,结果打印出来是 0100101 而不是 1010010,或者结果里每一位都有但顺序完全反了。排查时往往先怀疑运算符优先级,翻了半天才发现根本问题在输出顺序。

原因:余数是短除法逐次生成的,先算出来的是最低位。如果把余数边算边正向写入输出缓冲区,得到的就是低位在前、高位在后的逆序结果。实例代码里用栈正是为了自动倒序,但如果压栈后没有完整弹出,或弹出一半就结束,同样会出现部分正确部分颠倒的奇怪输出。

解决:确认压栈循环完整执行到 n 为 0,出栈循环执行到 top 小于 0,二者缺一不可。最简单的自查方法是把栈里的元素从头到尾打印一遍,对比数学手算的余数序列是否一致。

5.2 十六进制的 10 到 15 输出成数字:结果错位成 1515

现象:255 转十六进制,预期 FF,实际输出 1515;175 转十六进制,预期 AF,实际输出 A15。

原因:余数 10 到 15 在 C 语言里是 int 类型的数值,直接打印会变成两个字符'1''0'。没有字符映射表的话,每一位占用的字符数不固定,结果整体长度变长,看起来就像错位。更隐蔽的是,10 被打印成两个字符会让后续所有数字都错位,肉眼极难排查。

解决:使用统一的字符映射表,字符串"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"按数组下标即余数值,一次取字符输出,保证任意进制下每一位都恰好对应一个字符。不要在打印时写if (r >= 10) printf("%c", 'A' + r - 10);这类分支,分支一多就容易漏边界。

5.3 输入 0 时输出空串:循环条件把特殊值吞了

现象:输入 0 和 10,程序打印空行或只显示转换结果:,没有任何数字。初学者经常怀疑是 printf 格式问题,实际是转换函数根本没生成字符。

原因:短除法的循环条件是while (n > 0),输入 0 时一次循环都不进入,栈为空,出栈循环自然也没有内容可写。数学上 0 在任何进制下都是 0,不需要除法运算,但代码没有覆盖这个特殊情况。

解决:在循环之前单独判断if (n == 0),直接向输出缓冲区写入字符'0'并返回。如果用do-while写循环也能让 0 至少执行一次取余运算,但单独判断的写法语义更清楚,也更方便后续添加负数支持。

5.4 栈容量“刚好够”:数据源从 int 换成 long 立刻翻车

现象:代码在 32 位输入下一切正常,开发者顺手把函数参数从 int 换成了 long long,结果转换大数时输出乱码或程序直接崩溃。多数人第一反应是算法写错了,实际是栈数组开小了。

原因:教材里常见的示例把栈定义成char stack[32],这个容量对应 32 位 int 二进制的最大位数。参数类型扩大后,二进制位数会超过 32,压栈先越界写坏相邻内存,出栈再读回垃圾数据。

解决:数组长度和输入类型绑定,用宏统一管理,不要分散写数字。按前文推导,long 类型配 64 个元素安全,long long 同样 64 个元素足够,因为 64 位整数的二进制位最多 64 个。给栈元素选类型时还要注意:char 能存下 36 进制下的最大余数 35,但为了和数组下标、循环变量保持一致,直接用 int 省去隐式转换的麻烦。

6. 进阶收尾:把数制转换封装成可复用小工具,用已知对应表做自检

6.1 把栈抽成结构体接口:从一段代码变成一块积木

数组模拟栈写完之后,下一步是把它从局部变量升级成结构体。定义Stack结构体和push、pop、is_empty三个操作函数,转换逻辑就可以不再关心 top 指针的细节,只调用栈接口。这也是很多课程设计从“写一个函数”进化到“写一个小模块”的转折点。

typedef struct { int data[MAX_STACK]; int top; } Stack; void push(Stack *s, int v) { s->data[++s->top] = v; } int pop(Stack *s) { return s->data[s->top--]; } int is_empty(const Stack *s) { return s->top < 0; }

逻辑说明:push 先移动 top 再写数据,pop 先取数据再移动 top,两处索引操作互为镜像。参数说明:所有接口都要求传入 Stack 指针,避免结构体整体拷贝;is_empty 用top < 0判断比top == -1更通用,即使初始化值不同也无需修改接口逻辑。封装之后,dec2base 里的两个循环分别改写为push(&s, n % base)和out[i++] = digits[pop(&s)],语义更接近教材里的抽象栈。

6.2 用断言做回归自检:替代肉眼核对的手工测试

每次改完代码重新运行一遍手工输入很费时间,更可靠的做法是把已知对应表写进测试函数,用 assert 自动比对。这个习惯能拦住绝大多数扩展代码时引入的回归问题。

#include <assert.h> #include <string.h> void self_test(void) { char buf[MAX_STACK]; assert(dec2base_v2(82, 2, buf) == 0 && strcmp(buf, "1010010") == 0); assert(dec2base_v2(255, 16, buf) == 0 && strcmp(buf, "FF") == 0); assert(dec2base_v2(0, 10, buf) == 0 && strcmp(buf, "0") == 0); assert(dec2base_v2(-128, 2, buf) == 0 && strcmp(buf, "-10000000") == 0); }

逻辑说明:四个断言分别覆盖普通二进制、十六进制大写映射、零值、负数四类典型场景。参数说明:strcmp 返回值需要加== 0判断,直接把 strcmp 放进 assert 会导致字符串相等时断言失败。以后每次改动栈容量或字符映射逻辑,只要运行一次带 self_test 的程序就能立刻发现行为变化,比对着屏幕猜强得多。

这套代码迭代到这一步,我已经反复踩过好几遍同样的坑:最早把栈容量写成 32,换 long 输入后越界崩溃;后来加负数支持时对 LONG_MIN 直接取负,运行结果随机出错。现在每次写这类工具,我都会在一开始就把边界参数表列出来,再把自检函数挂上,改完跑一遍就收工。数制转换题目虽小,但栈的压入弹出、字符映射、边界处理一个不少,你顺着这条路把它吃透,后面再碰表达式求值、括号匹配、逆波兰计算器都会顺畅很多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 9:49:15

Zabbix Agent安装配置实战:从模式选择到批量部署

1. 项目逻辑与前期规划 1.1 标题里的“装置配置”到底说的是什么 先纠正一个小笔误&#xff1a;标题里写的“装置配置”&#xff0c;结合项目场景来看&#xff0c;指的其实是“安装配置”。Zabbix Agent 在 Linux 平台上的安装配合&#xff0c;是所有基于 Zabbix 做监控的同学…

作者头像 李华
网站建设 2026/10/22 18:13:10

Header-Editor插件:前端调试HTTP请求头的轻量级实践指南

简介&#xff1a;Header-Editor-4.1.1是一款面向Web开发人员、测试工程师及网络安全学习者的轻量级HTTP请求头编辑工具插件&#xff0c;专为调试接口、模拟请求场景、绕过前端校验或定制化网络行为设计。资源包共4个文件&#xff0c;含2个Chrome扩展&#xff08;.crx&#xff0…

作者头像 李华
网站建设 2026/10/21 15:52:09

插件系统开发实战:从plugin.json设计到加载失败排查全链路

1. 从"plugins"这个标题说起&#xff1a;插件系统到底在解决什么问题"plugins"这个词看起来简单到几乎没什么可写的&#xff0c;但恰恰是这种极简标题背后藏着最复杂的一类工程问题。我做了十多年开发&#xff0c;接触过各种形态的插件体系——从编辑器扩展…

作者头像 李华
网站建设 2026/10/19 12:03:08

Spring含大厂高频面试题(详细+图片讲解)

1、Spring框架中的单例bean是线程安全的吗&#xff1f;候选人&#xff1a;不是线程安全的。当多用户同时请求一个服务时&#xff0c;容器会给每个请求分配一个线程&#xff0c;这些线程会并发执行业务逻辑。如果处理逻辑中包含对单例状态的修改&#xff0c;比如修改单例的成员属…

作者头像 李华