参考:Andrew Koenig《C 陷阱与缺陷(第二版)》7.7节
目录
三条性质
三条不可能同时成立
C 到底保证了什么
例子:哈希表下标
实测:C 给的是哪一套
同一个式子,Python 给的是相反的答案
Koenig 在 7.7 节一上来没写实现,先问了一个问题:
「假定我们让 a 除以 b,商为 q,余数为 r。……我们希望 a、b、q、r 之间维持怎样的关系呢?」
然后列了三条。
三条性质
q*b + r == a——「最重要的一点……因为这是定义余数的关系」
这条好理解,这就是关于余数的反运算
改变 a 的正负号,q 的符号跟着变,绝对值不变
这是什么意思呢?比如:
q = 5 / 2 = 2;
r = 5 % 2 = 1;
此时改变被除数的符号: 5 -> -5 就有:
q = -5 / 2 = -2;
r = -5 % 2 = -1;
此时也满足第一条:
(-2)*2 + (-1) = -5;
但这样就会与第三条冲突:
b>0 时,保证
r>=0且r<b——「如果余数用于哈希表的索引,确保它是一个有效的索引值很重要」
还是以 -5 举例,第三条应该这么运算:
q = -5 / 2 = -3;
r = -5 % 2 = 1;
这样也满足第一条:
(-3)*2 + 1 = -5;
但这样算与第二条就产生冲突了
第 3 条不是凑数的。做哈希表下标,余数必须是正数——这一点后面会用上。
三条不可能同时成立
书用3/2推导的:
3/2:商 1,余 1 —— 性质 1 满足,没问题。
(-3)/2呢?- 先满足性质 2 → 商
-1,那余数必须是-1→ 性质 3 破了(余数是负的)。
- 先满足性质 3 → 余数
1→ 由性质 1 反推商是-2→ 性质 2 破了。
- 先满足性质 2 → 商
两条路,必破一条。
书里的话:
因此,C 语言或者其他语言在实现整数除法截断运算时,必须放弃上述三条原则中的至少一条。大多数程序设计语言选择了放弃第 3 条,而改为要求余数与被除数的正负号相同。
注意这句:「大多数程序设计语言选择了放弃第 3 条」。有 "大多数",就说明有例外。
C 到底保证了什么
然而,C 语言的定义只保证了性质 1,以及当
a>=0且b>0时,保证|r|<b以及r>=0。后面部分的保证与性质 2 或者性质 3 比较起来,限制性要弱得多。
性质 1 是标准钉死的;性质 2 和性质 3 是"编译器实际上这么干",不是"标准要求这么干"。
(补一条后来发生的事:C99(1999 年)起,标准明文规定整数除法向零截断——"代数商去掉小数部分"。性质 2 从此由标准兜底。但性质 3 到今天也没人保证。)
例子:哈希表下标
书设了一个场景:n是标识符算出来的结果,要通过除法得到哈希表项h,满足0<=h<HASHSIZE。
如果已知n恒为非负,一行就够:
h = n % HASHSIZE;如果n可能为负:
h = n % HASHSIZE; if (h < 0) h += HASHSIZE;书最后还给了一条更省事的建议:「更好的做法是,程序在设计时就应避免 n 的值为负这样的情形,并且声明 n 为无符号数。」
到这里书讲完了。总结就是:C 放弃了"余数永远为正"这条,改用"余数跟被除数同号"。
实测:C 给的是哪一套
#include <stdio.h> int main(void) { printf("-7 / 2 = %d\n", -7 / 2); printf("-7 %% 2 = %d\n", -7 % 2); printf(" 7 / -2 = %d\n", 7 / -2); printf(" 7 %% -2 = %d\n", 7 % -2); printf("-7 / -2 = %d\n", -7 / -2); printf("-7 %% -2 = %d\n", -7 % -2); return 0; }-7 / 2 = -3 -7 % 2 = -1 7 / -2 = -3 7 % -2 = 1 -7 / -2 = 3 -7 % -2 = -1两条规律,跟书对得上:
商向零截断:
-7/2给-3,不是-4。这就是性质 2——符号变了,绝对值没变。
余数跟被除数同号
:-7 % 2 = -1(跟 -7 的负号)、7 % -2 = 1(跟 7 的正号)。这就是书说的"大多数 C 编译器在实践中的做法"。
同一个式子,Python 给的是相反的答案
把同样几组数丢给 Python:
(Python 的/是浮点除,所以对照要用整除//)
算式 | C 的 | Python 的 | C 的 | Python 的 |
| -3 | -4 | -1 | 1 |
| -3 | -4 | 1 | -1 |
| 3 | 3 | -1 | -1 |
前两行全部不一样,而且余数的符号正好相反。
规律也清楚了:
- C:余数的符号跟被除数
- Python:余数的符号跟除数(
7 % -2给-1,跟的是 -2)
回到第一章那三条性质——C 挑的是性质 2,Python 挑的是性质 3。
书说"大多数程序设计语言选择了放弃第 3 条",Python 就是那个没放弃的。
所以谁算错了?都没错。
标准没规定的地方,两种选择都合法。
书那句"大多数 C 编译器在实践中也都是这样做的"——"实践中"三个字,就是在说标准不管这事。