在带有Linux的x86机器上使用gcc 4.5.2编译时,以下内容不会给我任何警告:
char foo = 255;
但是当我使用-pedantic
时,gcc 说:
警告:隐式常量转换中的溢出
gcc的行为方式有点奇怪,这让我怀疑我是否真的理解这项任务中发生了什么。我认为如果 char
在 POSIX 上有 8 位长并且默认情况下是签名的,它就无法容纳255
.
在 C 标准中,它说无符号整数溢出导致溢出,但有符号整数溢出是未定义的。那么这个赋值是未定义的行为吗?为什么海湾合作委员会会这样做?
总结:结果是实现定义的,很可能是-1
的,但它很复杂,至少在原则上是这样。
关于溢出的规则对于运算符与转换是不同的,对于有符号类型与无符号类型是不同的 - 并且转换规则在 C90 和 C99 之间更改。
从 C90 开始,具有有符号整数操作数的运算符溢出("溢出"意味着数学结果不能用表达式的类型表示)具有未定义的行为。对于无符号整数操作数,该行为被很好地定义为通常的环绕(严格来说,标准不称之为"溢出")。但是您的声明:
char foo = 255;
不使用任何运算符(=
是初始值设定项,而不是赋值项),因此在这种情况下都不适用。
如果类型 char
可以表示值255
(无论是普通char
无符号还是CHAR_BIT >= 9
),那么行为当然是很好的定义。int
表达式255
隐式转换为char
。(自CHAR_BIT >= 8
以来,此特殊情况不可能调用无符号的环绕。
否则,转换会产生无法存储在char
中的结果。
从 C90 开始,转换的结果是实现定义的——这意味着它保证将foo
设置为类型 char
范围内的某个值,您可以通过阅读实现的文档来确定该值是什么,这是告诉您转换如何工作所必需的。(我从未见过存储值不是-1
的实现,但原则上任何结果都是可能的。
C99 更改了定义,以便溢出转换为有符号类型要么产生实现定义的结果,要么引发实现定义的信号。
如果编译器选择执行后者,则必须记录引发的信号。
那么,如果提出实现定义的信号会发生什么?该标准第7.14节说:
完整的信号集、它们的语义和默认值 处理是实现定义的
(对我来说)并不完全清楚信号"默认处理"的可能行为范围是什么。在最坏的情况下,我想这样的信号可能会终止程序。您可能无法定义捕获信号的信号处理程序。
7.14 还说:
如果函数返回,如果 sig 的值是 SIGFPE,SIGILL、SIGSEGV 或任何其他实现定义的值 对应于计算异常,行为未定义; 否则,程序将在原来的位置恢复执行 打断。
但我认为这不适用,因为溢出转换不是此处使用该术语的"计算异常"。(除非实现定义的信号恰好是SIGFPE
、SIGILL
或SIGSEGV
——但那将是愚蠢的)。
因此,最终,如果实现选择发出信号以响应溢出转换,则行为(不仅仅是结果)至少是实现定义的,并且在某些情况下可能是未定义的。无论如何,似乎没有任何便携式方法可以处理这样的信号。
在实践中,我从未听说过利用 C99 中的新措辞的实现。对于我听说过的所有编译器,转换的结果都是实现定义的 - 并且很可能产生您对 2 补码截断的期望。(而且我完全不相信 C99 的这种变化是一个好主意。如果不出意外,它给出这个答案的时间大约是原本需要的 3 倍。
有符号整数溢出仅在计算算术表达式的中间结果时发生时才会导致未定义的行为,例如在二进制互助、一元递减等期间。此外,如果浮点值超出范围,将浮点值转换为整数类型会导致未定义的行为。
到有符号类型的溢出整型转换不会导致未定义的行为。相反,它产生实现定义的结果(有可能引发实现定义的信号)。
根据 C11, 6.3.1.3:
当具有整数类型的值转换为
_Bool
以外的其他整数类型时,如果 [...] 新类型是有符号的并且该值不能在其中表示;要么 结果是实现定义的信号或实现定义的信号被引发。