☰
c++加法高精度算法的简单实现
2026/11/6 6:47:32 网站建设 项目流程

前言


高精度加法要解决的问题很具体:当两个整数的位数超过内置类型能表示的范围时,怎样仍然得到逐位精确的和。比如一千位的十进制数,任何标准的整数类型都装不下,浮点类型虽然范围够大,但只有有限的有效数字,低位会被舍入掉。这类需求出现在大数计算、某些加密算法的辅助运算、以及教学题目里。


一个常见的误解是「用long long就够了」。long long至少 64 位,能表示的十进制位数大约十九位,超过这个长度就必须换方法。另一个误解是「用double存,反正范围很大」——double的尾数是 53 位二进制,约十五到十七位十进制有效数字,位数一多,个位就开始不准,而且这种误差不会报错,只会悄悄给出错误答案。


本文讲的是最基础的一种实现:把数字当成字符串,按小学竖式从低位往高位逐位相加,处理进位。这个思路简单、可移植、不依赖任何第三方大数库,复杂度是线性的。文章会先说明内置类型的边界,再给出无符号版本的实现,然后补上符号处理与边界情况,最后列出实现时最容易踩的坑。


一、内置整数类型到底能装多大


C++ 标准只规定各整数类型的最小宽度,具体宽度由实现决定。标准保证long long至少 64 位,int至少 16 位,但现实中常见的组合是:int32 位,long在 Windows(LLP64 模型)上是 32 位、在 64 位 Linux(LP64 模型)上是 64 位,long long是 64 位。




类型标准保证的最小宽度常见实现能表示的十进制位数(约)



int16 位32 位9

long32 位Windows 32 位 / Linux 64 位9 或 18

long long64 位64 位19

unsigned long long64 位64 位20



(「常见实现」一列不是标准规定,换平台要以<climits>里的INT_MAX、LLONG_MAX等宏为准。)


有符号整数溢出在 C++ 里是未定义行为(undefined behavior, UB),不是「回绕成负数」这么温和。无符号整数溢出则是良定义的模运算,会回绕,但回绕后的值显然不是我们要的数学结果。所以「先算再看有没有溢出」这条路走不通,必须在表示层面就换掉。


高精度加法的核心交换是:不用一个整数表示整个数,而用一个字符序列表示每一位。std::string天然适合,下标访问是常数时间,末尾追加是摊还常数时间。


二、竖式加法的逐位规则


手算两个多位数相加时,我们从最右边一位开始,把同一位的两个数字与上一位的进位相加,结果的个位留下,十位作为新的进位,一直做到两个数都没有位、且进位为 0 为止。


用字符串实现时,字符串的第 0 个字符是最高位,而计算要从最低位开始,于是有两个选择:先把两个字符串反转,或者用两个下标从末尾往前走。后者不需要额外空间,代码也更直接。


每一位的和最大是 9 + 9 + 1 = 19,用一个int暂存完全够。留下的位是sum % 10,新进位是sum / 10,由于和不超过 19,进位只可能是 0 或 1。


复杂度:设两个串长度分别为 m 和 n,循环次数是 max(m, n) 再加至多一次,时间 O(max(m, n)),结果串占 O(max(m, n)) 空间。这是这类问题的最优量级,因为读入和写出本身就要这么多。


有一个容易忽略的点:循环条件必须是「还有位」或者「进位不为 0」三者取或,不能只写前两者。否则像 999 + 1 这种会在最高位丢掉那个 1。


三、符号、前导零与非法输入


真正可用的加法还要处理三种情况。


第一是符号。两个同号的数,绝对值相加,符号不变;两个异号的数,等于绝对值大的减去绝对值小的,符号取绝对值大的那一方。所以除了addUnsigned,还需要一个cmpAbs比较绝对值和一个subUnsigned做「大减小」。比较时先比长度(去掉前导零之后),长度相同再逐位比较字典序。


第二是前导零。输入可能是"000123",结果也可能出现"000"这样的形态。统一的做法是写一个stripLeadingZeros,在参与运算前和返回结果前各调用一次;全部是零时返回"0"。


第三是非法输入。字符串里出现非数字字符时,c - '0'会得到一个无意义的偏移,运算结果没有意义但程序不会报错。工程代码应当在入口处检查每个字符是否落在'0'到'9'之间,不合法就拒绝,而不是继续算。


下面给出完整的示例。为了控制篇幅,示例假定输入已经过合法性检查,只处理十进制、允许前导零、允许正负号。


// 适用于 C++17
#include <algorithm>
#include <iostream>
#include <string>

// 去掉前导零;全零时返回 "0"
std::string stripLeadingZeros(const std::string& s) {
std::size_t pos = s.find_first_not_of('0');
if (pos == std::string::npos) {
return "0";
}
return s.substr(pos);
}

// 无符号竖式加法:a、b 只含数字字符
std::string addUnsigned(const std::string& a, const std::string& b) {
std::string res;
res.reserve(std::max(a.size(), b.size()) + 1);
int i = static_cast<int>(a.size()) - 1;
int j = static_cast<int>(b.size()) - 1;
int carry = 0;
while (i >= 0 || j >= 0 || carry != 0) {
int sum = carry;
if (i >= 0) {
sum += a[static_cast<std::size_t>(i)] - '0';
--i;
}
if (j >= 0) {
sum += b[static_cast<std::size_t>(j)] - '0';
--j;
}
res.push_back(static_cast<char>('0' + sum % 10));
carry = sum / 10;
}
std::reverse(res.begin(), res.end());
return stripLeadingZeros(res);
}

