C语言大整数加法详解:高精度运算入门与OJ实战 1. 题目拆解先搞清楚它在考什么1.1 字母OJ与入门关先交代一句背景。标题里的zzulioj是某高校在线评测系统Online Judge的缩写很多学校的程序设计课程、实验作业、蓝桥杯热身都跑在这类OJ上。1151是题号题目叫大整数加法。对刚学C语言、刚接触算法竞赛的初学者来说这道题几乎是绕不开的一道坎它不考高精度乘法、不考快速幂就把“两个超长整数相加”这个最基础的问题摆在你面前让你自己动手实现一套完整的加法逻辑。为什么要单独设这么一道题原因很简单C语言内置的整数类型是有限宽的。int能表示的范围大概在-21亿到21亿之间long long也不过是-9.22×10^18到9.22×10^18。一旦数字位数超过19位比如一百位的整数相加完全超出基础类型的存储能力程序直接溢出结果是错的甚至编译器都不给你报错。这种“存不下”的问题在真实开发里也会遇到金额计算、大数统计、加密算法、科学计算处处都可能出现超过内置类型范围的数字。于是就有了“高精度运算”这一整套处理思路而大整数加法就是它的第一课。1.2 这道题的实际需求是什么题目一般会给你两个可能很长的十进制整数比如长度达到几百位甚至上千位要求输出它们相加的结果。你需要自己想办法把它们读进来、存下来、按位相加、处理进位最后把结果输出。这里有个重要的认知要纠正大整数加法不是“数据结构的炫技”它本质上是“用程序模拟手算过程”。小学做加法怎么做的个位对齐、逐位相加、满十进一。计算机做不到直接用加法器去处理几百位的数字因为内置类型放不下。但我们可以用数组一位一位地存一位一位地加照样得出正确结果。这就是高精度加法的全部秘密——没有任何高深数学完全是模拟。1.3 什么样的人适合认真学这道题刚学完C语言循环、数组、字符串的初学者想验证自己是不是真的会写程序准备参加蓝桥杯、ACM新手赛、校内程序设计竞赛的入门选手想补高精度算法专题、后面要学大整数乘法、减法、阶乘的进阶者非科班转码、正在刷OJ题库找感觉的自学者这道题最大的价值不是“AC”本身而是逼你完整走一遍“分析问题-设计存储方案-处理边界-调试优化”的工程流程。很多新手容易栽在“思路好像对了但代码就是WAWrong Answer”往往就是边界条件和存储方式没想清楚。下面我把整个实现过程拆开讲每一步该怎么做、为什么这么做都给你说透。2. 高精度加法的核心思路存储、对齐、进位三件事2.1 为什么数组要倒着存实现大整数加法第一步是处理“输入”和“存储”。题目输入是两个数字字符串你不能直接转成int因为长度可能上百位。所以标准做法是定义字符数组读入例如char a[1005], b[1005]; scanf(%s %s, a, b);读进来的是字符串每个字符是0~9。接下来要把它们变成整型数字存到int数组里。这里最关键的一个细节倒着存。比如输入是12345正着读是a[0]1, a[1]2……但我们要把它存成num[0]5, num[1]4, num[2]3, num[3]2, num[4]1。为什么因为加法要从个位开始算。个位是字符串的最后一位如果正着存个位在数组末尾处理进位时数组下标管理和长度判断都会变得别扭。倒着存之后个位永远在下标0十位在下标1百位在下标2这样我们从下标0开始循环相加顺序天然就是“从低位到高位”和手算过程完全一致。换个角度理解正着存等于把数字“翻过来”写倒着存等于把数字“正过来”放进了适合程序处理的结构。这一步做对了后面代码会非常清爽。2.2 逐位相加与进位的处理逻辑假设两个数倒着存到了数组a和b长度分别是lenA和lenB。加法的核心循环可以写成类似这样的逻辑int maxLen lenA lenB ? lenA : lenB; int carry 0; for (int i 0; i maxLen; i) { int sum (i lenA ? a[i] : 0) (i lenB ? b[i] : 0) carry; result[i] sum % 10; carry sum / 10; } if (carry 0) { result[maxLen] carry; maxLen; }这里有几个核心细节要解释清楚。第一短的数缺位补0。如果a长度是3、b长度是5那么i3和i4时a[i]不存在我们就当它是0处理。这个“缺位补零”的做法保证两数位数不同也能对齐相加。第二carry进位必须参与下一次计算。每次相加都是“当前位两个数字 上次进位”而不是只加两个数字。sum % 10是留在本位的数sum / 10是产生的新进位。这个逻辑模拟的就是满十进一。第三循环结束后可能还有残留进位。比如999 1位数从3位变成4位最后carry1不能丢要单独把最高位补上。这是新手最容易漏的地方漏了就会得到错误结果“000”而非“1000”。2.3 为什么不能直接用一个超大整型类型有的同学会问我用long long存到极限再不行用double行不行答案是不行。long long再大也有限而题目可以给你构造任意长度的数字。用double更不靠谱double用浮点表示精度有限几十位的整数就已经无法精确保存更何况几百位。所以高精度加法的本质就是放弃“语言内置类型存储数字”的想法改用“数组模拟手算”。这也解释了为什么高精度运算几乎是所有算法竞赛入门训练的必经之路它迫使你从“依赖数据类型”切换到“理解数字的本质表示”。数字就是一串按位排列的符号用什么容器存、怎么对齐、怎么进位都是自己决定的。理解了这一点后面再学高精度减法、乘法、除法都是同一个思路的变体。3. 完整实现从读入到输出的一次过写法3.1 字符串长度与逆序转换的细节先把完整的参考代码给出。这个写法我实际提交过多个OJ的同类题稳妥可过。#include stdio.h #include string.h #define MAXN 1005 int main() { char sa[MAXN], sb[MAXN]; int a[MAXN] {0}, b[MAXN] {0}, res[MAXN] {0}; int lenA, lenB, lenRes, carry, i, sum; scanf(%s%s, sa, sb); lenA strlen(sa); lenB strlen(sb); // 逆序存储字符0的ASCII码值是48减48就是对应数字 for (i 0; i lenA; i) { a[i] sa[lenA - 1 - i] - 0; } for (i 0; i lenB; i) { b[i] sb[lenB - 1 - i] - 0; } // 从个位开始逐位相加 lenRes lenA lenB ? lenA : lenB; carry 0; for (i 0; i lenRes; i) { sum a[i] b[i] carry; res[i] sum % 10; carry sum / 10; } // 处理最高位的额外进位 if (carry) { res[lenRes] carry; lenRes; } // 倒序输出因为结果也是个位在下标0 for (i lenRes - 1; i 0; i--) { printf(%d, res[i]); } printf(\n); return 0; }看几个容易栽的点。第一sa[lenA - 1 - i]这一步。lenA-1是字符串最后一个字符的下标也就是个位。随着i从0递增我们依次取倒数第1位、倒数第2位……正好完成逆序。这一步如果写成sa[i]那你存进去的是正序后面输出结果时还得再算一遍下标容易乱。第二字符转数字用减0。0的ASCII码是489是57所以0-009-09。这是通用做法。有些教材写减48效果一样但写减0更直观、可读性更好。第三数组初始化为0。我在定义时直接写了int a[MAXN] {0}这样即使两位长度不同较短的数组后面那些位自然就是0省去了补零的代码。如果你定义成局部变量不初始化里面的值是随机的垃圾值那后面相加就会算出莫名其妙的结果。这是个非常经典的坑。3.2 输入方式与OJ平台的隐藏细节不同OJ上这题的输入格式略有差别。常见的包括一行里用空格分隔两个数如“123 456”上面代码直接scanf(%s%s, sa, sb)即可每行一个数如第一行是第一个数第二行是第二个数。这种情况下printf里换成scanf(%s%s, sa, sb)通常也能通过因为%s会跳过换行符和空白字符有些题目有多组测试数据循环读入直到EOF文件结束。这时候要写成while (scanf(%s%s, sa, sb) ! EOF)每次处理一组输出一组还有个细节如果题目说明数字可能包含前导零比如“00123”你直接按字符串逆序存、逐位加结果会输出“123”如果结果本身不产生额外进位输出时前面不会有多余的0。这是符合常规预期的。但如果题目要求保留前导零那就得专门处理不过通常高精度加法题都不要求保留。3.3 结果输出为什么会多此一举地乱很多新手写出正确结果后输出阶段反而出错。最常见的原因是把结果数组直接用正序打印for (i 0; i lenRes; i) { printf(%d, res[i]); }比如9991res数组里存的是[0, 0, 0, 1]正序打印输出“0001”显然是错的。正确做法是从lenRes-1往0倒着打印。这个错误发生频率极高我自己见过大量同学WA在这一个循环上。还有一个小坑如果结果为0比如00res数组全是0倒序打印时i从0开始for循环条件i 0执行一次printf(0)输出正确。这说明i的循环条件如果写成i 0就会漏掉最高位的0导致什么都不输出。所以循环条件必须包含0。4. 常见错误与调试技巧直接给你一张排查表4.1 WA答案错误的高频原因我按出现频率从高到低列一遍问题现象根本原因解决方案输出结果有前导0如“00123”输出循环时把结果数组正序打印了改成从最高位倒序打印9991结果变成000丢弃了最高位的残留进位循环结束后单独判断carry并补位两位长度不同时结果错乱逆序存储时下标计算错误仔细检查sa[lenA - 1 - i]的写法结果含随机垃圾值局部数组未初始化短数高位补了垃圾值定义时初始化int a[MAXN] {0}数字特别长时数组越界MAXN开太小看题目范围通常1005或10005足够读入可能失败导致死循环scanf不判返回值while (scanf(...) ! EOF)4.2 一个实用的小技巧用字符串模拟手算验证在你懒得写完整程序、或者想快速验证某个边界情况时可以直接拿笔和纸把这个算法的流程走一遍。比如算“58 6”逆序存储a [8, 5]b [6]maxLen 2i086014res[0]4carry1i15016res[1]6carry0结束carry0lenRes保持2倒序输出6 4即64就这么几步全是算术。如果你写代码前能自己在纸上对几个例子的处理过程有数写代码时不容易手忙脚乱。4.3 调试时的万能手段printf大法编程环境允许的情况下在关键位置打印中间变量是排查问题最直接的办法。比如在循环里printf(i%d sum%d carry%d\n, i, sum, carry)就能看到每一位相加的过程。这个手段在OJ上没法用评测环境不会显示你的输出但本地调试阶段非常好使。调试完记得删掉或注释掉。还有一个思路是“对比暴力破解”写一个用long long直接算的版本专门用来验随机小数据。你生成几组小整数分别用两个程序算如果高精度版本和暴力版本输出一致说明大框架没问题。这个方法在应付边界情况时特别有用。5. 进阶思路从1151延伸出去5.1 同样的代码怎么改造成大整数减法大整数加法会了以后减法几乎是同一个思路只不过多了两个难点比较大小和借位。先判断a和b的大小确保大的减小的结果如果为负就单独输出负号。逐位相减时如果当前位不够减就向高一位借1当作10。这个“借位”逻辑和加法的“进位”逻辑是对称的掌握了加法再上手减法会轻松很多。5.2 当题目需要多组输入时怎么办有的版本会要求先输入一个n表示后面有n组大整数相加。这时可以把核心加法逻辑封装成函数void bigAdd(char sa[], char sb[]) { // 逻辑同上输出结果后换行 }然后在main里循环调用即可。这也顺带练习了一个编程好习惯不要把一堆逻辑全堆在main里可读性和可维护性会好很多。5.3 高精度乘法、阶乘的思路预告加法是“一位一位加、处理进位”乘法是“两层循环交叉相乘、再统一处理进位”阶乘则是“反复做高精度乘低精度”。这些题在OJ上也是成片出现但只要你把加法这关彻底吃透后面只是套同样的框架往深了走。所以我才反复强调1151这道题的真正价值不只是AC那一刻的爽感而是你为整套高精度算法打下的地基。6. 个人实操小结这些坑我当年一个个踩过我当年第一次写大整数加法时犯的错很有代表性我把数组正着存了然后一直在输出那里纠结为什么逆序打印还是不对。后来才意识到是存储阶段就出了问题整个数据结构和我想的不一样。这个“存储方向决定后续所有操作方向”的教训让我后来写任何算法题都会先想清楚“数据在数组里是怎么摆放的”而不是上来就写循环。还有一次我把数组定义在函数内部没初始化结果每次运行结果都莫名其妙。这才明白C语言局部变量不自动清零这个特性有多坑。从那以后我定义数组时基本都顺手写上 {0}这个习惯一直保留到现在。再分享一个不算技巧但很实用的经验遇到高精度题目先别急着动手先拿最极端的样例测试思路。比如999 1、1 999、0 0、5000位5000位。这几个用例能一次性覆盖掉进位穿透、长度对齐、等长相加、超长数字等核心边界。如果你本地都能跑过再提交到OJ大概率一遍过。如果你按这篇的思路写完了代码但仍卡在某个测试点上建议回去重点检查两件事一是输出是否倒序二是循环结束后是否处理了多余的进位。这两个问题是高精度加法WA里占比最高的两宗罪把住了它们你的AC率会明显提升。