C++ 模数需要两个 *un*signed 字节之间的减法运算才能工作,为什么?
C++ modulus requires cast of subtraction between two *un*signed bytes to work, why?
以下Arduino (C++)代码
void setup()
{
Serial.begin(115200);
byte b1 = 12;
byte b2 = 5;
const byte RING_BUFFER_SIZE = 64;
byte diff = b2 - b1;
byte diff2 = (byte)(b2 - b1) % RING_BUFFER_SIZE; //<---NOTE HOW THE (byte) CAST IS *REQUIRED* TO GET THE RIGHT RESULT!!!!
Serial.println(b1);
Serial.println(b2);
Serial.println(RING_BUFFER_SIZE);
Serial.println(diff);
Serial.println(diff2);
}
void loop()
{
}
产生预期的:
12
5
64
249
57 //<--correct answer
而没有此处所示的“(byte)”转换:
void setup()
{
Serial.begin(115200);
byte b1 = 12;
byte b2 = 5;
const byte RING_BUFFER_SIZE = 64;
byte diff = b2 - b1;
byte diff2 = (b2 - b1) % RING_BUFFER_SIZE; //<---(byte) cast removed
Serial.println(b1);
Serial.println(b2);
Serial.println(RING_BUFFER_SIZE);
Serial.println(diff);
Serial.println(diff2);
}
void loop()
{
}
它产生:
12
5
64
249
249 //<--wrong answer
为什么不同?为什么模运算符仅适用于显式转换?
注:"byte" = "uint8_t"
5 - 12
给出 -7
(int
)。所以你的代码确实 -7 % 64
。
从数学上讲,我们预计这会给出 57
。但是,在 C 和 C++ 中,%
对于负数,它不会按照数学上的预期执行。相反,它满足以下等式:
(a/b) * b + a%b == a
现在,(-7)/64
给出了 0
,因为 C 和 C++ 使用向零截断来进行正负整数除法。因此 -7 % 64
的计算结果为 -7
.
最后,将 -7
转换为 uint8_t
得到 249
。
当您写 (byte)-7 % 64
时,您实际上是在 249 % 64
给出预期的答案。
关于 b2 - b1
的行为:所有整数运算至少以 int
精度进行;对于 -
的每个操作数,如果它是比 int
更窄的整数类型,它首先被提升为 int
(保持值不变)。如果在此升级后类型不同(在这种情况下它们不会),则可能会发生进一步的转换。
在代码中,b2 - b1
表示 (int)b2 - (int)b1
产生 int
;无法指定以较低精度执行操作。
算术运算要对 int
或更大的数进行运算。所以,你的 byte
在被减去之前被提升为整数 - 而且,你可能会得到实际的 int
,C/C++ 是可以的,因为它们可以容纳 byte
.
的整个范围
如果减法的结果向下转换为 byte
,它会给出预期的溢出行为。但是,如果您在 diff2
计算中省略了转换,那么您将对 negative int
进行取模。并且,因为 C/C++ 带符号的除法向零舍入,带符号的模数与被除数的符号相同。
这里的第一个失误是期望减法直接作用于您的 byte
类型,或者将您的无符号 byte
转换为无符号 int
。级联问题是忽略了 C++ 有符号除法的行为(如果您不知道应该首先期望有符号算术成为一个问题,这是可以理解的)。
请注意,如果您的 RING_BUFFER_SIZE
不是 2 的幂,那么对于这种情况,除法将无法正常工作。并且,由于它是二的幂,请注意:
(b2 - b1)&(RING_BUFFER_SIZE-1)
应该可以正常工作。
最后(如评论中所建议的),进行环形缓冲区减法的正确方法是确保 b1 < RING_BUFFER_SIZE
(这对环形缓冲区操作有意义),并使用类似:
(b2>b1)? b2 - b1 : RING_BUFFER_SIZE + b2 - b1
以下Arduino (C++)代码
void setup()
{
Serial.begin(115200);
byte b1 = 12;
byte b2 = 5;
const byte RING_BUFFER_SIZE = 64;
byte diff = b2 - b1;
byte diff2 = (byte)(b2 - b1) % RING_BUFFER_SIZE; //<---NOTE HOW THE (byte) CAST IS *REQUIRED* TO GET THE RIGHT RESULT!!!!
Serial.println(b1);
Serial.println(b2);
Serial.println(RING_BUFFER_SIZE);
Serial.println(diff);
Serial.println(diff2);
}
void loop()
{
}
产生预期的:
12
5
64
249
57 //<--correct answer
而没有此处所示的“(byte)”转换:
void setup()
{
Serial.begin(115200);
byte b1 = 12;
byte b2 = 5;
const byte RING_BUFFER_SIZE = 64;
byte diff = b2 - b1;
byte diff2 = (b2 - b1) % RING_BUFFER_SIZE; //<---(byte) cast removed
Serial.println(b1);
Serial.println(b2);
Serial.println(RING_BUFFER_SIZE);
Serial.println(diff);
Serial.println(diff2);
}
void loop()
{
}
它产生:
12
5
64
249
249 //<--wrong answer
为什么不同?为什么模运算符仅适用于显式转换?
注:"byte" = "uint8_t"
5 - 12
给出 -7
(int
)。所以你的代码确实 -7 % 64
。
从数学上讲,我们预计这会给出 57
。但是,在 C 和 C++ 中,%
对于负数,它不会按照数学上的预期执行。相反,它满足以下等式:
(a/b) * b + a%b == a
现在,(-7)/64
给出了 0
,因为 C 和 C++ 使用向零截断来进行正负整数除法。因此 -7 % 64
的计算结果为 -7
.
最后,将 -7
转换为 uint8_t
得到 249
。
当您写 (byte)-7 % 64
时,您实际上是在 249 % 64
给出预期的答案。
关于 b2 - b1
的行为:所有整数运算至少以 int
精度进行;对于 -
的每个操作数,如果它是比 int
更窄的整数类型,它首先被提升为 int
(保持值不变)。如果在此升级后类型不同(在这种情况下它们不会),则可能会发生进一步的转换。
在代码中,b2 - b1
表示 (int)b2 - (int)b1
产生 int
;无法指定以较低精度执行操作。
算术运算要对 int
或更大的数进行运算。所以,你的 byte
在被减去之前被提升为整数 - 而且,你可能会得到实际的 int
,C/C++ 是可以的,因为它们可以容纳 byte
.
如果减法的结果向下转换为 byte
,它会给出预期的溢出行为。但是,如果您在 diff2
计算中省略了转换,那么您将对 negative int
进行取模。并且,因为 C/C++ 带符号的除法向零舍入,带符号的模数与被除数的符号相同。
这里的第一个失误是期望减法直接作用于您的 byte
类型,或者将您的无符号 byte
转换为无符号 int
。级联问题是忽略了 C++ 有符号除法的行为(如果您不知道应该首先期望有符号算术成为一个问题,这是可以理解的)。
请注意,如果您的 RING_BUFFER_SIZE
不是 2 的幂,那么对于这种情况,除法将无法正常工作。并且,由于它是二的幂,请注意:
(b2 - b1)&(RING_BUFFER_SIZE-1)
应该可以正常工作。
最后(如评论中所建议的),进行环形缓冲区减法的正确方法是确保 b1 < RING_BUFFER_SIZE
(这对环形缓冲区操作有意义),并使用类似:
(b2>b1)? b2 - b1 : RING_BUFFER_SIZE + b2 - b1