对编程中的取模运算的一些发现
看c++ primer的时候看到赋值溢出会取模,-1赋给unsigned char得到255(第五版中文p33),让我对负数取模的运算产生了疑问。
问题
于是又翻了下英文书(p35),意思是一样的:
If we assign an out-of-range value to an object of unsigned type, the result is the remainder of the value modulo the number of values the target type can hold. For example, an 8-bit unsigned char can hold values from 0 through 255, inclusive. If we assign a value outside this range, the compiler assigns the remainder of that value modulo 256. Therefore, assigning –1 to an 8-bit unsigned char gives that object the value 255.
马上用C++试了下,-1 % 256
,结果是-1
。是C++有问题还是我理解有问题?正准备手算一下,突然了新问题,负数的取模运算该怎么算?
解答
上一次正经学相关内容大概要追溯到本科学离散数学的时候了,已经记不得什么了,不如搜一下。也是发现stackoverflow上有类似的疑问,而且有了新发现,那就是Python的取模运算结果和C++不一样。
维基百科里给出了不同语言在取模运算上不同的定义。
根据百科对模的定义,“modulo operation returns the remainder or signed remainder of a division”,实际上取余取模其实是一回事。并且运算使用的是欧几里得除法:
a和b是整数,存在唯一的q和r,满足: \[ \begin{matrix} a = bq+r \\ 0 \leq r \lt |b| \\ b \neq 0 \end{matrix} \]
从定义里就能看出,余数是非负的。但是在编程语言里,并不一定满足\(r \geq 0\),当\(a\)和\(b\)有一个是负数的情况下,不同编程语言就有不同的处理。而且在某个版本的C++之前,余数的符号还是由C++的实现决定的(也就是说用不同的编译器可能会有不同的结果)。
目前C++的取模运算使用truncated定义,也就是商舍去小数部分,向0取整。例如-0.5变成0,-1.5会变成-1, 1.5变成1,0.5变成0。
这也就解释了为什么-1\%256 = -1
。首先-1/256
商是0
,然后r = a-bq = -1 - 256*0 = -1
。
而Python使用的是Floored定义,商向下取整。例如-1.5变成-2,-0.5变成-1,1.5变成1,0.5变成0。 在这种情况下\(q = \lfloor -1/256 \rfloor = -1\),那么\(r = -1 - 256 * (-1) = 255\)。
其他
C++的定义
我还查了下cppreference,对%(remainder operator)的定义是(只节选了相关部分):
lhs % rhs
The result of built-in remainder is the remainder of the integer division of lhs by rhs. If rhs is zero, the behavior is undefined.
If a / b is representable in the result type, (a / b) * b + a % b == a.
Note: Until CWG issue 614 was resolved (N2757), if one or both operands to binary operator % were negative, the sign of the remainder was implementation-defined, as it depends on the rounding direction of integer division. The function std::div provided well-defined behavior in that case.
而 integer division:
lhs / rhs
The result of built-in division is lhs divided by rhs. If rhs is zero, the behavior is undefined.
If both operands have an integral type, the result is the algebraic quotient (performs integer division): the quotient is truncated towards zero (fractional part is discarded).
商向0取整,与维基百科的内容相符。它也是说了在CWG issue 614被解决之前C++的余数符号是由实现决定的,不过现在已经统一了。
编程中取模的陷阱
维基百科里的例子就很好,判断一个整数是否是奇数可能会写这样的代码
1
if(a%2 == 1)
在忽视了负数除数或者被除数可能是负数的情况下,如果不了解取模运算在编程中会有不同定义的话,就会出错了。