这段代码里有几处细节值得说明。res.reserve只是减少重新分配,不影响正确性。下标用int而不是std::size_t,是因为std::size_t是无符号的,i >= 0恒为真,循环会越界。static_cast<std::size_t>(i)在下标访问时把已经确定非负的值转回去,避免有符号与无符号比较的警告。sum % 10和sum / 10对非负的sum没有歧义。


接着是绝对值比较与「大减小」:


// 适用于 C++17
// 比较两个无符号十进制串的绝对值:a < b 返回 -1,相等返回 0,a > b 返回 1
int cmpAbs(const std::string& a, const std::string& b) {
std::string x = stripLeadingZeros(a);
std::string y = stripLeadingZeros(b);
if (x.size() != y.size()) {
return x.size() < y.size() ? -1 : 1;
}
if (x < y) return -1;
if (x > y) return 1;
return 0;
}

// 要求 a >= b(按绝对值),返回 a - b 的无符号结果
std::string subUnsigned(const std::string& a, const std::string& b) {
std::string res;
res.reserve(a.size());
int i = static_cast<int>(a.size()) - 1;
int j = static_cast<int>(b.size()) - 1;
int borrow = 0;
while (i >= 0) {
int d = a[static_cast<std::size_t>(i)] - '0' - borrow;
if (j >= 0) {
d -= b[static_cast<std::size_t>(j)] - '0';
--j;
}
if (d < 0) {
d += 10;
borrow = 1;
} else {
borrow = 0;
}
res.push_back(static_cast<char>('0' + d));
--i;
}
std::reverse(res.begin(), res.end());
return stripLeadingZeros(res);
}

有了这两个函数,带符号的加法就可以写成一个分派:


// 适用于 C++17
// 输入形如 "123"、"-456",已通过合法性检查
std::string addSigned(const std::string& a, const std::string& b) {
bool negA = !a.empty() && a[0] == '-';
bool negB = !b.empty() && b[0] == '-';
std::string ua = negA ? a.substr(1) : a;
std::string ub = negB ? b.substr(1) : b;

if (negA == negB) {
std::string sum = addUnsigned(ua, ub);
if (negA && sum != "0") {
return "-" + sum;
}
return sum;
}
int cmp = cmpAbs(ua, ub);
if (cmp == 0) {
return "0";
}
if (cmp > 0) {
std::string diff = subUnsigned(ua, ub);
return (negA && diff != "0") ? "-" + diff : diff;
}
std::string diff = subUnsigned(ub, ua);
return (negB && diff != "0") ? "-" + diff : diff;
}

cmp > 0表示 a 的绝对值更大,结果符号跟 a 走;否则结果符号跟 b 走。diff != "0"的判断是为了避免产生"-0":数学上零没有符号,"-0"虽然多数解析器能吃,但作为字符串结果不干净,后续比较也容易出问题。


常见坑点


1. 用内置类型硬扛❌long long c = std::stoll(a) + std::stoll(b);—— 位数一多就抛异常或溢出,有符号溢出还是 UB。 ✅ 位数可能超过十八九位时,直接用字符串竖式,不要先转成整数。


2. 循环条件漏掉进位❌while (i >= 0 || j >= 0) { ... }—— 999 + 1 的最高位进位丢失。 ✅while (i >= 0 || j >= 0 || carry != 0) { ... }。


3. 无符号下标倒着遍历❌for (std::size_t i = a.size() - 1; i >= 0; --i)——i >= 0恒真,而且空串时a.size() - 1会下溢,循环失控。 ✅ 用int下标,或者用std::string::const_reverse_iterator。


4. 字符和数字混着加❌sum += a[i];—— 加进去的是'7'的编码值 55,不是 7。 ✅sum += a[i] - '0';,回写时再'0' + digit。


5. 结果不剥前导零❌ 直接返回"000123",与"123"做字符串比较时判定为不相等。 ✅ 返回前统一调用去零函数,全零返回"0"。


6. 把负数直接按位相加❌ 对"-12"和"5"都去掉符号后相加,得到"-17"。 ✅ 先比较符号:同号加绝对值,异号用大绝对值减小绝对值并取较大者的符号。


7. 非法字符不检查❌ 假定输入一定是数字,c - '0'算出负数也照常进位。 ✅ 入口处逐字符检查是否在'0'到'9'之间,不合法立即拒绝。


总结




要点做法



表示用std::string存十进制位,第 0 位是最高位

方向从末尾往前遍历,或先反转再算

进位每位和除以 10,循环条件必须包含进位

符号同号加、异号减,比较绝对值决定结果符号

边界去前导零、拒绝非法字符、避免"-0"

复杂度时间与空间都是 O(max(m, n))



高精度加法的难点不在算法,而在边界:进位、前导零、符号、下标类型,任何一处疏忽都会得到「看起来像对的」错误结果。把无符号的竖式写对,再把符号处理拆成独立的比较与减法,代码就能保持清晰,也容易扩展到减法、乘法和除法。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询