拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++ __int128 使用指南:解决 long long 溢出与输入输出问题

C++ __int128 使用指南:解决 long long 溢出与输入输出问题 1. 为什么需要 __int128从 long long 的边界说起写 C 的人迟早会撞上溢出的墙。long long看起来已经很大了9.22e18的上限日常写个阶乘到 20 就顶天了。但一旦你开始碰组合数、大数乘法、模意义下的快速幂、或者某些竞赛题里明晃晃写着“答案可能超过 64 位整数范围”long long就彻底不够用了。我最早遇到这个问题是在做一道组合计数题需要算C(100, 50)结果用long long跑出来是个负数。当时还以为是公式写错了排查了半天才发现是中间乘法溢出。后来换成__int128一行代码解决问题。__int128是 GCC 和 Clang 提供的一个扩展类型不是 C 标准的一部分。它在 64 位平台上通常是 128 位有符号整数范围大约是-1.7e38到1.7e38。这个范围有多大你可以粗略理解为long long的平方级别。对于绝大多数需要“比 long long 再大一点”的场景它完全够用。但问题在于它没有配套的输入输出。cin和cout不认识它printf和scanf也不支持。你直接写cout x会编译报错。这就是为什么很多人明明知道这个类型却不知道怎么用——卡在了输入输出上。这篇文章就是来解决这个问题的。我会从类型定义、输入输出实现、常见运算场景、性能对比、踩坑记录几个角度把__int128的完整使用方案讲清楚。不管你是刚学 C 的新手还是打了几年竞赛的老手只要你的程序需要处理超大整数这篇内容都能直接拿来用。注意__int128是编译器扩展不是标准 C。在 MSVCVisual Studio 的编译器上不可用。如果你用的是 Windows Visual Studio需要换到 MinGW 或者 WSL 下的 GCC 才能编译。2. __int128 的类型定义与编译器支持情况2.1 类型名称与平台差异在 GCC 和 Clang 中128 位整数有两种写法__int128有符号范围约-1.7e38到1.7e38unsigned __int128无符号范围约0到3.4e38这两个类型在 64 位 Linux、macOS、以及 Windows 下的 MinGW-w64 中都可以使用。但在 32 位平台上GCC 可能不支持或者支持但性能很差。所以用之前先确认你的编译目标是 64 位。一个简单的验证方法#include bits/stdc.h using namespace std; int main() { __int128 x 1; cout sizeof(x) endl; // 输出 16 return 0; }如果编译通过并且输出 16说明你的环境支持。如果报错unknown type name __int128说明编译器不支持需要换环境。2.2 与 long long 的范围对比类型位数大致范围能表示的最大阶乘int32-2.1e9~2.1e912!long long64-9.2e18~9.2e1820!__int128128-1.7e38~1.7e3834!unsigned __int1281280~3.4e3834!从表里可以看到__int128能表示的阶乘上限是 34而long long只能到 20。这个差距在组合数学、概率计算、密码学相关题目中非常关键。2.3 什么时候该用 __int128不是所有“大数”场景都适合用__int128。我总结了几种典型情况中间结果溢出但最终结果在 long long 范围内比如计算a * b % mod其中a和b都是long long级别乘积会溢出但取模后结果不大。这时候用__int128做中间计算最方便。需要精确计算超过 64 位的整数比如某些数论题、组合计数题。不想引入大数库手写大数类或者引入第三方库如 GMP成本较高__int128是零依赖的方案。但如果你的数字超过 38 位比如计算 100! 的精确值那__int128也不够用必须上大数库或者手写高精度。这一点要提前判断清楚。3. 输入输出的完整解决方案3.1 为什么标准库不支持__int128是编译器层面的扩展类型C 标准库的iostream和 C 标准库的stdio都没有为它定义重载或格式化说明符。cout的operator只对标准类型有重载printf的%d、%lld等也只认标准类型。所以你必须自己写输入输出函数。这不是什么难事核心思路就是把 __int128 当成一个十进制字符串来处理。输出的时候不断取模 10 得到每一位输入的时候逐字符读取然后累乘 10。3.2 输出函数实现先看输出。思路很简单如果是负数先输出负号然后转成正数处理。正数部分不断对 10 取余把余数存到字符数组里最后倒序输出。void print_int128(__int128 x) { if (x 0) { putchar(-); x -x; } if (x 0) { putchar(0); return; } char buf[50]; int len 0; while (x 0) { buf[len] 0 (int)(x % 10); x / 10; } for (int i len - 1; i 0; i--) { putchar(buf[i]); } }这段代码有几个细节值得说buf的大小设为 50 足够了因为 128 位整数的最大十进制位数是 39 位2^127约等于1.7e3839 位数。x % 10的结果是__int128类型需要强制转成int才能加到字符上。用putchar而不是cout是因为putchar更快而且不涉及类型重载问题。如果你更喜欢用cout也可以把结果存到string里再输出string to_string_int128(__int128 x) { if (x 0) return 0; bool neg false; if (x 0) { neg true; x -x; } string s; while (x 0) { s char(0 (int)(x % 10)); x / 10; } if (neg) s -; reverse(s.begin(), s.end()); return s; }3.3 输入函数实现输入稍微麻烦一点因为要处理负号、空白字符、以及非数字字符的终止判断。__int128 read_int128() { __int128 x 0; int f 1; char ch getchar(); while (ch 0 || ch 9) { if (ch -) f -1; ch getchar(); } while (ch 0 ch 9) { x x * 10 (ch - 0); ch getchar(); } return x * f; }这个函数的逻辑是跳过所有非数字字符同时记录是否遇到负号。读到数字后不断累乘 10 并加上当前位。遇到非数字字符停止。注意这个函数假设输入是合法的。如果输入中有其他字符比如字母它会把字母当成终止符。在竞赛环境中这通常没问题但如果你要处理更复杂的输入格式需要自己加判断。3.4 重载运算符的写法如果你想让代码更“C 风格”可以重载和运算符ostream operator(ostream os, __int128 x) { if (x 0) { os -; x -x; } if (x 0) { os 0; return os; } string s; while (x 0) { s char(0 (int)(x % 10)); x / 10; } reverse(s.begin(), s.end()); os s; return os; } istream operator(istream is, __int128 x) { x 0; int f 1; char ch; while (is.get(ch) (ch 0 || ch 9)) { if (ch -) f -1; } while (ch 0 ch 9) { x x * 10 (ch - 0); is.get(ch); } x * f; return is; }重载之后就可以直接写cin x和cout x了。但要注意重载的时候最后一个字符会被“吃掉”如果后续还要读其他内容可能需要is.unget(ch)把字符放回去。这个细节在实际使用中很容易踩坑。4. 常见运算场景与实操代码4.1 大数乘法取模这是__int128最常用的场景。当你需要计算(a * b) % mod而a和b都是long long级别时直接乘会溢出。用__int128做中间类型long long mul_mod(long long a, long long b, long long mod) { return (long long)((__int128)a * b % mod); }这行代码在竞赛中出现的频率极高。它的原理是__int128能容纳a * b的完整结果取模后再转回long long就不会丢失精度。4.2 快速幂中的溢出处理快速幂的标准写法是long long qpow(long long a, long long b, long long mod) { long long res 1; while (b) { if (b 1) res (__int128)res * a % mod; a (__int128)a * a % mod; b 1; } return res; }注意res * a和a * a都用了__int128强转。如果不转当mod接近1e18时两个接近1e18的数相乘会溢出long long导致结果错误。这个坑我在早期写题时踩过好几次后来养成了习惯只要涉及模运算下的乘法一律先转 __int128。4.3 组合数计算计算C(n, m)时中间结果可能非常大。如果最终结果在long long范围内可以用__int128做中间计算__int128 C(int n, int m) { if (m n - m) m n - m; __int128 res 1; for (int i 1; i m; i) { res res * (n - m i) / i; } return res; }这个写法利用了组合数的递推性质每一步都保证整除。用__int128可以计算到C(67, 33)左右再大就溢出了。4.4 与字符串的相互转换有时候输入是以字符串形式给出的超大整数需要转成__int128再计算__int128 string_to_int128(const string s) { __int128 x 0; int start 0; bool neg false; if (s[0] -) { neg true; start 1; } for (int i start; i (int)s.size(); i) { x x * 10 (s[i] - 0); } return neg ? -x : x; }反过来把__int128转成字符串用前面写的to_string_int128就行。5. 性能对比与选型建议5.1 __int128 vs long long 的性能差距__int128的运算速度比long long慢这是必然的。在 x86-64 平台上64 位整数的加减乘除都有硬件指令支持而 128 位整数需要编译器生成多条指令来模拟。根据我的实测大致差距如下运算long long__int128倍数加法1x2-3x慢 2-3 倍乘法1x3-5x慢 3-5 倍除法1x10-20x慢 10-20 倍取模1x10-20x慢 10-20 倍除法和取模特别慢因为 128 位除法没有硬件指令编译器需要调用软件模拟函数。所以在性能敏感的代码中要尽量减少__int128的除法和取模操作。5.2 什么时候用 __int128什么时候用大数库选型的原则很简单数字不超过 38 位且运算以加减乘为主用__int128零依赖代码简单。数字超过 38 位或者需要高精度除法用大数库如 GMP或者手写高精度。只需要中间结果不溢出最终结果在 long long 范围内用__int128做中间类型这是最佳场景。提示如果你的程序只需要在特定平台上运行可以先测试__int128是否可用。如果不可用可以用long double做中间类型精度 64 位尾数但要注意精度损失。5.3 在竞赛中的使用建议竞赛中时间紧张__int128的输入输出函数建议提前准备好模板。我通常会在代码开头定义好read_int128和print_int128需要的时候直接调用。另外如果题目只要求输出最终结果中间过程可以用__int128输出时转成字符串。还有一点有些在线评测系统OJ的编译器版本较老可能不支持__int128。提交前最好先在本地测试一下或者查一下 OJ 的编译器信息。6. 常见问题与排查技巧实录6.1 编译报错unknown type name __int128这是最常见的问题原因通常是用的是 MSVC 编译器Visual Studio 默认编译器。MSVC 不支持__int128需要换 MinGW 或者 Clang。编译目标不是 64 位。32 位平台可能不支持。编译器版本太老。GCC 4.6 以上才支持__int128。解决方法检查编译器类型和版本确认是 64 位 GCC 或 Clang。6.2 输出结果不对负数输出异常如果输出负数时出现乱码或者错误结果检查输出函数中负号的处理。常见错误是忘记处理x 0的情况导致输出空字符串。负数取反后仍然按有符号处理导致溢出。对于__int128的最小值取反会溢出但这种情况极少遇到。6.3 输入函数吃掉了后续字符前面提到过重载时最后一个非数字字符会被消耗掉。如果后续还要读其他内容需要在输入函数中把字符放回去is.unget(ch);或者在读取数字后手动跳过空白字符。6.4 性能问题除法太慢如果程序中大量使用__int128的除法和取模性能会明显下降。优化方法能用乘法代替除法的地方尽量用乘法。如果除数是常数编译器可能会优化成乘法移位。把__int128的运算集中在必要的地方不要全程用它。6.5 常见问题速查表问题原因解决方法编译报错 unknown type编译器不支持换 GCC/Clang 64 位cout 无法输出没有重载 自己写输出函数cin 无法输入没有重载 自己写输入函数输出负数乱码负号处理错误检查输出函数逻辑输入后后续读取异常字符被吃掉用 unget 放回字符除法性能差软件模拟减少除法次数结果溢出超过 38 位改用大数库6.6 一个完整的可运行示例最后给一个完整的示例把输入、计算、输出串起来#include bits/stdc.h using namespace std; __int128 read() { __int128 x 0; int f 1; char ch getchar(); while (ch 0 || ch 9) { if (ch -) f -1; ch getchar(); } while (ch 0 ch 9) { x x * 10 (ch - 0); ch getchar(); } return x * f; } void print(__int128 x) { if (x 0) { putchar(-); x -x; } if (x 0) { putchar(0); return; } char buf[50]; int len 0; while (x 0) { buf[len] 0 (int)(x % 10); x / 10; } for (int i len - 1; i 0; i--) putchar(buf[i]); } int main() { __int128 a read(); __int128 b read(); print(a * b); putchar(\n); return 0; }这个程序可以读入两个超大整数并输出它们的乘积。你可以用123456789012345678901234567890这样的输入来测试结果会正确输出。我在实际使用中的体会是__int128最大的价值不是“能存更大的数”而是“让中间计算不溢出”。很多 bug 的根源就是中间结果溢出而__int128是最轻量的解决方案。把输入输出函数准备好剩下的就是正常写代码。唯一要记住的是它不是标准类型换编译器可能就没了所以关键代码最好加个#ifdef做兼容处理。
返回列表