「取模」與「取餘」在數學定義、程式語言實作、硬體指令層面均有差異。混淆兩者會導致負數運算邏輯錯誤、雜湊分佈不均、環狀緩衝區索引越界等難以察覺的 Bug。本文從數學定義出發,對比主流語言行為,給出跨語言移植的安全守則。
給定整數 a (被除數)、b ≠ 0 (除數),存在唯一整數商 q 與餘數 r 滿足:
a = b * q + r
差異在於 商 q 的取整方向:
| 運算類型 | 商 q 取整規則 | 餘數 r 範圍 | 數學符號 |
|---|---|---|---|
| 取餘 | 向零截斷 (Truncation, fix()) | |r| < |b|,sign(r) = sign(a) | a rem b |
| 取模 | 向下取整 (Floor, floor()) | 0 ≤ r < |b| (b>0),sign(r) = sign(b) | a mod b |
a = -8, b = 3
真商 = -2.666...
取餘: q = trunc(-2.666) = -2
r = a - b*q = -8 - 3*(-2) = -2 (sign 同 a)
取模: q = floor(-2.666) = -3
r = a - b*q = -8 - 3*(-3) = 1 (sign 同 b,且 0 ≤ r < 3)
| 語言 | 運算子 | 語意 | -8 % 3 | 8 % -3 | -8 % -3 | 備註 |
|---|---|---|---|---|---|---|
| C (C99+) | % | 取餘 | -2 | 2 | -2 | C89 未定義負數行為 |
| C++ (C++11+) | % | 取餘 | -2 | 2 | -2 | 同 C |
| Java | % | 取餘 | -2 | 2 | -2 | Math.floorMod 為取模 |
| C# | % | 取餘 | -2 | 2 | -2 | Math.IEEERemainder 不同 |
| Python | % | 取模 | 1 | -2 | -2 | divmod 回傳 (q, r) 取模語意 |
| JavaScript | % | 取餘 | -2 | 2 | -2 | ES2021 新增 Math.rem |
| Go | % | 取餘 | -2 | 2 | -2 | |
| Rust | % | 取餘 | -2 | 2 | -2 | .rem_euclid() 為取模 |
| Ruby | % | 取模 | 1 | -2 | -2 | |
| PHP | % | 取餘 | -2 | 2 | -2 | fmod 同取餘 |
| Swift | % | 取餘 | -2 | 2 | -2 | |
| SQL (標準) | MOD() | 取模 | 1 | -2 | -2 | 各資料庫實作略有差異 |
b (非負);取餘結果號同 a。b > 0),極適合陣列索引、雜湊桶位、環狀緩衝區:index = ((key % N) + N) % N (取餘語言) vs index = key % N (取模語言)。// C/C++/Java/C#/JS/Go: 取模 (非負結果)
int mod(int a, int b) { int r = a % b; return r < 0 ? r + (b > 0 ? b : -b) : r; }
// Python/Ruby: 取餘 (同號被除數)
int rem(int a, int b) { int r = a % b; return (a < 0) != (b < 0) && r != 0 ? r - b : r; }
(-8,3), (8,-3), (-8,-3), (0,5), (5,0)。mod()。IDIV 指令產生的餘數符號同被除數 (取餘語意)。IDIV 後插入修正指令 (測試餘數符號、條件加減除數)。取餘 = 向零截斷商,結果號同被除數 (C/C++/Java/C#/JS/Go 默認)。
取模 = 向下取整商,結果號同除數 (非負) (Python/Ruby/SQL 默認)。
遇到負數運算,**務必查閱該語言規格**,或封裝統一語意函式,切勿憑直覺假設行為。