为什么取模运算符(%)只适用于整数?

92

我最近遇到了一个问题,可以通过取模除法轻松解决,但输入是浮点数:

给定一个周期性函数(例如 sin)和一个只能在周期范围内计算它的计算机函数(例如 [-π, π]),创建一个可以处理任何输入的函数。

"显而易见"的解决方案类似于:

#include <cmath>

float sin(float x){
    return limited_sin((x + M_PI) % (2 *M_PI) - M_PI);
}

为什么这不起作用?我收到了这个错误:

error: invalid operands of types double and double to binary operator %

有趣的是,在Python中它确实能工作:

def sin(x):
    return limited_sin((x + math.pi) % (2 * math.pi) - math.pi)

20
π不等于3.14,事实上它不能表示为任何浮点数类型。对于大的x值计算sin(x)实际上需要进行一个非常困难的超越论参数化过程,它不能通过有限近似π来完成。 - R.. GitHub STOP HELPING ICE
3
这几乎肯定是一份作业,所以浮点数误差要么超出了作业范围,要么意味着需要进行更严格的数值分析讨论。无论哪种情况下,fmod 函数很可能是教师想要的内容。 - Dennis Zickefoose
2
这不是作业,只是在阅读另一个SO问题时遇到的一些事情(https://dev59.com/wG025IYBdhLWcg3wSkAq#6091846)。 - Brendan Long
2
好的,我在陈述时应该更加精确。我的观点是,如果参数可以无限增长(不仅仅是双精度指数大小),那么没有任何有限的 pi 近似值是足够的。对于双精度来说,是的,一个非常非常长的 pi 近似值就足够了。 - R.. GitHub STOP HELPING ICE
1
@aschepler:我认为你没有理解问题。 - R.. GitHub STOP HELPING ICE
显示剩余4条评论
9个回答

86

由于正常数学概念的"余数"只适用于整数除法,即需要产生整数商的除法。

为了将"余数"的概念扩展到实数上,您必须引入一种新的"混合"操作,该操作将生成实数操作数的整数商。Core C语言不支持此类操作,但它作为标准库提供了fmod函数,以及C99中的remainder函数。(请注意,这些函数不相同,并且具有某些特殊性质。 特别地,它们不遵循整数除法的舍入规则。)


9
根据98标准中对%的定义,可知 "(a/b)*b + a%b" 等于 "a"。对于浮点类型,(a/b)*b 已经等于 a [就浮点类型而言],因此 a%b 往往不是非常有用。 - Dennis Zickefoose
1
@Dennis:确实,在一个域中,余数总是0。我认为对于浮点数,%运算符最合适的定义是a-(a/b)*b,这将是0或非常小的值。 - R.. GitHub STOP HELPING ICE
7
您可以通过要求“floor(a/b)*b + a%b = a”来轻松修复此公式。请注意,对于整数而言,floor(a/b) = a/b。 - vog
C风格的整数除法使用截断而不是向下取整,但核心问题仍然存在。 - dan04
30
关于 "余数" 的正常数学概念仅适用于整数除法的说法,并不完全正确。在模算术的数学概念中,也适用于浮点数值。这是唐纳德·库努斯在他的经典著作《计算机程序设计艺术》(第一卷)中讨论的首要问题之一。也就是说,这曾经是基本知识。但今天的学生未能获得他们应得的教育,我个人认为。 - Cheers and hth. - Alf

60

您要查找的是fmod()函数。

我猜为了更具体地回答您的问题,在旧语言中,%运算符仅被定义为整数模除运算符,而在新语言中,他们决定扩展运算符的定义。

编辑:如果我要猜测原因,我会说这是因为模算术的概念源于数论,并且专门涉及整数。


11
“老语言”- APL可以追溯到1960年代,其模运算符“|”适用于整数和浮点数据(也适用于标量、向量、矩阵、张量等)。如果C的“%”模运算符在处理浮点数时使用相同的功能,它也可以像fmod一样工作。 - rcgldr
@rcgldr的设计目标并没有要求浮点数取模。C语言的实现是为了编译Unix,并减少操作系统所需汇编语言的数量。"C是一种命令式过程语言。它的设计是使用相对简单的编译器进行编译,提供对内存的低级访问,提供能够高效映射到机器指令的语言结构,并且需要最小的运行时支持。" https://zh.wikipedia.org/wiki/C%E8%AF%AD%E8%A8%80 - harper
1
@harper - 由于C语言包括浮点数算术运算,如加、减、乘和除,使用与整数相同的语法,我不明白为什么它不能使用相同的语法(%)来包括模运算。是否包括它似乎是任意的选择。 - rcgldr

18

我不能确定,但我猜这主要是历史遗留问题。早期的许多C编译器根本不支持浮点数。后来才添加了它,但即使是后来添加的也没有完全支持——主要是添加了数据类型和语言中支持的最基本操作,但其他所有的操作都留给了标准库。


1
在我阅读列表时,这是我看到的第一个合理的答案,给你点赞。实际上,现在我已经把它们全部阅读完了,这是唯一一个合理的答案。 - Cheers and hth. - Alf
我也来晚了,但还是要点个赞。我曾经为6809和Z80嵌入式系统编写C语言代码。我无法承受包含C运行时库所需的空间开销,甚至不得不编写自己的启动代码。浮点运算是我负担不起的奢侈品 :) - Richard Hodges

