先问个面试高频题-7 % 3在 C 里等于多少在 Python 里又等于多少如果你脱口而出都是 1那我建议你先把这篇文章读完——因为这个问题的标准答案会让很多写了好几年代码的程序员都愣一下。我做了十几年开发见过不少线上事故和代码移植翻车现场最后定位到根因都是栽在取模和取余这两个看似“一个意思”的操作上。今天这篇我把两个概念的数学定义、各语言实现差异、互相转化的公式一次性讲透再把容易踩的坑全列出来。不管你是刚入门的学生还是被负数取模折磨过的老手这篇文章都值得收藏。1. 先搞清楚基础概念取模和取余到底是什么1.1 你以为是同一个东西其实不是先从最底层的数学关系说起。不管取模还是取余它们都脱胎于同一个带余除法的公式a q × b r其中 a 是被除数b 是除数q 是商r 是余数。这个公式本身没有争议问题出在 q 怎么取。如果 a 7b 3那 q 2r 1大家看法完全一致小学二年级教的就是这个。但一旦出现负数比如 a -7b 3事情就麻烦了。你可以说 q -2r -1因为 -7 -2 × 3 (-1)也可以说 q -3r 2因为 -7 -3 × 3 2。两个式子都严格成立但余数完全不同。所以取模和取余的分歧根源根本不在“余数怎么算”而在于“负数的除法里商怎么取整”。这是理解整个话题的钥匙商取整的方向定了余数就定了。取余Remainder用的是向零取整也就是截断除法truncated division取模Modulo用的是向下取整也就是地板除法floor division。同一套带余除法公式两种商的定义衍生出两种看起来很像、实则不同的运算。1.2 数学定义余数的符号跟着谁走教科书和 IEEE 相关规范里一般这样区分取余Remainder / %余数 r 的符号与被除数 a 相同。数学表达是 r a − trunc(a / b) × b其中 trunc 表示向零取整也就是直接把小数部分丢掉。取模Modulo余数 r 的符号与除数 b 相同。数学表达是 r a − floor(a / b) × b其中 floor 表示向下取整也就是往负无穷方向取整。用大白话说取余跟着被除数走取模跟着除数走。这句话我用了十几年比记任何公式都快。举个例子-7 取余 3被除数是 -7所以余数带负号结果是 -1而 -7 取模 3除数是 3所以余数非负结果是 2。同样一对数只是“取整方向”不同结果就差了整整一个除数的距离。这里要特别提醒一件事国内不少教材把“取模”简单等同于“取余”甚至直接在书里写“% 就是取模运算符”。但从 C 语言标准到 Java 官方文档% 的正式名称都是 remainder余数只有 Python 的 % 官方明确写的是 modulo取模。所以在工程语境里讨论这两者必须绑定具体的编程语言否则就是在鸡同鸭讲。2. 核心区别负数面前一切都现了原形2.1 两种取整方向决定两种结果当被除数和除数同号时取模和取余没有任何区别结果完全一样。只有当两者异号时分歧才会显现。这是整篇文章最重要的结论没有之一。为了让你一眼看明白我列一张对比表直接拿常见的负数和正数组合来实测运算商取整方式余数符号-7 与 37 与 -3-7 与 -3取余Remainder向零取整trunc与被除数同号-11-1取模Modulo向下取整floor与除数同号2-2-1以 -7 与 3 为例手工算一遍trunc(-7/3) trunc(-2.333…) -2余数 r -7 − (-2) × 3 -1而 floor(-7/3) floor(-2.333…) -3余数 r -7 − (-3) × 3 2。看出来了吧两个结果之间恰好差一个除数 3。7 与 -3 的组合同理取余得 1跟随被除数 7 为正取模得 -2跟随除数 -3 为负。最右侧 -7 与 -3 的组合因为被除数、除数同号两种运算结果一致都是 -1。这个表我建议你在面试前或写代码前背下来。别嫌记表格笨真正到了排查 bug 的时候能脱口而出这些结果的人定位问题的速度会快很多。2.2 一句话版本和常见误区网上很多面试题喜欢问“取模和取余的区别”我的固定回答是取余的余数跟被除数同号取模的余数跟除数同号两者在被除数和除数同号时无差别异号时差一个除数。顺带纠正一个流传很广的误区很多人以为“取模结果一定是非负的”。这个说法只有在除数为正数时才成立。如果除数是负数比如 7 取模 -3严格结果就是 -2是负数。Python 里 7 % -3 的结果也是 -2这是规范行为不是 bug。如果你业务上需要的是“经典数学意义的非负余数”那得自己先把除数归一化或者按需加上 |b| 调整别指望任何语言默认给你这个保证。2.3 除数为 0 时一切免谈不管取模还是取余除数为 0 都是未定义行为C/C或抛出异常Java、Python。这个其实不用说太多但我想强调一个容易被忽略的边界即使除数为 0 在语言层面不会崩比如某些弱类型语言里可能返回 NaN 或 0也不要依赖这种行为。任何除以 0 的操作都说明调用方的参数校验有漏洞第一时间修上游而不是在取模函数里打补丁。3. 各语言中的 % 运算符同样是百分号灵魂完全不同3.1 C/C% 是取余不是取模C 语言标准C99 及以后明确规定% 的余数符号与被除数相同。也就是说C/C 里的 % 是彻底的 remainder 语义。这是很多 C 系程序员最容易困惑的点因为几乎所有教材都把 % 翻译成“取模运算符”但实际行为和数学取模根本不是一回事。用一段最简代码验证#include cstdio int main() { std::printf(-7 %% 3 %d\n, -7 % 3); std::printf(7 %% -3 %d\n, 7 % -3); std::printf(-7 %% -3 %d\n, -7 % -3); return 0; }输出结果-7 % 3 -1 7 % -3 1 -7 % -3 -1注意第三个结果 -7 % -3 -1余数严格跟随被除数 -7 的符号。如果你在 C 里默认“取模结果就应该是非负的”第一个例子就会踩坑。我在工程里见过不少新鲜代码就是因为这一下数组下标算出了负数直接越界崩溃。3.2 Java同样叫 %同样取余但自带救赎Java 的 % 运算符和 C 系一样是 remainder 语义。但 Java 留了一个官方后门——Math.floorMod(a, b)方法实现的才是真正的取模。这个方法是 Java 8 加入的我强烈建议在涉及负数取模的业务里直接用它省心又不容易出错。public class ModDemo { public static void main(String[] args) { System.out.println(-7 % 3 (-7 % 3)); System.out.println(Math.floorMod(-7, 3) Math.floorMod(-7, 3)); System.out.println(Math.floorMod(7, -3) Math.floorMod(7, -3)); } }输出-7 % 3 -1 Math.floorMod(-7, 3) 2 Math.floorMod(7, -3) -2看到没floorMod 的结果和 Python 的 % 完全一致。写 Java 的朋友遇到负数和取模相关的逻辑第一反应应该是 floorMod而不是自己造轮子。自己写的修正函数往往只在除数为正时正确除数为负就穿帮。3.3 Python% 才是真取模但也要防着点Python 的 % 严格实现了取模语义结果符号与除数一致。这也是为什么 Python 的负索引用起来那么顺手——列表的lst[-1]直接取最后一个元素底层就是取模运算在工作。print(-7 % 3) # 2 print(7 % -3) # -2 print(-7 % -3) # -1Python 语义“更安全”的同时也埋了另一个坑你习惯了 Python 的 %再写 C 或者 JavaScript 时会把同样的逻辑直接平移过去然后得到完全不同的结果。我见过最典型的事故就是内部工具用 Python 做数据预处理到了生产环境用 C 重算两边对不上账最后查出来是 % 语义不一致。这不是语言对错问题是跨语言移植时没有对齐运算语义。3.4 JavaScript、Go、Rust、C# 的实际情况JavaScript 的 % 和 C 系一样也是 remainder 语义取整方向向零。Go 的 % 同样是 remainder。Rust 的 % 是 remainder但它提供了rem_euclid()方法来实现欧几里得取模。C# 的 % 同样是 remainder需要用Math.DivRem配合自行处理或者写个扩展方法。我把主流语言的语义整理成速查表贴代码时可以直接对着查语言运算符语义结果符号真正的取模方法C/C%remainder与被除数同号需手写Java%remainder与被除数同号Math.floorMod(a, b)Python%modulo与除数同号直接用 %JavaScript%remainder与被除数同号需手写Go%remainder与被除数同号需手写Rust%remainder与被除数同号rem_euclid()C#%remainder与被除数同号需手写注意一个事实主流语言里默认实现 remainder 语义的才是大多数Python 反而是少数派。所以永远不要默认“取模结果都是非负的”工程上完全取决于你用的是哪门语言以及你有没有主动调用取模版本的方法或函数。4. 转化公式不换语言也能得到想要的结果4.1 从取余转取模一个公式通吃假设你被困在 C 系语言里只有 remainder 语义的 %但业务需要的是取模语义最简单可靠的转换公式是mod ((a % b) b) % b这里的 % 是 C 系语言的 remainder 运算b 为正数时公式成立。原理不难理解如果余数为负加上一个除数 b就把它拉回到非负区间如果余数本来就是非负加 b 之后会溢出到 [b, 2b) 区间再模一次就还原回去了。这个公式写过一遍就忘不掉因为它逻辑自洽。不过这个公式有一个小缺点它做了两次除法运算。在性能敏感的循环里两次除法是实打实的开销。我更推荐下面这个只做一次除法的版本// 返回与除数同号的取模结果b 必须非零 inline int mod(int a, int b) { int r a % b; if (r ! 0 ((a ^ b) 0)) { r b; } return r; }这个写法的核心是异或判断(a ^ b) 0的意思是 a 和 b 符号相反。当余数非零且被除数、除数异号时余数需要加一个 b 来调整符号到与除数一致。自己验证一下a -7b 3r -1异号成立加 3 得 2a 7b -3r 1异号成立加 -3 得 -2a -7b -3同号不动还是 -1。全部正确。4.2 从取模转取余反过来怎么算如果需要反方向操作——在 Python 这种默认取模的语言里拿到向零取整的余数remainder——也有现成的套路。原理是如果取模结果为 0取余结果也是 0如果取模结果与取余结果不同号那就说明需要减去一个除数来纠正方向。def remainder_from_mod(a, b): m a % b # Python 取模结果符号跟除数走 if m 0: return 0 # m 和 a 异号时说明需要往零的方向回退一个 b if (m 0) ! (a 0): return m - b return m print(remainder_from_mod(7, 3)) # 1 print(remainder_from_mod(-7, 3)) # -1 print(remainder_from_mod(7, -3)) # 1 print(remainder_from_mod(-7, -3)) # -1说句实话工程上“从取模转取余”的需求远少于“从取余转取模”。前者多数出现在跨语言复现算法、单元测试对齐这类场景。但把它弄明白你对两种运算的理解才算闭环。4.3 从除法层面理解转化的本质取模和取余之间的转化本质上就是两种除法商的转换。记两个函数trunc_div(a, b) 是向零取整的商floor_div(a, b) 是向下取整的商。两者之间的规律极其简单当 a、b 异号且 a 不能被 b 整除时floor_div trunc_div − 1其余情况两者相等。商一旦确定余数就是 a − q × b没有任何悬念。所以如果你所在的语言连 % 都没有比如某些嵌入式环境、SQL 方言、古老的 COBOL也可以用最基本的除法和乘法组合实现取模int floor_div(int a, int b) { int q a / b; // a、b 异号且不能整除时商需要再减 1 if ((a % b ! 0) ((a 0) ! (b 0))) { --q; } return q; } int mod(int a, int b) { return a - floor_div(a, b) * b; }这套代码在任何语言里都能移植因为它只依赖最基础的 / 和 %。理解这层本质后你再看到各种取模转换公式就不会觉得是魔法而是同一套数学规律的排列组合。5. 实际应用场景什么时候必须较真5.1 循环数组与环形缓冲负数索引就是崩溃现场处理循环队列、轮询调度时常用下标对长度取模来做环形回绕。如果下标是负数比如从末尾往前数用 remainder 语义会得到负数索引数组越界直接崩溃。这个场景必须用取模语义。典型例子时间轮、TCP 序号回绕、音视频帧缓冲。假设缓冲区长度是 8现在要从位置 -1 往前找一格也就是物理上的最后一格。用 C 语言写arr[pos % 8]当 pos -1 时结果就是arr[-1]未定义行为轻则读到脏数据重则段错误。改用arr[mod(pos, 8)]就安全了因为取模结果永远落在 [0, 8) 范围内。这也是为什么 Python 的负索引用起来那么舒服——lst[-1]直接就是最后一个元素底层就是取模语义在兜底。但同样的代码逻辑搬到 C、Java、JavaScript 里稍不留神就是两类事故一类是直接崩溃一类是静默地拿到错误索引。判断技巧很朴素凡是要用运算结果做数组下标、指针偏移、分身路由一定确保结果落在 [0, length) 区间内也就是必须用取模语义不能直接拿 % 糊弄。5.2 哈希分片与一致性哈希负值路由会写错节点做分布式缓存分片时常用hash(key) % node_count来路由。在 Java 里直接写hash % node_count当 hash 为负数时会得到负数下标轻则路由错误重则数组越界。我见过不止一次线上缓存数据“诡异丢失”排查到最后发现是取余的锅——数据根本没丢是写到别的分片去了。解决方案一用 Java 的Math.floorMod(hash, nodeCount)语义清晰结果一定落在 [0, node_count) 内。解决方案二老派做法先消符号位再取模int shard (hash Integer.MAX_VALUE) % nodeCount;第二种方法用Integer.MAX_VALUE抹掉符号位牺牲了一位精度理论上对哈希分布的均匀性有极微小的影响但大部分场景感知不到。我个人的建议是优先用floorMod因为它的意图一眼就能看懂而位运算写法需要看注释才能明白。在团队协作里可读性往往比那一点点性能更重要。5.3 字体取模名字相同、含义完全不同的“取模”既然这次的热搜词里出现了“java字体取模”我必须专门说一句嵌入式、LCD 点阵屏、MiniLED 屏幕开发里说的“字体取模”跟数学上的取模运算没有半毛钱关系。那个“取模”指的是把字符点阵信息抽取成二进制数据——比如把汉字“中”的 16×16 点阵按行扫描成 32 个字节每个 bit 表示一个像素是否点亮。Java 开发者做串口屏、打印上位机时经常碰到的“字模”就是这个东西。如果你搜“java字体取模”是想要一套把字形转成字节数组的方案思路一般是四步加载字体获取字形轮廓、栅格化到 BufferedImage、按阈值抽取像素、按位打包成字节数组。这套流程里的“取模”是“提取字模”的缩写和本文讲的数学取模是两个完全不同的语境。看到这个词先确认上下文别搞混了。5.4 判断奇偶与整除性写错一个条件负奇数全翻车判断一个数能否被另一个数整除用a % b 0在取余和取模语义下结果一致因为整除时余数一定为 0没有符号问题不需要区分。但判断奇偶就有坑。在 C 系语言里if (a % 2 1) // 对正奇数成立对负奇数不成立-7 % 2 在 C 里等于 -1不等于 1条件判断失败。正确的写法是a % 2 ! 0或者用位运算(a 1) 1——二进制最后一位不受符号影响正负奇数都能正确识别。Python 里没有这个问题-7 % 2 1因为除数是正 2取模结果自动落回 [0, 2) 区间。同样的需求两种语言两种写法跨语言复制代码时最容易在这里翻车。5.5 加密算法与随机数底层实现必须严格取模RSA 等公钥算法大量使用模运算其中负数的处理非常敏感。密码学实现里几乎都会要求严格的数学取模也就是非负结果。OpenSSL、GMP 这类大数库内部都是用 floor 语义或者先把操作数归一化再运算。自己写密码学相关代码时千万不要直接拿 C 的 % 上否则可能在密钥生成阶段就埋下错误。同理生成半开区间随机数 [0, n) 时如果源随机数是带符号的 int 且可能为负先取模再取余的老路容易产生偏置。最好统一走 floorMod或者先清除符号位。这个问题在实现一致性哈希、AB 测试分流、抽奖算法时都很常见属于那种平时不显眼、出问题就要查半天的经典隐雷。6. 常见问题与踩坑实录6.1 为什么 Python 里正常C 里就翻车这是整个话题里最典型的移植事故。场景通常是用 Python 做算法原型时负索引用得飞起一切正常把同样的逻辑用 C 重写突然数组越界或者数据错位。原因就是第 3 节说的语言差异——Python % 是取模C % 是取余。排查时不要只盯着业务逻辑先全局搜索所有 % 运算检查是否涉及负数操作数。如果是统一替换为取模函数或者把负数操作数预先归一化到非负。我在 C 工程里习惯放一个公共工具函数就是第 4.1 节那个mod(int a, int b)所有需要环形回绕、数组下标的场景一律走它不放行裸写%。6.2 负数除以负数取模结果到底是正是负很多人只记住了“取模非负”结果一看 7 % -3 在 Python 里输出 -2当场懵住。这里再强调一遍取模结果的符号跟除数走除数是负数时结果就是负数。想要经典数学意义上的“非负余数”请先把除数归一化或者按需加 |b| 调整。这是规范行为不是语言 bug。面试时如果被问到顺手答出这一点比背十个公式都加分。6.3 位运算优化 的陷阱编译器优化里非负数的x % 8经常被优化成x 7。对非负 x两者完全等价。但 x 为负数时事情变得很微妙x % 8的结果是负数符号跟随被除数x 7的结果也是负数补码的符号位会被保留但两者的数值往往不一样。比如在 C 系语言里-7 % 8 -7而 -7 7 1。差得天南地北。这个坑在底层网络协议解析、位图处理时特别容易踩。如果你用 (n - 1)替代% n来做快速取模一定要先确认操作数不可能为负。否则性能优化没优化出问题反而优化出了 bug。6.4 转化公式中的溢出风险(a % b b) % b这个公式在 a 的绝对值极大时存在溢出风险。比如 a 接近 INT_MIN先加 b 再取模中间结果理论上没问题但在某些语言整数溢出回绕的语义下中间结果可能变成一个奇怪的数最终结果自然不对。虽然多数业务数据不会触发但保险起见建议用第 4.1 节那个带 if 判断的版本——它没有额外的加法溢出路径因为 r 与 b 异号时的 r b绝对值一定小于 max(|r|, |b|) 的两倍在正常业务数据范围内不会溢出。写库代码、框架代码时这一点尤其重要因为你无法预测调用方会传什么极值进来。6.5 浮点数的 % 别乱用C 的fmod、Java 的%对浮点数也有定义但浮点数取余在业务里意义有限尤其是在判断“能否整除”时浮点精度会让a % b 0变得完全不可靠。比如 0.3 % 0.1 在浮点下不是 0而是一个接近 0 的微小值。如果你在业务里真需要浮点取整余数先想想是不是数据建模本身有问题必要时用 BigDecimal 或者把数放大成整数再运算。千万别指望浮点取余给你精确结果这在任何语言里都是不可能的。最后说个我自己的习惯凡是涉及除法、取余、取模的代码动笔之前先问自己一句“这里允许负数吗”这个问题一出来一半的 bug 从源头就消失了。剩下的那一半就是这篇文章里写的各种语言差异和边界条件了。收藏起来写代码前翻一遍能帮你少加不少夜班。