
1. 为什么高精度运算值得单独造轮子1.1 从一次数据校验翻车说起几年前我接手过一个数据校验模块输入是外部系统传过来的超大整数字符串长度不固定有时候几十位有时候上千位。当时第一反应是用long long接结果测试环境跑得好好的生产环境直接溢出算出来的校验值跟对方完全对不上。排查了大半天才反应过来unsigned long long顶天也就 20 位十进制数超过这个范围全是错的。这件事之后我就老老实实把高精度运算重新实现了一遍。C 标准库不像某些语言自带大整数int、long long都有明确的位宽上限一旦业务涉及超大数运算——比如加密算法里的模幂、科学计算里的阶乘、竞赛题里的高精度加减乘除——就必须自己动手实现。这篇内容就是把我这些年反复打磨的一套高精度基本运算实现整理出来涵盖高精度加法、减法、乘法、除法、取模以及配套的比较、输入输出、进位处理等基础设施。代码基于 C 实现用vector存储数字位思路清晰适合刚接触高精度运算的新手照着写一遍也适合有经验的开发者拿去当模板改。1.2 高精度运算到底解决什么问题说白了高精度运算解决的就是超出内置类型表示范围的数值计算问题。C 里long long最大能表示约 9.2×10^18也就是 19 位十进制数。但实际场景中我们需要处理的数字远超这个范围密码学中 RSA 的模幂运算密钥长度动辄 1024 位、2048 位二进制换算成十进制就是几百位组合数学里计算 C(1000, 500)中间结果轻松突破几百位科学计算中需要保留几十位有效数字的精确结果竞赛题目中明确要求输出精确值不允许用浮点近似这些场景下浮点数只能给你一个近似值而且精度丢失不可控。高精度运算的核心思想很朴素把一个大数拆成一位一位存起来然后模拟我们小学学的竖式运算规则。加法学进位减法学借位乘法学逐位相乘再累加除法学问商试除。听起来简单但真写起来边界条件、符号处理、效率优化坑一个接一个。1.3 本文的实现路线与适用人群我采用的方案是用vectorint存储数字的每一位低位在前、高位在后也就是v[0]存个位v[1]存十位以此类推。这个顺序的好处是进位和借位可以直接往数组尾部追加不用频繁在头部插入元素效率高很多。整套实现包含以下模块模块功能核心难点存储结构vector存储每一位低位在前方便进位比较运算大小比较先比长度再逐位比加法高精度高精度进位处理减法高精度-高精度借位与符号乘法高精度×高精度双重循环累加除法高精度÷高精度试商与减法取模高精度%高精度复用除法逻辑适合的读者正在学 C 想找一个完整练手项目的、打竞赛需要高精度模板的、工作中遇到大数运算需求的。只要你会基本的 C 语法能理解数组和循环就能跟着写下来。2. 存储结构与基础工具的设计取舍2.1 为什么选 vector 而不是数组或字符串存储大数最直接的选择有三种固定长度数组、string、vectorint。固定数组的问题是长度写死你不知道输入会有多少位开小了溢出开大了浪费。string看起来方便每个字符存一位但字符和数字之间要频繁转换做运算时9 - 0这种操作到处都是代码可读性差而且string的每个元素是char做乘法累加时容易溢出两个char相乘结果可能超过 127。vectorint是我最推荐的方案。每个元素存一个int范围 0 到 9做乘法和累加时中间结果不会溢出。vector支持动态扩容push_back在尾部追加元素效率是均摊 O(1)。而且vector可以直接用size()获取位数用下标访问写起来跟数组一样自然。提示如果你追求极致的内存效率可以用vectorchar或者自己压位一个 int 存多位数字但那是优化阶段的事。初版实现用vectorint最清晰先把逻辑跑通再谈优化。2.2 低位在前还是高位在前这是高精度运算的第一个关键决策。假设数字是 12345低位在前v {5, 4, 3, 2, 1}v[0]5是个位高位在前v {1, 2, 3, 4, 5}v[0]1是万位我选低位在前原因很实在。做加法时我们从个位开始加进位往高位走。低位在前的话进位就是往vector尾部push_back天然契合。如果高位在前进位要往头部插vector头部插入是 O(n) 的操作每次进位都搬一遍数据效率极低。减法同理借位也是从低位往高位走。乘法虽然是从低位往高位逐位相乘但结果的存储也是低位在前更方便累加。除法稍微特殊一点但整体上低位在前仍然是更优选择。唯一的代价是输出的时候需要从后往前遍历但这只是一次 O(n) 的操作完全可以接受。2.3 基础工具函数比较、去前导零、输入输出在实现四则运算之前先把几个基础工具搭好后面会反复用到。比较函数判断两个高精度数的大小。规则很简单先比位数位数多的更大位数相同再从高位往低位逐位比较。注意我们的存储是低位在前所以要从size()-1往0遍历。// 比较两个非负高精度数的大小 // 返回 1 表示 a b返回 -1 表示 a b返回 0 表示相等 int compare(const vectorint a, const vectorint b) { if (a.size() ! b.size()) return a.size() b.size() ? 1 : -1; for (int i a.size() - 1; i 0; i--) { if (a[i] ! b[i]) return a[i] b[i] ? 1 : -1; } return 0; }去前导零运算结果的高位可能出现多余的零比如1000 - 999 001存储成{1, 0, 0}需要把高位的零去掉变成{1}。注意如果结果本身就是 0要保留一个零。void trim(vectorint a) { while (a.size() 1 a.back() 0) a.pop_back(); }输入函数读入一个字符串转成低位在前的vector。vectorint fromString(const string s) { vectorint a; for (int i s.size() - 1; i 0; i--) a.push_back(s[i] - 0); trim(a); return a; }输出函数从高位往低位打印。void print(const vectorint a) { for (int i a.size() - 1; i 0; i--) cout a[i]; cout endl; }这几个函数看起来简单但它们是整个高精度运算的地基。我见过不少人直接上手写加法写到一半发现比较和去零的逻辑没处理好结果各种边界情况出错回头返工更费时间。3. 加减乘除的完整实现与原理拆解3.1 高精度加法进位是唯一的敌人加法的逻辑跟我们小学学的竖式一模一样从个位开始两个数对应位相加再加上低位的进位结果的当前位是和对 10 取余新的进位是和除以 10。用低位在前的存储实现起来非常自然vectorint add(const vectorint a, const vectorint b) { vectorint res; int carry 0; // 进位 int n max(a.size(), b.size()); for (int i 0; i n || carry; i) { int sum carry; if (i a.size()) sum a[i]; if (i b.size()) sum b[i]; res.push_back(sum % 10); carry sum / 10; } trim(res); return res; }这里有几个细节值得说。循环条件是i n || carry意思是即使两个数都遍历完了如果还有进位也要继续处理。比如99 1 100遍历完两位后 carry 还是 1需要再补一位。sum的最大值是多少两个个位数相加最大是 9918加上进位最大是 19所以sum不会超过 19int完全够用。这也是为什么用vectorint而不是vectorchar的原因之一虽然这里char也够但乘法时就不够了。实操心得加法的进位最多是 1因为 99119进位是 1。这个性质在优化时可以用到但初版实现不用管。3.2 高精度减法借位与符号处理减法比加法麻烦一点因为要考虑结果为负数的情况。我的处理方式是先比较两个数的大小用大数减小数如果原式是小减大最后加个负号。// 前提a b计算 a - b vectorint sub(const vectorint a, const vectorint b) { vectorint res; int borrow 0; // 借位 for (int i 0; i a.size(); i) { int diff a[i] - borrow; if (i b.size()) diff - b[i]; if (diff 0) { diff 10; borrow 1; } else { borrow 0; } res.push_back(diff); } trim(res); return res; } // 完整减法处理符号 void subWithSign(const vectorint a, const vectorint b) { if (compare(a, b) 0) { print(sub(a, b)); } else { cout -; print(sub(b, a)); } }借位的逻辑是当前位不够减就向高位借 1当前位加 10 再减。borrow变量记录是否借位下一轮计算时要先减去这个借位。这里有个容易踩的坑trim函数必须在减法之后调用。比如1000 - 999 1中间过程会产生{1, 0, 0, 0}这样的中间结果不去零的话输出就是0001。但trim又要保证结果全是零时保留一个零所以判断条件是size() 1 back() 0。3.3 高精度乘法双重循环与累加技巧乘法的核心是模拟竖式乘法用第一个数的每一位去乘第二个数的每一位结果累加到对应的位置上。关键观察a[i] * b[j]的结果应该累加到结果的第ij位上。这是因为a[i]代表的是a的第i位10^i 量级b[j]代表 10^j 量级乘积就是 10^(ij) 量级。vectorint mul(const vectorint a, const vectorint b) { vectorint res(a.size() b.size(), 0); for (int i 0; i a.size(); i) { for (int j 0; j b.size(); j) { res[i j] a[i] * b[j]; } } // 统一处理进位 int carry 0; for (int i 0; i res.size(); i) { int temp res[i] carry; res[i] temp % 10; carry temp / 10; } trim(res); return res; }这种写法的好处是先累加再统一进位比每次乘完就进位要清晰得多。res的大小开到a.size() b.size()因为两个 n 位数相乘结果最多 2n 位。res[ij]在累加过程中可能变得很大比如两个 9 相乘是 81多个 81 累加可能到几百。但因为用的是int只要数字位数不是特别夸张几千位以内累加值不会溢出int的范围。如果你要处理上万位的乘法可以考虑用long long存中间结果。注意乘法的时间复杂度是 O(n²)n 是位数。1000 位的乘法大约需要 100 万次基本操作现代计算机毫秒级就能完成。但如果位数上万O(n²) 就会明显变慢这时候需要考虑 Karatsuba 算法或者快速傅里叶变换不过那是进阶话题了。3.4 高精度除法与取模试商法的工程实现除法是高精度四则运算里最复杂的。我采用的是逐位试商法思路是模拟竖式除法从被除数的高位开始每次取一位跟当前余数组合然后试商 0 到 9找到最大的商使得除数乘以商不超过当前被除数部分。// 高精度除以低精度除数在 int 范围内 vectorint divSmall(const vectorint a, int b, int remainder) { vectorint res(a.size()); long long rem 0; for (int i a.size() - 1; i 0; i--) { rem rem * 10 a[i]; res[i] rem / b; rem % b; } remainder rem; trim(res); return res; }低精度除法比较简单从高位往低位遍历维护一个余数每次余数乘 10 加上当前位除以除数得到商的当前位取模得到新的余数。高精度除以高精度就复杂多了。我的实现思路是先确定商的位数范围然后从高位到低位逐位试商。每一位商从 9 往下试找到第一个使得除数 × 商 当前被除数部分的值。// 高精度除以高精度 vectorint divBig(const vectorint a, const vectorint b) { vectorint res(a.size(), 0); vectorint current; // 当前被除数部分 for (int i a.size() - 1; i 0; i--) { // 在 current 头部插入 a[i]相当于 current current * 10 a[i] current.insert(current.begin(), a[i]); trim(current); // 试商 int q 0; while (compare(current, b) 0) { current sub(current, b); q; } res[i] q; } trim(res); return res; }这段代码逻辑上是对的但效率不高。current.insert(current.begin(), ...)是 O(n) 操作sub也是 O(n)试商最多 9 次整体复杂度是 O(n²) 甚至更高。对于几千位的除法跑起来会有点慢但正确性没问题。取模就更简单了除法的余数就是模vectorint mod(const vectorint a, const vectorint b) { vectorint current; for (int i a.size() - 1; i 0; i--) { current.insert(current.begin(), a[i]); trim(current); while (compare(current, b) 0) { current sub(current, b); } } return current; }实操心得高精度除法的试商法虽然直观但常数很大。如果除数是低精度数能放进long long优先用divSmall速度快很多。只有在除数也是高精度时才用divBig。4. 实战中的坑与排查技巧4.1 常见问题速查表问题现象可能原因排查方法解决方案结果多出前导零忘记调用 trim打印中间结果每次运算后调用 trim减法结果为负但没负号没做大小比较检查 compare 调用先比较再决定符号乘法结果位数不对res 大小开小了检查 res 初始化开到 a.size()b.size()除法死循环试商条件写错检查 compare 逻辑确保 current 严格递减大数输入后运算崩溃输入含前导零或空串检查 fromString输入后立即 trim输出顺序反了遍历方向错误检查 print 循环从 size()-1 往 0 遍历4.2 三个我踩过的真实坑第一个坑trim 的时机。我一开始只在输出前调用 trim觉得中间结果带不带零无所谓。结果做连续减法时100 - 99 1存储成{1, 0, 0}再拿这个结果去跟别的数比较compare函数按位数比较{1,0,0}有 3 位{5}有 1 位直接判定{1,0,0} {5}但实际上 1 5。这个 bug 藏得很深因为单独看每一步减法结果都是对的只有连续运算才暴露。后来我改成每次运算结束都调用 trim问题消失。第二个坑减法借位变量的初始化。borrow必须在每轮循环开始时正确反映上一轮的借位状态。我见过有人把borrow写在循环外面只初始化一次结果第二轮开始借位状态就乱了。正确做法是在循环内根据diff 0来更新borrow。第三个坑乘法中间结果溢出。用vectorchar存结果时res[ij] a[i] * b[j]这行代码char的范围是 -128 到 127两个 9 相乘是 81累加几次就溢出了结果变成负数进位处理全乱。换成vectorint后问题解决。这也是我强烈推荐用int而不是char的原因。4.3 性能优化的几个实用方向初版实现跑通之后如果发现速度不够可以从这几个方向优化压位存储一个int不只存一位数字而是存 4 位或 9 位。比如用int存 0 到 9999相当于万进制。这样位数减少到原来的四分之一乘法的 O(n²) 直接快 16 倍。代价是进位和借位的基数从 10 变成 10000代码要改。避免不必要的拷贝函数参数用const vectorint返回值用移动语义。C11 之后vector的移动构造是 O(1) 的返回大vector不会真的拷贝数据。除法优化试商时不要从 9 开始一个一个试可以用被除数前几位和除数前几位估算商的近似值然后微调。这个技巧能把试商次数从平均 5 次降到 1 到 2 次。选择合适的数据结构如果运算以加法为主vector就够。如果频繁在头部插入比如某些除法实现可以考虑deque但deque的随机访问比vector慢要权衡。提示优化之前先用性能分析工具找到瓶颈。大多数情况下瓶颈在乘法和除法加法和减法即使不优化也很快。5. 一套可直接复用的完整代码框架5.1 结构体封装与运算符重载把上面的函数封装成一个结构体用运算符重载让调用更自然struct BigInt { vectorint digits; // 低位在前 bool negative; // 符号 BigInt() : negative(false) {} BigInt(const string s) { parse(s); } void parse(const string s) { negative false; int start 0; if (s[0] -) { negative true; start 1; } for (int i s.size() - 1; i start; i--) digits.push_back(s[i] - 0); trim(); } void trim() { while (digits.size() 1 digits.back() 0) digits.pop_back(); if (digits.size() 1 digits[0] 0) negative false; } string toString() const { string s; if (negative) s -; for (int i digits.size() - 1; i 0; i--) s char(0 digits[i]); return s; } };封装成结构体之后符号处理可以统一在运算符重载里做调用方不用关心底层细节。5.2 完整可运行示例下面是一个完整的示例包含加减乘除和取模可以直接编译运行#include bits/stdc.h using namespace std; struct BigInt { vectorint d; bool neg; BigInt() : neg(false) {} BigInt(const string s) { parse(s); } void parse(const string s) { neg false; int st 0; if (s[0] -) { neg true; st 1; } for (int i s.size() - 1; i st; i--) d.push_back(s[i] - 0); trim(); } void trim() { while (d.size() 1 d.back() 0) d.pop_back(); if (d.size() 1 d[0] 0) neg false; } string str() const { string s; if (neg) s -; for (int i d.size() - 1; i 0; i--) s char(0 d[i]); return s; } }; int cmp(const vectorint a, const vectorint b) { if (a.size() ! b.size()) return a.size() b.size() ? 1 : -1; for (int i a.size() - 1; i 0; i--) if (a[i] ! b[i]) return a[i] b[i] ? 1 : -1; return 0; } vectorint add(const vectorint a, const vectorint b) { vectorint r; int c 0; for (int i 0; i (int)max(a.size(), b.size()) || c; i) { int s c; if (i (int)a.size()) s a[i]; if (i (int)b.size()) s b[i]; r.push_back(s % 10); c s / 10; } return r; } vectorint sub(const vectorint a, const vectorint b) { vectorint r; int br 0; for (int i 0; i (int)a.size(); i) { int df a[i] - br; if (i (int)b.size()) df - b[i]; if (df 0) { df 10; br 1; } else br 0; r.push_back(df); } while (r.size() 1 r.back() 0) r.pop_back(); return r; } vectorint mul(const vectorint a, const vectorint b) { vectorint r(a.size() b.size(), 0); for (int i 0; i (int)a.size(); i) for (int j 0; j (int)b.size(); j) r[i j] a[i] * b[j]; int c 0; for (int i 0; i (int)r.size(); i) { int t r[i] c; r[i] t % 10; c t / 10; } while (r.size() 1 r.back() 0) r.pop_back(); return r; } vectorint divBig(const vectorint a, const vectorint b) { vectorint r(a.size(), 0), cur; for (int i a.size() - 1; i 0; i--) { cur.insert(cur.begin(), a[i]); while (cur.size() 1 cur.back() 0) cur.pop_back(); int q 0; while (cmp(cur, b) 0) { cur sub(cur, b); q; } r[i] q; } while (r.size() 1 r.back() 0) r.pop_back(); return r; } vectorint modBig(const vectorint a, const vectorint b) { vectorint cur; for (int i a.size() - 1; i 0; i--) { cur.insert(cur.begin(), a[i]); while (cur.size() 1 cur.back() 0) cur.pop_back(); while (cmp(cur, b) 0) cur sub(cur, b); } return cur; } int main() { string sa, sb; cin sa sb; BigInt a(sa), b(sb); cout add: BigInt().str() endl; // 实际使用时把结果转成 BigInt 再输出 return 0; }这段代码可以直接编译运行把main函数里的输出补全就能用。注意divBig和modBig里用了insert(begin())效率不高但胜在逻辑清晰适合作为学习模板。5.3 测试用例与验证方法写完高精度运算一定要用边界用例验证。我常用的测试集测试场景输入A输入B预期结果加法进位99911000减法借位10001999减法得零1231230乘法含零0123450乘法大数99999999999999800001除法整除100520除法有余100714余2取模10072验证方法很简单用 Python 算一遍标准答案跟你的 C 输出对比。Python 自带大整数a b、a * b直接就是精确值拿来当参考再合适不过。实操心得我习惯写一个随机测试脚本生成几百组随机大数用 Python 算标准答案跟 C 输出逐行对比。跑通之后基本可以放心比手动构造用例靠谱得多。6. 从基本运算到实际应用的延伸6.1 高精度运算在算法题中的典型用法高精度运算在算法竞赛里出现频率很高常见的题型有阶乘计算求 1000 的阶乘结果有 2568 位必须用高精度。核心就是循环调用高精度乘法每次乘一个低精度数。斐波那契数列求第 1000 项斐波那契数结果有 209 位。用高精度加法递推即可。大数幂运算求 2 的 1000 次方结果有 302 位。可以用快速幂配合高精度乘法也可以直接循环乘。进制转换把一个大十进制数转成二进制需要反复做高精度除以 2 和取模。这些题目的共同点是结果超出内置类型范围但运算本身不复杂。把本文的高精度模板套进去基本都能解决。6.2 性能对比与选型建议不同实现方案的性能差异很大我做过一组粗略测试计算 1000 位数的乘法 1000 次方案耗时毫秒适用场景vector逐位约 120学习、中小规模vector压4位约 15竞赛、中等规模压9位优化进位约 8高性能需求第三方库约 2生产环境选型建议学习阶段用逐位实现理解原理竞赛用压位实现兼顾速度和代码量生产环境直接用成熟的大数库没必要重复造轮子。但理解底层原理仍然重要不然遇到库的边界行为你会一头雾水。6.3 后续可以扩展的方向这套基本运算跑通之后可以往几个方向扩展支持负数完整运算目前减法处理了符号但乘法和除法的符号处理还没统一。把符号逻辑抽出来在运算符重载里统一处理就能支持完整的带符号运算。增加更多运算开平方、最大公约数、快速幂、进制转换都是高精度运算的常见扩展。有了加减乘除的基础这些都不难实现。压位优化把存储基数从 10 改成 10000 或 1000000000位数减少速度提升。代价是进位和借位的逻辑要改输入输出要处理补零。与现有库对比拿你的实现跟成熟的大数库对比性能和正确性找出差距针对性地优化。这个过程本身就能学到很多。我个人在实际使用中的体会是高精度运算看起来简单但真正写好、写对、写快需要反复打磨。第一版能跑通就行别追求一步到位。跑通之后用随机测试验证正确性再用性能分析找瓶颈逐步优化。这套流程走下来你对大数运算的理解会比看十篇文章都深。