12

C和C++中的取模运算符%是针对两个整数定义的,但是有一个可用于双精度浮点数的fmod()函数。


5
这是回答OP问题的答案,但忽略了OP试图做的根本问题:对于大的x值,sin(fmod(x,3.14))甚至sin(fmod(x,M_PI))并不等于sin(x)。实际上这些值可能相差多达2.0。 - R.. GitHub STOP HELPING ICE
2
@R..:没错,但那是一个不同的问题,我并不完全确定是否有一个被接受的答案,尽管这个主题已经有很多研究。 (http://scholar.google.com/scholar?hl=en&q=The+evaluation+of+periodic+functions+with+large+input+arguments&btnG=Search&as_sdt=1%2C21&as_ylo=&as_vis=0) - Mark Elliot
@R - 我修正了方程使其正确。实际的方程并不是重点(一旦我有了测试用的函数,它就相当容易找出来了)。 - Brendan Long
% 不是模数运算符,而是余数运算符吗? - chux - Reinstate Monica

7
约束条件在标准中规定:
C11(ISO/IEC 9899:201x)§6.5.5 乘法运算符

每个操作数都必须具有算术类型。%运算符的操作数应具有整数类型。

C++11(ISO/IEC 14882:2011)§5.6 乘法运算符

*和/的操作数应具有算术或枚举类型;%的操作数应具有整数或枚举类型。对操作数执行通常的算术转换,并确定结果的类型。

解决方案是使用fmod,这正是为什么首先将%的操作数限制为整数类型的原因,根据C99 Rationale §6.5.5 乘法运算符

C89委员会拒绝将%运算符扩展到浮点类型上工作,因为这样的用法将重复fmod提供的功能。


2

在C++中,当你试图找到两个FloatDouble类型的数的余数时,%运算符无法工作。

因此,你可以尝试使用math.h / cmath.h中的fmod函数,或者使用以下代码来避免使用该头文件:

float sin(float x) {
    float temp;
    temp = (x + M_PI) / ((2 *M_PI) - M_PI);
    return limited_sin((x + M_PI) - ((2 *M_PI) - M_PI) * temp));
}

2
尝试使用fmod

2

"模数运算的数学概念同样适用于浮点数,这是唐纳德·克努斯在他经典著作《计算机程序设计艺术》(第一卷)中讨论的最初问题之一。也就是说,它曾经是基本知识。"

浮点数取模运算符定义如下:

m = num - iquot*den ; where iquot = int( num/den )

如所示,浮点数上的%运算符的无操作似乎与标准有关。CRTL提供“fmod”,通常也提供“remainder”来对fp数字执行%。这两者之间的区别在于它们处理中间“iquot”舍入的方式。

“remainder”使用四舍五入,“fmod”使用简单截断。

如果您编写自己的C ++数值类,则可以通过包括重载运算符%来修改无操作遗留问题。

此致敬意


一个稍微更好的定义是使用数学值 floor(num/den+0.5)。这样的定义总是会产生一个精确可表示的结果,而 Knuth 的定义则不会对例如 (1E-37, 1.0) 进行精确计算,因为 Knuth 定义的数学精确结果将是 (1E-37 - 1.0)。 - supercat
从实际角度来看,我认为Knuth先生的定义可能基本上是有用的。以这种方式定义运算符,使其永远不需要舍入,可能在美学上是令人愉悦的,但它并不像说两个相差某个分母的确切倍数的数字应该属于同一个模等价类那样重要。事实上,在(x+den)-den和(x-den)+den属于彼此相同的等价类,但不属于x的情况下,尽管缺乏舍入可能在美学上是令人愉悦的,但将x放入该类中可能更好。 - supercat
顺便问一下,你认为采用“舍入模式”的概念比采用独立的“加法、对结果取上限”和“加法、对结果取下限”操作更好吗?在某些情况下需要编写的代码会以不同的模式执行,这样有多大的用处呢?例如,如果一个程序执行 x = 16.0; y=0.1; z=x+y;,那么将后面的语句替换为 z=1.1; 可以提高运行效率,但是如果实现支持舍入模式,则无法这样做。 - supercat
如果一个函数执行多个FPU操作(这将是大多数执行任何操作的函数的情况),全局设置一个将应用于所有操作的FPU模式有多有用?我认为,如果想要计算a*b-c*d的最小可能值,那么应该将a*b向-inf舍入,将c*d向+inf舍入。将所有内容都向同一方向舍入会产生无意义的结果。 - supercat
@supercat 哦,顺便说一下... int( 1E-37 / 1.0 ) 是0,而不是1,因此Knuth的定义会得到1E-37。 最好的问候 - Love
显示剩余4条评论

1

对于 C/C++,这仅适用于整数运算。

Python 更加广泛,允许您获取浮点数的余数,即数字可以被除以多少次的余数:

>>> 4 % math.pi
0.85840734641020688
>>> 4 - math.pi
0.85840734641020688
>>> 

3
余数并不是“模数的另一个名称”!请参见:https://dev59.com/4GYr5IYBdhLWcg3wb5rc 或从数学角度来看:http://math.stackexchange.com/questions/801962/difference-between-modulus-and-remainder 简而言之,模和余数只有在正数情况下才相同。另一个例子是余数无法让你绕着罗盘(逆时针)转。请更正,因为我太吝啬了,不想点踩 :P - GitaarLAB

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