-7 / 2 在 C 里是 -3,在 Python 里是 -4,谁算错了?

参考:Andrew Koenig《C 陷阱与缺陷(第二版)》7.7节

目录

三条性质

三条不可能同时成立

[C 到底保证了什么](#C 到底保证了什么)

例子:哈希表下标

[实测:C 给的是哪一套](#实测:C 给的是哪一套)

[同一个式子,Python 给的是相反的答案](#同一个式子,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 破了。

两条路,必破一条。

书里的话:

因此,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 恒为非负,一行就够:

cpp 复制代码
h = n % HASHSIZE;

如果 n 可能为负:

cpp 复制代码
h = n % HASHSIZE;
if (h < 0)
    h += HASHSIZE;

书最后还给了一条更省事的建议:「更好的做法是,程序在设计时就应避免 n 的值为负这样的情形,并且声明 n 为无符号数。」

到这里书讲完了。总结就是:C 放弃了"余数永远为正"这条,改用"余数跟被除数同号"。


实测:C 给的是哪一套

cpp 复制代码
#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;
}
cpp 复制代码
-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 的 % |
| -7 ÷ 2 | -3 | -4 | -1 | 1 |
| 7 ÷ -2 | -3 | -4 | 1 | -1 |
| -7 ÷ -2 | 3 | 3 | -1 | -1 |

前两行全部不一样,而且余数的符号正好相反。

规律也清楚了:

  • C :余数的符号跟被除数
  • Python :余数的符号跟除数 (7 % -2 给 -1,跟的是 -2)

回到第一章那三条性质------C 挑的是性质 2,Python 挑的是性质 3。

书说"大多数程序设计语言选择了放弃第 3 条",Python 就是那个没放弃的。

所以谁算错了?都没错。

标准没规定的地方,两种选择都合法。

书那句"大多数 C 编译器在实践中也都是这样做的"------"实践中"三个字,就是在说标准不管这事。


相关推荐
俊俊几年的4 小时前
动态规划(Dynamic Programming)详解:从入门到 LeetCode Hot 100
linux·c++·算法·leetcode·嵌入式·动态规划·c
§ꦿCFོ༉14 小时前
嵌入式八股文—C语言入门学习(四)
c语言·开发语言·学习
2603_9696948516 小时前
【JavaWeb学习记录】MyBatis核心知识点与实战笔记:从入门到动态SQL
笔记·sql·学习·mysql·mybatis·javaweb·学习方法
用户37215742613517 小时前
C# 提取 PDF 附件:包括文档附件和页面注释附件
c#
寂寞的船18 小时前
【Unity Bug挑战】第12期 Awaitable 二次 await 死锁之谜 —— 为什么同一个异步操作等两次就卡死?
unity·c#·游戏引擎·bug
北京云帆互联科技1 天前
云帆培训考试系统更新说明:v9.3.0
java·学习
老歌老听老掉牙1 天前
案例 7:二维 Rosenbrock 函数最小值(香蕉函数)
c++·pso
Severus_black1 天前
【C++初阶】Stack、Queue算法题大练兵!
c语言·c++·算法·stl
sxlishaobin1 天前
Maven Setting详解
java·学习·maven
xiaoye-duck1 天前
《C++ 项目:手写大模型接入 SDK》DataManager数据管理模块:SQLite持久化存储
开发语言·数据库·c++·sqlite