尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

int溢出与大数运算:从13!翻车到C++高精度计算实现

int溢出与大数运算:从13!翻车到C++高精度计算实现 1. 从一个溢出事故说起1.1 那次13!引发的翻车大一那年写C语言作业老师布置了一道很普通的阶乘题。我信心满满地写下int result 1;循环从1乘到n前11个数都正常12! 输出479001600一切看起来都很完美。结果乘到13的时候输出变成了一个负得莫名其妙的数字——1932053504。我盯着屏幕看了半天第一反应是代码写错了后来又觉得是编译器出了问题压根没想到问题出在int本身的容量上。后来学了计算机组成原理、操作系统、数据表示才终于弄明白int在32位环境下是有符号整数最高位是符号位能表示的只有 -2147483648 到 2147483647。只要运算结果一超出这个范围有符号整数溢出的行为在C/C里直接是未定义行为undefined behavior具体表现出来可能是负数、是0也可能是任何一个你没见过的数。而阶乘这种增长速度极快的运算就是最容易把int干翻的场景之一。这个经历让我对int类大数计算产生了很大的兴趣。后面我也陆续做过不少相关练习——计算大数阶乘、大数次方甚至用字符串模拟手算乘法。今天就把这些经验整理一下从int上限讲起聊到大数运算的实现思路顺便把那些年和int相关的各种编译错误、转换问题一并复盘。1.2 int的类型边界与常见误区先说基础。C/C里int到底占几个字节其实标准没有硬性规定只要求至少16位。但在绝大多数现代编程环境Windows/Linux/macOS的32位或64位程序中int基本是4字节也就是32位。有符号时范围是 -2147483648 ~ 2147483647无符号时范围是 0 ~ 4294967295。很多人容易忽略几个点有符号int溢出是未定义行为不是一个可预测的环绕。无符号int溢出是明确定义的回绕按照2^32取模。long在Windows里是4字节Linux 64位下才是8字节跨平台时要当心。long long在C11中是标准类型至少8字节。浮点数从一开始就不适合做精确整数运算double虽然能表示到很大的数但超过 2^53 后精度就开始丢失。所以在处理阶乘、次方这类增长爆炸的运算时第一反应不应该是换更大的int/long long再试试而是先算清楚这个结果需要用多少bit才能装下。如果结果是几百位甚至上千位的十进制数那就直接走大数运算方案别再纠结内置类型了。2. 为什么阶乘和次方最容易挑战int上限2.1 阶乘的增长速度让人没有防备阶乘是个很骗人的东西前几项看起来都很温和1! 15! 12010! 362880012! 479001600都还在int能扛的范围内。到了13!一下子就变成了6227020800直接超出int上限的3倍。更有意思的是long long也撑不了多久。long long最大能到 9223372036854775807约9.22×10^1820! 2432902008176640000还在范围内但21! 51090942171709440000直接爆了。也就是说就算你换成long long也只能算到20!再多一位就顶不住。所以实战中我会先做一个粗略估算目标n的阶乘大约是多少位。用斯特林公式可以直接算个大概位数 ≈ log10(n!) n * log10(n) - n * log10(e) 0.5 * log10(2πn)比如算100!位数大概是 100 * 2 - 100 * 0.434 0.5 * 1.8 157.5也就是说100!约158位。int连它的零头都装不下。2.2 次方运算同样不省心次方也是同样的道理。2^30 1073741824还在int范围内2^31 2147483648刚好越界。很多人在做快速幂或者二分答案时都会遇到一个经典场景mid (left right) / 2没问题但如果是left right本身已经超过int上限就直接炸了。次方的炸法还有另一种隐蔽的不是结果溢出而是中间步骤溢出。比如你要计算(a * b) % moda和b明明都在int范围内但 a*b 容易超过int上限需要先转long long再乘。这种中间过程溢出比最终结果溢出更容易被忽略排查起来也更花时间。计算次方时如果指数特别大连long long都扛不住。遇到这种题目第一反应应该是快速幂并且每一步都取模或者干脆把中间结果也存成大数。2.3 想用更大的类型续命到底行不行很多人遇到溢出会想int不行就long longlong long不行就unsigned long long还不行就用__int128GCC扩展支持。这些做法在小规模计算里确实管用但有一个根本问题内置整型的位数是有限的。我们来统计一下类型大小最大值能算到多少位十进制数int4字节2147483647约10位long long8字节约9.22×10^18约19位__int12816字节约1.7×10^38约39位就算用__int128也只能支撑到大约39位的整数。遇到像 50! 这种65位的数照样无解。再往上就没有内置类型可换了必须换思路。所以结论很明确如果你需要计算的结果位数超过40位就别在类型选择上挣扎了。直接用大数运算方案用数组或字符串来存储每一位数字用模拟手算的方式做加减乘除。这也是为什么大数阶乘、大数次方这类题目几乎成了算法学习里绕不开的基础练习。3. 动手实现C大数阶乘与大数次方3.1 核心思路用数组模拟手算乘法大数运算最常见的实现思路就是模拟小学学的竖式乘法。我们把一个大数拆成一位一位的数字存进数组或vector里然后逐位相乘、逐位进位。举个例子计算 12345 × 61 2 3 4 5 × 6 ------------------- 6 12 18 24 30从低位往高位依次计算每位乘以6后把乘积的个位留在这个位置上十位以上的部分加到下一位这就是进位。把30记在个位进位324加上进位3得27个位7留下进位2一路算到头还剩余进位就直接往高位追加。这个过程用代码实现非常直观。数字的存放方式我通常采用低位在前即vector的第0位存个位这样进位操作只需要在数组末尾扩展效率最高也最不容易出错。3.2 大数阶乘的完整实现直接上代码。我用vector 来存储每一位数字初始化为1因为0! 1! 1。外层循环从2一直乘到n内层循环对当前大数的每一位做乘法并处理进位。#include iostream #include vector #include string using namespace std; // 大数乘以一个小整数 void multiply(vectorint num, int x) { int carry 0; for (int i 0; i (int)num.size(); i) { int product num[i] * x carry; num[i] product % 10; carry product / 10; } while (carry 0) { num.push_back(carry % 10); carry / 10; } } // 计算 n!返回字符串 string factorial(int n) { vectorint num(1, 1); for (int i 2; i n; i) { multiply(num, i); } string result; for (int i (int)num.size() - 1; i 0; i--) { result.push_back(char(0 num[i])); } return result; } int main() { int n 100; string res factorial(n); cout n ! res endl; cout 位数: res.size() endl; return 0; }这段代码的输出结果是100!的全部158位数字。关键点在multiply函数里num[i] * x carry可能会比较大但x本身不超过nnum[i]只有0~9所以num[i] * x最大也就9n完全在int可承受范围内不需要动用long long。我刚跑了一下验证结果100!确实等于93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000正好158位。这个输出非常整齐可以拿来验证自己的实现。3.3 大数乘大数与快速幂如果只是算阶乘上面的大数×小整数方案就够了。但题目如果再变一下让你算大数的次方比如 2^1000就需要大数×大数的乘法了。大数×大数的思路依然是模拟竖式只不过需要两层循环。核心公式是a[i] * b[j]的结果要累积到结果的ij和ij1位上。我自己写的时候习惯用一个长度是a.size() b.size()的数组来存中间结果最后再统一处理进位。#include iostream #include string #include algorithm using namespace std; // 大数乘以大数字符串输入字符串输出 string multiplyStrings(string a, string b) { if (a 0 || b 0) return 0; int n a.size(), m b.size(); vectorint res(n m, 0); // 从低位到高位逐位相乘 for (int i n - 1; i 0; i--) { for (int j m - 1; j 0; j--) { int mul (a[i] - 0) * (b[j] - 0); int p1 i j; // 高位位置 int p2 i j 1; // 低位位置 int sum mul res[p2]; res[p2] sum % 10; res[p1] sum / 10; } } // 跳过前导0并转为字符串 string result; for (int v : res) { if (!(result.empty() v 0)) { result.push_back(char(0 v)); } } return result.empty() ? 0 : result; }有了大数乘大数大数次方就很自然了。如果指数比较小直接循环乘就完事指数大则要上快速幂。快速幂的原理是二进制分解把指数拆成2的幂之和这样原本需要n次乘法降为约log2(n)次。// 快速幂base^expbase是大数exp是普通整数 string power(string base, int exp) { string result 1; while (exp 0) { if (exp 1) { result multiplyStrings(result, base); } base multiplyStrings(base, base); exp 1; } return result; } int main() { string res power(2, 1000); cout 2^1000 res endl; cout 位数: res.size() endl; return 0; }2^1000的结果是10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376共302位。这里有个实现细节得提醒一下快速幂里每次base multiplyStrings(base, base)会让数字越来越大乘法的时间复杂度是平方级的。如果指数特别大比如百万级别这个做法会非常慢那时候可能需要FFT之类的优化但这就是另一个话题了。4. int相关的编译错误与转换问题速查4.1 int转QStringQt开发常踩的坑把int转成字符串几乎是写QT界面时最常见的一个操作。很多新手会直接写QString s 123;编译直接报错因为QString没有一个接收int的隐式构造函数。正确做法有几种int num 42; QString s1 QString::number(num); // 方式1 QString s2 QString(%1).arg(num); // 方式2 QString s3 QStringLiteral(42); // 方式3直接用字面量就不用转了反过来也一样QString转int需要用toInt()而且最好判断一下是否成功。有个细节QString::number默认是十进制如果你想转十六进制可以用QString::number(num, 16)这在写进制转换小工具时很方便。4.2 枚举与int的强制转换C里的enum和int之间的关系规则比较绕。传统枚举不带class的可以隐式转换为int比如enum Color { RED, GREEN, BLUE }; int x RED; // 合法x 0但反过来就不行Color c 1; // 编译错误必须显式转换Color c static_castColor(1);C11之后有了enum class强枚举连枚举隐式转int都不允许了必须static_cast。这种设计的初衷是防止不小心把类型混淆。如果你看到c int enum报错多半是这三个原因之一反向赋值没转换、enum class和普通enum混用、在switch里拿int和枚举比较。4.3 函数指针误当成int返回invalid conversion from void (*)() to int这个报错一眼看上去很吓人实际上大多数情况就是函数名忘加括号。比如你写int x someFunction; // 忘加括号someFunction是函数名编译器 interprets 函数名作为函数指针void (*)()类型然后你想把它赋给int类型对不上就报了这个错。解决办法很简单加上调用括号int x someFunction();。我当年还踩过另一个类似的坑int x rand;忘了写括号。编译器提示的invalid conversion from int (*)(void) to int和上面几乎一模一样排查方法就是看是不是漏了括号或者误把函数名当作变量使用。4.4 Python里的次方类型错误Python虽然自带大整数但类型错误一样会出现。最常见的就是TypeError: unsupported operand type(s) for ** or pow(): str and int比如2 ** 3字符串不能做次方运算。解决方式是把字符串转成int或floatint(2) ** 3 # 得到 8这个错误的本质就是类型不匹配和C里把int赋给string、把string赋给int是一个道理。我建议初学者把这类错误归成一类先检查两侧操作数的类型再去看运算逻辑。很多时候一个type()打印出来问题就一目了然了。5. 常见问题与避坑清单5.1 求int类型数字长度的方法评论区常有人问如何求int类型数字的长度这里总结一下几种写法// 方式1循环除10 int getLen(int n) { if (n 0) return 1; int len 0; while (n ! 0) { n / 10; len; } return len; } // 方式2转字符串 int getLen2(int n) { string s to_string(n); return s.size(); } // 方式3数学方法 int getLen3(int n) { if (n 0) return 1; return (int)log10(abs(n)) 1; }三种方式各有优劣循环除10最直观转字符串最通用负数也自动处理数学方法最快但要注意负数要先取绝对值。如果你的int是负数循环除10容易被负数除法的行为坑到建议先取绝对值。5.2 从编程语言到GIS、PLC、大数据分析的类型直觉类型选择的问题并不只在编程题里出现。比如GIS属性表里字段类型分为char、float、int、time等选错类型会导致排序、查询结果完全不对PLC编程里int和real区别也非常大int是整数、real是浮点数运算时若隐式转换不当就会丢精度大数据分析项目里用虚拟机搭Hadoop或Spark跑数据时如果原始数据的类型不乱后面清洗、聚合都会省很多事。我自己做招聘网站大数据分析与可视化项目时深有体会数据仓库里字段类型定得不好后面做统计会崩给你看。比如薪资字段若用字符串存储12000和9000按字符串排序的结果是12000排前面而不是9000。这类问题看起来和int无关本质其实是同一件事编程语言/数据库里的类型定义决定了你能对它做什么运算。5.3 大数运算避坑清单最后集中梳理一下我踩过的坑和一些实用经验。先看编码顺序问题。大数乘法里外层循环从低位到高位还是从高位到低位我自己的习惯是都从低位开始然后用数组下标来管理位置。从高位开始处理进位很容易出错因为进位是向右低位传递的方向。再来看进位溢出问题。multiply函数里进位可能不止一位。比如大数乘以13时某一位原本是99×13 117除了留下7之外进位是11这是一个两位数。所以进位变量要用int不能假设它是个位数。还有前导零的清理问题。大数乘大数后结果数组的最前面可能残留几个0。我在代码里用了一个判断if (!(result.empty() v 0))意思是跳过所有开头的0只把非零之后的内容输出。这个细节不处理输出的结果可能带着一堆没意义的0。最后是空间预估。使用vectorint存数字每位占4字节存一个10000位的大数只要40KB完全不用担心内存问题。真正要注意的是时间复杂度大数乘法的复杂度是O(n*m)写循环时别不经意写出O(n^3)。5.4 验证结果的两个小技巧写大数运算代码低频次的测试根本测不出问题。我的做法是用已知的小规模数据做交叉验证。比如5! 120power(2, 10) 1024如果这些小的都对基本说明算法骨架没错。用一个外部验证工具做大数校验。Python本身支持任意精度整数随手写pow(2, 1000)或者math.factorial(100)打印出来直接和C程序输出做对比。这种方法虽土但极有效能快速定位是大数乘法错了还是逻辑边界错了。我几乎每次写完大数代码都会这样交叉验证一遍。别嫌麻烦大数运算的边界条件多一个疏忽就要调试很久。6. 写在最后的经验之谈从13!溢出翻车那次开始我慢慢意识到数据类型从来不是一个简单的语法细节而是一种底层思维。int能表示多少、溢出后会发生什么、什么场景必须换大数方案——这些表面上看着基础的东西恰恰决定了程序能跑多远、能算多大。过去两年我陆续写过很多大数相关的练习大数加法、大数乘法、大数阶乘、大数快速幂。每次回头看旧代码都能发现更好的写法。比如一开始我用固定数组存1万位后来改成vector动态扩展一开始乘法模块和输出模块写在一起后来拆成独立的函数方便复用。如果让我给刚接触这块内容的人一个建议我会说先别急着背模板。拿张纸把竖式乘法手工算一遍观察每一位的进位是怎么产生的然后再动手写代码。这个过程只需要十分钟但对理解程序的帮助超过抄十遍代码。我以前就是太着急跳过了手算子推这一步结果在进位处理上反复出错。关于这篇草稿本身它最初确实只是我2022年6月4日随手记录的一些大数计算笔记。现在重新整理把阶乘、次方、int类型问题以及各种编译报错都串到了一起也算是对那段学习历程的一次完整复盘。如果你正被某个int溢出、大数计算或者奇怪的类型转换问题困扰希望这篇文章能帮你少走一点弯路。
返回列表