
参考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 2r 5 % 2 1;此时改变被除数的符号 5 - -5 就有q -5 / 2 -2r -5 % 2 -1;此时也满足第一条-2*2 -1 -5但这样就会与第三条冲突b0 时保证r0且rb——「如果余数用于哈希表的索引确保它是一个有效的索引值很重要」还是以 -5 举例第三条应该这么运算q -5 / 2 -3r -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 破了。两条路必破一条。书里的话因此C 语言或者其他语言在实现整数除法截断运算时必须放弃上述三条原则中的至少一条。大多数程序设计语言选择了放弃第 3 条而改为要求余数与被除数的正负号相同。注意这句「大多数程序设计语言选择了放弃第 3 条」。有 大多数就说明有例外。C 到底保证了什么然而C 语言的定义只保证了性质 1以及当a0且b0时保证|r|b以及r0。后面部分的保证与性质 2 或者性质 3 比较起来限制性要弱得多。性质 1 是标准钉死的性质 2 和性质 3 是编译器实际上这么干不是标准要求这么干。补一条后来发生的事C991999 年起标准明文规定整数除法向零截断——代数商去掉小数部分。性质 2 从此由标准兜底。但性质 3 到今天也没人保证。例子哈希表下标书设了一个场景n是标识符算出来的结果要通过除法得到哈希表项h满足0hHASHSIZE。如果已知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 给的是相反的答案把同样几组数丢给 PythonPython 的/是浮点除所以对照要用整除//算式C 的/Python 的//C 的%Python 的%-7 ÷ 2-3-4-117 ÷ -2-3-41-1-7 ÷ -233-1-1前两行全部不一样而且余数的符号正好相反。规律也清楚了C余数的符号跟被除数Python余数的符号跟除数7 % -2给-1跟的是 -2回到第一章那三条性质——C 挑的是性质 2Python 挑的是性质 3。书说大多数程序设计语言选择了放弃第 3 条Python 就是那个没放弃的。所以谁算错了都没错。标准没规定的地方两种选择都合法。书那句大多数 C 编译器在实践中也都是这样做的——实践中三个字就是在说标准不管这事。