算术溢出是否等同于模运算?

25

我需要在C中执行模256算术运算。那么我可以简单地执行以下操作吗?

unsigned char i;
i++;

代替

int i;
i=(i+1)%256;

1
好的,自从C99以来就有了stdint.h,你可以简单地使用uintXX_t。http://www.cplusplus.com/reference/cstdint/ - user2485710
7
虽然这个方法可以运行,但它属于过于聪明的技巧,很可能会被认为是错误而被另一个开发者删除。使用取模时明确表达你的意图可以避免这种情况的发生。 - Mgetz
如果你不必依赖任何其他人来维护你的代码,那么是的。我只是不明白为什么你要这样做。维护混乱的代码所需的额外工作是不值得的。 - Peter Abolins
3
记住 % 是余数而不是模运算!尽管对于正数来说模运算等同于取余。这两段代码并不等价,因为第一段给出的是模运算结果,而第二段给出的是余数(你的 iint 而不是 unsigned int)。 - Grijesh Chauhan
2
@v3ga - 很好你问了;这引发了一个有趣的讨论,也让我意识到“并非每个字节都是8位”- 这对我来说是新闻。你是对的,最好使用“显式”的代码,使其明显你在做什么 - 而且好的编译器知道快速处理 %256 的技巧,所以你不必微调优化。写清晰的代码有很多值得说的地方 - 特别是如果它是实验并且需要评分!感谢你开启了一个有趣的讨论。 - Floris
显示剩余3条评论
7个回答

25

不,没有任何保证 unsigned char 有八位。使用来自 <stdint.h>uint8_t 就可以了,这样您就完全没问题了。这需要支持 stdint.h 的实现:任何符合 C99 标准的编译器都支持,但旧编译器可能不提供此项功能。

注意:无符号算术永远不会溢出,并以“模 2^n”的方式运行。有符号算术溢出会产生未定义的行为。


10
@Floris:是的,但一个C字节不一定是8位。它被定义为一个char的大小。 - Alexandre C.
2
而且,OP甚至没有提到他所针对的机器类型,因此你不知道,你应该在你的回答中指明这一点;其他人可能也需要这个信息。 - user2485710
2
@user2485710:完成。由于在这种环境中字节技巧非常普遍,因此确实需要谨慎提醒。 - Alexandre C.
1
即使使用符合C99标准的编译器,所有的intN_tuintN_t类型都是可选的,并不能保证一定可用。在任何没有8位字节(尽管这可能不太常见)的系统上,它们很可能不可用。 - Crowman
2
@PaulGriffiths:我知道的一个例子是特定的16位德州仪器DSP嵌入式平台,它只支持字寻址,因此所有内容至少为16位。 sizeof(char)== sizeof(short)== sizef(int)== 1 == 16位。在这里建议的假设在那里行不通。 - Jason R
显示剩余5条评论

6

是的,你举的两个例子的行为是相同的。请看C99 6.2.5 §9:

包含无符号操作数的计算永远不会溢出,因为结果如果无法表示为结果无符号整数类型中的值,则会对其进行取模,取模值为大于结果类型所能表示的最大值的数字。


5
很可能是这样,但在这种情况下,原因实际上相当复杂。
unsigned char i = 255;
i++;

i++ 相当于 i = i + 1

(嗯,几乎相同。i++ 返回的是在增加之前的 i 值,所以实际上相当于 (tmp=i; i = i + 1; tmp)。但由于在这种情况下结果被丢弃,因此不会引起任何额外的问题。)

由于 unsigned char 是一个窄类型,所以 + 运算符的 unsigned char 操作数将被提升为 int(假设 int 可以容纳 unsigned char 范围内的所有可能值)。因此,如果 i == 255,且 UCHAR_MAX == 255,则加法的结果为 256,并且是有符号的 int 类型。

该赋值将值为 256 的 int 类型数值隐式地转换为 unsigned char 类型。转换成无符号类型是有明确定义的;结果会被模除(MAX + 1),其中 MAX 是目标无符号类型的最大值。
如果 i 被声明为 unsigned int:
unsigned int i = UINT_MAX;
i++;

