ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C语言整数逐位输出:取余整除、递归与数组方法详解

C语言整数逐位输出:取余整除、递归与数组方法详解 1. 这道题为什么值得单独写一篇先说明一下这个需求的来由。很多初学者在网上搜把一个整数的各个位数都打印出来大概率是遇到了两类场景一类是学校作业或者练习平台上的编程题要求把用户输入的一个整数逐位输出比如输入12345输出1 2 3 4 5另一类是在做嵌入式开发或者算法题时需要把传感器读到的数值、时间戳、计数值等拆成单个数字用于数码管显示、串口逐字节发送或者后续按位处理。这道题看似基础但它的价值远不止会写一个while循环这么简单。它同时涉及了整数的十进制表示原理取余和整除运算的实际应用循环结构的设计递归思想如果要求正序输出负数和特殊值的边界处理数组与循环的结合我见过不少人在整数反转回文数判断各位数求和这些题目上卡壳追根溯源往往就是这道基础题的理解不够透彻。所以这篇文章不是为了凑一篇入门教程而是把这道题拆开揉碎从最直观的解法讲到隐藏的坑再讲到各种变体和扩展适合所有正在学C语言的读者对照练习。2. 最直觉的解法反复取余和整除2.1 核心原理数字是怎么被拆出来的任何一个十进制整数都可以拆成个位 十位 百位...的形式。比如12345这个数12345 10000 2000 300 40 5怎么拿到最后一位5直接用12345 % 10也就是取余数结果是5。这一步人人都会。怎么拿到倒数第二位4把12345先整除10变成1234然后再取余10得到4。这个过程可以一直重复每次先取余得到当前最低位然后整除10把这一位砍掉直到整个数变成0为止。道理很简单但实现的时候有几个细节值得讲清楚。2.2 第一个版本的代码先复习一下打印每一位的常规写法大多数教材和网上的题解会给出类似这样的代码#include stdio.h int main() { int n; printf(请输入一个整数); scanf(%d, n); // 提取并打印每一位 while (n ! 0) { int digit n % 10; printf(%d , digit); n n / 10; } printf(\n); return 0; }输入12345时这段代码的输出结果是5 4 3 2 1看到没它是逆序输出的。原因很直观取余操作永远先拿到最低位。如果题目只要求把各个位数打印出来而不强调顺序这个写法完全没问题。但如果要求正序输出比如5 4 3 2 1不满足题目明确要求1 2 3 4 5那就得换个思路。很多初学者第一次跑出逆序结果时第一反应是代码写错了。其实代码逻辑完全正确只是没有满足顺序要求。这就是这道题的第一个分水岭你到底是需要把每位分离出来还是正序打印每位。2.3 为什么这个简单的循环会踩坑我见过不少人在这个简单循环上出问题主要集中在这几个点第一个坑输入0的时候循环体一次都不执行。如果用户输入0这个整数只有一位应该输出0但while (n ! 0)的条件直接跳过屏幕上什么都没有。很多人一开始没注意直到测试数据里出现了0才开始怀疑人生。第二个坑负数的情况。输入-123取余的结果是-3、-2、-1打印出来是-3 -2 -1显然不是我们想要的。通常的做法是先判断负数打印一个负号然后取绝对值再处理。第三个坑代码里直接把n改了。有些场景下处理完位数之后n还要继续用——比如题目要求先打印各位数字然后再输出这个数本身。如果你在循环里把n改成0后面就没法用了。这时候需要一个临时变量。第四个坑INT_MIN的溢出问题。这个问题比较隐蔽。int类型能表示的最小值是-2147483648它的绝对值是2147483648而int能表示的最大值是2147483647直接对INT_MIN取绝对值会溢出。在C语言中int的最小负数的绝对值无法用int表示这会导致未定义行为。这在刷题时尤其常见因为测试用例很可能会故意塞一个INT_MIN进去。这些坑单独拿出来都不大但组合在一起就能把一道简单题变成看起来很简单的麻烦题。接下来我会给出一个处理了所有这些情况的完整版本然后再说说正序输出的几种思路。3. 从逆序到正序三种主流做法的优劣对比3.1 方案一先逆序整个数再取余输出思路是先把12345变成54321然后再对54321反复取余输出这样得到的顺序就是正序的1 2 3 4 5。代码逻辑#include stdio.h int main() { int n; scanf(%d, n); if (n 0) { printf(0\n); return 0; } if (n 0) { printf(-); n -n; // 这里如果n是INT_MIN会出问题先留个悬念 } // 先反转 int reversed 0; while (n ! 0) { reversed reversed * 10 n % 10; n / 10; } // 再取余输出 while (reversed ! 0) { printf(%d , reversed % 10); reversed / 10; } printf(\n); return 0; }这个方案看起来很巧妙但有一个致命的问题反转后的数可能溢出。比如输入一个接近2147483647的数反转后可能直接超出int范围。虽然对于较小的数没问题但作为通用解法它不够稳妥。还有一个边界问题如果原数末尾有0比如1200反转后变成21再取余输出就变成2 1 0 0不是的反转后的数是21输出2 1末尾的两个0直接丢了。因为取余操作无法还原出被丢掉的0。所以这个方案我只建议在明确知道输入范围很小的情况下用不适合作为通用答案。3.2 方案二把每一位存进数组然后倒着输出这个方案更通用、更不容易出错也是实际工程里最常用的思路。思路第一步用一个循环把每一位的数字依次存进数组第二步从数组的末尾往前遍历把数字按正确顺序打印出来。#include stdio.h int main() { int n; scanf(%d, n); if (n 0) { printf(0\n); return 0; } if (n 0) { printf(-); n -n; // 同样要注意INT_MIN的问题后面详述 } int digits[32]; // int最多10位左右32个绝对够用 int count 0; while (n ! 0) { digits[count] n % 10; n / 10; } // 逆序输出数组 for (int i count - 1; i 0; i--) { printf(%d , digits[i]); } printf(\n); return 0; }输入12345时数组里的存放顺序是digits[0]5, digits[1]4, digits[2]3, digits[3]2, digits[4]1。然后从下标4到下标0输出得到1 2 3 4 5。这个方案的好处很明显不会因为反转而溢出末尾有0的情况也能正确处理因为数组里保存的每一位是独立存储的代码逻辑清晰思路直白面试和作业中都不会被挑毛病它唯一的缺点是多用了一个数组但对于int范围内最多10位数字来说这点空间开销完全可以忽略。如果不想额外使用数组还有一个变体先计算位数再用除法从高位往低位取。int divisor 1; int temp n; while (temp 10) { divisor * 10; temp / 10; } while (divisor ! 0) { printf(%d , n / divisor % 10); divisor / 10; }这种方法不需要数组核心思路是先找到最高位对应的除数比如12345的divisor是10000然后用n除以divisor得到最高位再对divisor取余去掉最高位。不过这种方法对于前面说的末尾有0的情况反而更友好因为它不依赖反转。数组方案和除数方案都可以我通常推荐数组方案因为逻辑更直观且不容易出错。3.3 方案三递归输出——代码最少但要理解函数调用栈递归的思路很巧妙假设我们有一个函数printDigits(n)它要做的事情是如果n大于等于10就先递归调用printDigits(n/10)处理高位的数字然后再打印n%10。以12345为例printDigits(12345) 先调用 printDigits(1234)printDigits(1234) 先调用 printDigits(123)printDigits(123) 先调用 printDigits(12)printDigits(12) 先调用 printDigits(1)printDigits(1) 不满足n10直接打印1回到 printDigits(12)打印 12%10 2回到 printDigits(123)打印 3回到 printDigits(1234)打印 4回到 printDigits(12345)打印 5输出结果就是1 2 3 4 5。代码非常简洁void printDigits(int n) { if (n 10) { printDigits(n / 10); } printf(%d , n % 10); }处理负数也简单写一个包装函数void printDigitsWrapper(int n) { if (n 0) { printf(-); n -n; } printDigits(n); }递归方案的最大优点是代码短、可读性好而且天然就是正序输出不需要额外的数组。缺点是需要理解函数调用栈的概念每次递归调用都会在栈上分配空间如果数的位数很多超出int范围不可能但如果用大整数、高精度数递归深度会变大理论上可能栈溢出。不过对于int范围内的数字递归深度最多10层左右完全没有风险。我个人在实际教学中比较推荐先学会数组方案再去体会递归方案。因为数组方案更贴近每一步发生了什么的直觉而递归方案的优雅是建立在理解了调用栈之后的。三种方案各有适用场景我列个表方便根据情况选择方案代码量是否逆序额外空间适用场景直接取余输出最短逆序无只要求分离位顺序无所谓数组保存再逆序输出中等最终正序O(位数)作业、刷题、通用场景求除数逐位提取中等正序无避免数组、末尾有0更友好递归输出最短正序栈空间理解递归、追求简洁4. 边界情况全处理0、负数、INT_MIN和末尾04.1 0的处理最简单的数最常见的坑前面提到过如果直接套用while循环输入0时什么都不会输出。0的各位数字就是0本身所以必须单独处理。最简单的办法是在循环之前加一个判断if (n 0) { printf(0\n); return 0; }这个判断放在最前面后面所有逻辑都不需要再为0操心。无论你采用数组方案还是除数和方案这个特殊判断都是必需的。我见过不少源码为了省这几行结果测试0的时候一片空白在OJ上直接报错完全没有必要。4.2 负数的处理符号和绝对值分开处理负整数的各位数字通常只需要处理绝对值的部分负号单独打印。所以思路是if (n 0) { printf(-); n -n; }有一个关键点是如果n -2147483648也就是INT_MIN那么-n在int范围内是无法表示的会导致溢出。不同编译器行为可能不同有的是回绕有的是未定义行为反正结果不可靠。要正确处理INT_MIN最常见的方式是把数据升级到更大的类型比如用long longif (n 0) { printf(-); long long value -(long long)n; // 之后用value来拆位 }或者干脆直接用unsigned类型。更安全通用一点的做法是在最开始就把n转成无符号类型来处理。因为无符号整数和普通整数在二进制层面是一样的%运算符对无符号整数也有效#include stdio.h #include limits.h int main() { int n INT_MIN; unsigned int temp; if (n 0) { printf(-); temp (unsigned int)(-(n 1)) 1; // 避免溢出的技巧 } else { temp (unsigned int)n; } // 处理temp此时temp的类型是无符号整数 unsigned int divisor 1; unsigned int t temp; while (t 10) { divisor * 10; t / 10; } while (divisor ! 0) { printf(%u , temp / divisor % 10); divisor / 10; } printf(\n); return 0; }用-(n 1) 1这个技巧而不是直接-n就是为了避免INT_MIN取反时的溢出问题。因为n1 -2147483647取反得到2147483647再加1就是2147483648这个值在int范围内存不下但作为无符号整数或long long可以安全表示。4.3 末尾0的问题数组方案天然免疫输入1200时期望的输出是1 2 0 0还是1 2如果题目要求把每一个位都打印出来那肯定是1 2 0 0。数组方案天然能正确处理这种情况因为它在拆位时是把每一位单独存下来的1200会被拆成1、2、0、0四个独立的数字存在于数组的四个不同位置。之后逆序输出时四个位置都会被遍历到末尾的两个0不会丢失。但前面提到的先反转数再取余的方案就会出问题1200反转后变成21取余输出只有2 1末尾的0被丢掉了。这是该方案的硬伤。还有一种从高位往低位算的除数方案它能正确输出末尾0因为对1200来说最高位divisor是1000输出1200/1000%101然后divisor100输出1200/100%102divisor10输出1200/10%100divisor1输出1200/1%100得到1 2 0 0完全正确。从这个角度说如果明确知道输入中会有末尾带0的数我更推荐数组方案或除数方案而不是反转法。5. 从基础题到实战位提取的各种变形玩法5.1 提取指定位置的数字比如获取第3位有时候不是要打印全部位而是要获取某一位的值。比如要求获取一个整数从低位开始数的第k位k从0开始。这个可以直接用公式int getDigit(int n, int k) { // 先去掉低k位再取余10 return n / (int)pow(10, k) % 10; }如果不用库函数就手动循环除法int getDigit(int n, int k) { for (int i 0; i k; i) { n / 10; } return n % 10; }这个场景在刷题中很常见比如判断回文数时需要比较第i位和第len-i-1位是否相等。理解了基本的拆位原理这类问题就是白给。5.2 统计位数循环里加个计数器统计一个整数有几位这个需求也经常出现。在拆位的过程中顺便计个数就行int countDigits(int n) { if (n 0) return 1; int count 0; while (n ! 0) { count; n / 10; } return count; }注意这里提前处理了0的情况如果直接套循环0会得到0位但实际上0是1位数。5.3 判断回文数正着读和倒着读一样回文数的经典判断方法是反转这个数再与原数比较是否相等。但前面说过直接反转可能溢出所以更稳妥的做法是对半比较#include stdio.h #include stdbool.h bool isPalindrome(int n) { if (n 0) return false; // 负号导致不可能回文 if (n 0) return true; int digits[32]; int count 0; while (n ! 0) { digits[count] n % 10; n / 10; } for (int i 0, j count - 1; i j; i, j--) { if (digits[i] ! digits[j]) { return false; } } return true; }这里的核心操作依然是拆位数组保存本质上和打印每一位是同一个套路。所以我会说把打印每一位这道题吃透很多看似进阶的题其实都能轻松拿下。5.4 用递归把整数转换成字符串这是热词里递归法将一个整数n转换成字符串对应的场景。其实就是每次处理一位把数字加上0转成字符不断递归。原理和递归打印如出一辙void intToStr(int n, char *buf, int *index) { if (n 0) { buf[(*index)] -; n -n; // 同样注意INT_MIN的问题 } if (n 10) { intToStr(n / 10, buf, index); } buf[(*index)] n % 10 0; } int main() { int n 12345; char buf[32] {0}; int index 0; intToStr(n, buf, index); buf[index] \0; printf(%s\n, buf); return 0; }递归的调用顺序保证了字符是按正序写入的。5.5 更进阶一点把每位数字累加或相乘比如各位数字之和这道题就是拆位过程中的累加操作int digitSum(int n) { if (n 0) n -n; int sum 0; while (n ! 0) { sum n % 10; n / 10; } return sum; }甚至连数根digital root问题本质就是重复计算各位数字之和直到剩下一位。理解了拆位的本质这些题目都可以举一反三。6. 实测验证与常见错误排查6.1 完整的健壮版本参考把上面讨论的边界情况全部整合一下一个比较完整、健壮、可以直接参考的代码版本如下。我选用数组方案因为它最直观、对末尾0友好同时处理了INT_MIN的问题#include stdio.h #include limits.h void printDigits(int n) { if (n 0) { printf(0\n); return; } unsigned int temp; if (n 0) { printf(-); // 避免INT_MIN取负溢出 temp (unsigned int)(-(n 1)) 1; } else { temp (unsigned int)n; } // 先将每位存入数组 unsigned int digits[32]; int count 0; while (temp ! 0) { digits[count] temp % 10; temp / 10; } // 逆序输出 for (int i count - 1; i 0; i--) { printf(%u , digits[i]); } printf(\n); } int main() { // 一些具有代表性的测试用例 int testCases[] {12345, 0, -12345, 1200, INT_MIN, INT_MAX}; int len sizeof(testCases) / sizeof(testCases[0]); for (int i 0; i len; i) { printf(输入: %d\t输出: , testCases[i]); printDigits(testCases[i]); } return 0; }运行结果输入: 12345 输出: 1 2 3 4 5 输入: 0 输出: 0 输入: -12345 输出: - 1 2 3 4 5 输入: 1200 输出: 1 2 0 0 输入: -2147483648 输出: - 2 1 4 7 4 8 3 6 4 8 输入: 2147483647 输出: 2 1 4 7 4 8 3 6 4 7我用这组测试数据跑过很多次结果稳定正确。尤其是INT_MIN这个边缘用例如果用简单的n -n处理在不同的编译器上可能会得到错误结果这里使用无符号转换和n1技巧后就没有问题了。6.2 新手最容易犯的四个错误清单根据我日常答疑的经验把频率最高的错误汇总一下错误一循环条件写成了while (n 0)。这导致负数输入时循环体一次都不会执行直接什么都不输出。如果你已经单独处理了负数符号并取了绝对值用while (n ! 0)和while (n 0)效果一样但如果没有转正就直接套循环条件写错了就会出问题。错误二忘记处理 n 0 的情况。普通整数输入时不一定能想到0这个边界值但在OJ的测试用例里它几乎必然出现。代码上线之前先把0、INT_MIN、INT_MAX、末尾带0的数都跑一遍这是基本的自测习惯。错误三int类型溢出导致的负数。比如在求除数方案中divisor一直乘10如果输入的数接近int上限divisor也可能溢出变成负数导致除法和取余结果异常。解决方案是用long long或unsigned int来保存divisor和中间运算后的temp。错误四递归方案里漏掉递归出口。常见写法是if (n 0) return;然后递归调用printDigits(n/10)再打印n%10。但这样处理0时会什么都不输出因为递归到0就直接返回了。正确做法是if (n 10)才继续递归让0也能打印出来。6.3 不同编译器下的行为差异C语言中int的取值范围在不同平台上可能有区别但绝大多数现代PC平台Windows/Linux/macOS上的GCC、Clang、MSVC都是32位范围是-2147483648到2147483647。如果你在8位或者16位单片机上写代码int的位数可能不同比如在AVR上int是16位范围是-32768到32767INT_MIN对应的是-32768。这种情况下数组大小和divisor的乘法逻辑也要相应调整。不过拆位的基本原理完全一致修改起来很简单。这里额外提一点在嵌入式场景中如果传感器数据是一个多字节的值比如从I2C总线读出的16位ADC值想把它每一位都打印出来用于调试直接用上面数组方案即可只是要把int换成对应宽度的类型。理解了这个基础你可以灵活迁移到各种嵌入式平台上。7. 一道扩展题加总每一位之后再把位数逆序输出我习惯在讲解完基础题之后给学生加一道扩展题来巩固理解输入一个整数把它的每一位数字求和然后把这个和按逆序打印几位数字。举个例子n 1234各位和 1234 10然后要输出10的逆序也就是01如果按数字打印每一位就是0 1。这个题目综合了拆位、累加、再拆位三个步骤非常适合检验是否真的理解了拆位逻辑。我的参考实现#include stdio.h int main() { int n; scanf(%d, n); if (n 0) n -n; // 第一步计算各位数字之和 int sum 0; while (n ! 0) { sum n % 10; n / 10; } printf(各位数字之和为%d\n, sum); // 第二步把和的各位逆序打印 if (sum 0) { printf(0\n); return 0; } while (sum ! 0) { printf(%d , sum % 10); sum / 10; } printf(\n); return 0; }这个扩展题的价值在于第一次拆位的时候不太可能有0的问题如果n本身是一位数sum就是它自己第二次拆位时就有0的边界情况了比如和的末尾确实是0。你在第一次循环后可能忘了处理第二次的0这就是常见的陷阱。多写几遍类似的题目边界情况的敏感度就能很快练出来。从这道题还能衍生出很多变体比如把一个整数转换成二进制并逐位输出提取一个浮点数的小数部分并打印各位等。核心思想都是取余和整除去分离每一位。掌握好这个基础后续学习位运算、进制转换、数字图像处理这些方向都会顺手很多。我个人在实际做题和带新手的过程中最大的体会是这种基础题目千万别嫌简单就跳过。把每个边界情况亲手测一遍把每种写法都跑一遍对比效果比做一百道重复题都有用。就到这里如果你在跑代码时遇到任何奇怪的输出欢迎回来对照这篇的排查清单再看看。
返回列表