C语言中模运算的规则是什么?

23

在之前的课程中,我学到了 n % d = r 的概念,并将其理解为 n = d*q + r,其中 d 是除数,q 是商,r 是余数(请注意,余数永远不可能是负数)。

例如,-111 mod 11 的结果是 10,因为 -111 = -11*-11 + 10 (与 -111 = -11*10 -1 不同,因为那样会得到一个负余数)。

然而,当打印 -111 % 11 的结果时,结果是 -1 而不是 10。为什么?这难道不是技术上的错误吗?


2
啊,旧的负操作数模运算 :-) 我记得 Python 在所有特殊情况下都正确地(在数学意义上)计算它。 - Cameron
1
如果你想强制C语言的“%”运算符按照你期望的方式进行模运算,只需执行以下操作:((a % b) + b) % b - paddy
2
维基百科的模数操作页面上有一个非常好的表格,展示了每种编程语言的不同模数实现方式:http://en.wikipedia.org/wiki/Modulo_operation。 - Daan Timmer
一篇对你有帮助的文章:【如何在C/C++/Obj-C中编写一个可以处理负数的取模(%)运算符】(https://dev59.com/XW865IYBdhLWcg3wEaUx) - Grijesh Chauhan
1
在我看来,应该将余浩的回答标记为答案,但值得注意的是其他语言可能会有不同的处理方式。Ada有两个不同的函数:余数函数和模函数。当使用Python的模运算符时,它也会以不同的方式处理负数。 - user539810
Related - RiaD
3个回答

21

简短回答:

标准保证了 (a/b)*b + a%b 等于 a

在 C99 中,除法运算符 / 的结果向零截断。在这种情况下,% 运算符的结果是确定的,为 -1

在 C89 中,对于负操作数,除法运算符 / 可能会以任一方式截断。因此,% 运算符的结果也取决于机器。

详细回答:

根据 C99 6.5.5,

5 / 运算符的结果是第一个操作数除以第二个操作数的商;% 运算符的结果是余数。在这两个操作中,如果第二个操作数的值为零,则其行为未定义。

6 当整数被除时,/ 运算符的结果是代数商,任何小数部分都被舍去。如果商 a/b 是可表示的,则表达式 (a/b)*b + a%b 必须等于 a;否则,a/b 和 a%b 的行为未定义。

同一页上的注释说明了 / 如何工作,它说:

这通常被称为“向零截断”。

根据这个规则,-111 / 11 只能是 -10,而不是 1。由于 (a/b)*b + a%b 必须等于 a,所以我们有 -111 % 11 等于 -1

然而,《The C Programming Language》第2.5章给出了不同的答案:

对于负操作数,/ 的截断方向和 % 的结果符号取决于机器,溢出或下溢时的行为也是如此。

根据这个规则,合法结果可能是 -1 或者 10

原因在于 C89 3.3.5 中:

当整数被除以无法整除时,如果两个操作数都是正的,则 / 运算符的结果是小于代数商的最大整数,% 运算符的结果是正的。如果任一操作数为负,则 / 运算符的结果是小于代数商的最大整数或大于代数商的最小整数是实现定义的,% 运算符的结果的符号也是如此。如果商 a/b 是可表示的,则表达式 (a/b)*b + a%b 必须等于 a。

这实际上是从 C89 到 C99 的变化。

C99 的解释提供了一些历史原因:

在 C89 中,涉及负操作数的整数除法可能会以实现定义的方式向上或向下取整;其目的是避免在运行时代码中产生特殊情况的开销并强制执行特定的行为。然而,在 Fortran 中,结果将始终向零截断,开销似乎可以被数字编程社区接受。因此,C99 现在要求类似的行为,这应该有助于从 Fortran 移植代码。本文档中 §7.20.6.

numer denom quot rem
 7      3    2    1
–7      3   –2   –1
 7     –3   –2    1
–7     –3    2   –1

1
我认为只有-1是允许的 - 脚注90(C99标准n1256版本)中提到:“[...]将任何小数部分舍去后的代数商通常称为朝零截尾”,这意味着-111 / 11只能是-10,因此由于要求(a/b)*b + a%b必须等于a,所以-111 % 11只能是-1。 - Adam Rosenfield
此外,位运算符的功能也有所不同!尽管>>通常被比作2的幂次数上的模数和除法,但是>>并没有"朝零截断",因此-5 >> 1 == -3,而-11&3 == 1(而-11%4 == -3)。 - i Code 4 Food
1
是的,这在 C99 中已经改变了。C89 编译器可以执行任一操作,并且通常会执行对硬件最方便的操作。 - torek
@iCode4Food 对整数进行右移是实现定义的。 - Jim Balter

6

对于模数,-1是错误的答案。

C语言的%运算符是余数运算符而不是模数运算符,因此对于余数,10或-1都是可以接受的。


1
请定义模数的含义。根据不同的上下文,似乎有不同的定义,其中一些情况下,我认为-1是正确的。 - Rudy Velthuis
@RudyVelthuis: "模数"通常与模算术相关联。至少按照通常定义的模算术,只允许使用非负数。 - Jerry Coffin
就像我之前所说的,这取决于上下文。如果你在暗示欧几里得除法,那么余数确实总是正数。但在其他情况下,情况并非总是如此。特别是因为这是一个编程论坛,应该知道“模数”的含义可能因语言而异。 - Rudy Velthuis
@RudyVelthuis:我不同意。模数的含义就是它的含义。大多数编程语言实现的东西在某些情况下可能类似,但在其他情况下会有所不同(在大多数情况下)。 - Jerry Coffin
你能给出一个权威的模数定义吗?我认为在不同的上下文中,模数有着不同的含义。 - Rudy Velthuis

4

% 运算符的实现方式为,假设我们使用整数除法,那么 a == b * (a / b) + (a % b) 成立。

在这个例子中,-111 / 11 等于 -10,所以有 -111 == 11 * -10 + x,其中 x == -1 满足条件。


换句话说,a % b = a - (a / b) * b。由于-111 / 11的结果为-10,因此结果为-111 - (-10 * 11),即-1 - Rudy Velthuis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接