对编程中的取模运算的一些发现

看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)
那么如果a是负数,python会得到1,但是c++会得到-1。

在忽视了负数除数或者被除数可能是负数的情况下,如果不了解取模运算在编程中会有不同定义的话,就会出错了。