不会发生类型转换,但对于无符号类型,+ 运算符的语义也指定了模数 MAX+1

请记住,分配给 i 的值在数学上等同于 (i+1) % UCHAR_MAXUCHAR_MAX 通常为 255,并保证至少为 255,但合法地可以更大。

可能会有一种奇特的系统,UCHAR_MAX 无法存储在有符号的 int 对象中。这需要 UCHAR_MAX > INT_MAX,这意味着该系统至少应该有16位字节。在这样的系统上,提升将从 unsigned charunsigned int。最终结果将是相同的。你不太可能遇到这样的系统。我认为有一些 DSPs 的 C 实现具有大于8位的字节。字节中的位数由 <limits.h> 中定义的 CHAR_BIT 指定。 CHAR_BIT > 8 不一定意味着 UCHAR_MAX > INT_MAX。例如,您可以有 CHAR_BIT == 16sizeof (int) == 2,即16位字节和32位的 int

1
从技术上讲,i++ 相当于在递增 i 之前复制 i 并在后缀表达式中使用旧副本。而 ++i 则是真正等价于 i = i + 1/i += 1 - JAB

5
unsigned char c = UCHAR_MAX;
c++;

基本上是的,没有溢出,但不是因为c是无符号类型。这里有一个隐藏的c转换为int和从intunsigned char的整数转换,它是完全定义好的。

例如,

 signed char c = SCHAR_MAX;
 c++;

这也不属于未定义行为,因为实际上它等同于:

c = (int) c + 1;

在这里,从intsigned char的转换是实现定义的(请参见c99中的6.3.1.3p3关于整数转换的内容)。为了简化起见,假设CHAR_BIT == 8

有关上述示例的更多信息,请阅读此文章:

“来自地狱的小C函数”

http://blog.regehr.org/archives/482


感谢提供“来自地狱的小型 C 语言函数”链接。我之前看过它,但当时忘记将其加入书签。由于无法回忆起确切的标题,我无法通过 Google 搜索找到它。 - Tonny
两个unsigned char值的和不会溢出,但在sizeof(int)为2的机器上,两个unsigned char值的乘积可能会溢出。历史上,标准允许16位机器上的编译器对unsigned char x=255; x*=255;做任何喜欢的事情,并没有被视为缺陷,因为实际上,即使使用16位int的编译器也会表现得明智。这样的代码产生未定义行为被视为理论问题,而不是编译器否定时间和因果律的机会。 - supercat

3
如果您不想使用其他数据类型,还有另一种选择,这个选择还没有被提到。
unsigned int i;
// ...
i = (i+1) & 0xFF; // 0xFF == 255

这种方法的原理是模数元素等于2^n,意味着范围将会是[0, 2^n-1],因此位掩码将轻松保持值在所需的范围内。这种方法可能与unsigned char/uint8_t版本的效率差不多,具体取决于编译器背后执行的魔法和目标系统如何处理非字加载(例如,一些RISC架构需要额外的操作来加载非字大小的值)。当然,这也假设你的编译器不会检测无符号值上使用二的幂次方模算术并为你替换位掩码,因为在像那样的情况下,模数的使用将具有更大的语义价值(尽管以此作为决策基础并不完全可移植)。
这种方法的优点是,您可以将其用于不是数据类型大小的二的幂次方,例如:
i = (i+1) & 0x1FF; // i %= 512
i = (i+1) & 0x3FF; // i %= 1024
// etc.

2
这应该可以很好地工作,因为它只会溢出回到0。正如在另一个答案的评论中指出的那样,只有当值为无符号时才应这样做,因为使用有符号值可能会导致未定义的行为。
然而,最好还是保留使用取模运算,因为其他维护代码的人更容易理解代码,并且聪明的编译器可能已经在执行这种优化,使得使用这种方法变得毫无意义。此外,性能差异可能非常小,因此一开始就不太重要。

1
如果您用来表示该数字的位数等于除数的二进制(无符号)表示法中的位数(100000000)- 1,那么它将起作用,在这种情况下是:9-1 = 8(char)。

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