如果char是有符号的,那么"char foo = 255"是否是未定义行为?

39

在使用Linux上的x86机器和gcc 4.5.2编译时,以下内容没有任何警告:

char foo = 255;

但是当我使用-pedantic时,gcc会提示:

警告:隐式常量转换中发生溢出

gcc的行为有些奇怪,这让我怀疑自己是否真正理解了此赋值操作的含义。我认为,在POSIX上,如果char长8位并默认为带符号类型,则不能保存255

C标准规定,无符号整数溢出将导致溢出,而有符号整数溢出未定义。那么,这个赋值操作是未定义行为吗?gcc为什么会这样做呢?


2
我曾经在某个地方读到,只有真正的溢出才是未定义行为,因此赋值运算符不会引发未定义行为,但我不确定。无论如何,这个问题值得一加。 - user529758
2
但是在此之后,你尝试打印 foo 了吗?你得到了什么值? - smac89
1
警告只是让您知道数字不会与您设置的数字相同,而不是警告任何未定义的行为,但更一般的问题很有趣。我的直觉是它是未定义的(因为foo是有符号的,所以在运行时它将是未定义的),但我不知道。 - Dave
1
@Dave 在那个答案中哪一行说它是未定义的? - user529758
1
@Smac89:它打印“垃圾”,因为在ASCII字符中没有这样的符号,但您可以使用类似这样的值来组成Unicode字符。 - user1042840
显示剩余9条评论
3个回答

30

摘要:结果是实现定义的,很可能是-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的值为SIGFPESIGILLSIGSEGV 或任何其他对应于计算异常的实现定义值,则行为未定义;否则,程序将在被中断的点继续执行。

但我认为这不适用,因为溢出转换不是此处使用术语的“计算异常”。(除非实现定义的信号恰好是SIGFPESIGILLSIGSEGV——但这很愚蠢。)

因此,如果实现选择在响应溢出转换时引发信号,则行为(而不仅仅是结果)至少是实现定义的,并且在某些情况下可能是未定义的。无论如何,似乎没有任何可移植的方法来处理这样的信号。

实际上,我从未听说过采用C99中新措辞的实现。对于我听说过的所有编译器,转换的结果是实现定义的,并且很可能产生您从2的补码截断所期望的结果。(我完全不相信C99中的这种改变是一个好主意。如果没有其他的话,它使得这个答案需要的长度增加了3倍 。)


所以,“char foo = 255”在POSIX系统上是实现定义的,但实际上它归结为环绕。对于无符号类型,如“unsigned int foo = 9999999999”,是否也是相同的?我知道它会环绕,但它也是实现定义的吗?此外,如果这不是溢出,gcc为什么会在-char情况下使用-pedantic模式警告“溢出”会发生什么? - user1042840
在我看来,信号为SIGFPE实际上一点也不傻 - 虽然大多数现实世界的系统不会因整数溢出而引发信号,但它们确实经常会因整数除法而引发一个信号,而它们引发的信号就是SIFGPE。无论名称暗示了_浮点_错误与否。(当然,整数除法的结果是未定义行为) - Random832
@user1042840:转换为无符号类型是由标准定义的;它有效地丢弃结果中除低位N位之外的所有位(尽管标准是以算术值而不是位来定义结果的)。你为什么说这不是溢出?(C标准没有定义这个术语。) - Keith Thompson
1
@Keith Thompson:总结一下:1. 在转换中,有符号溢出是实现定义的(或在C99中引发实现定义的信号),无符号溢出会导致环绕。2. 在运算符中,有符号溢出是未定义行为(在第6.5节的第5点中描述),而无符号行为会导致环绕。我理解得对吗? - user1042840
如果您知道给定实现要使用哪个信号,您可能可以对其进行某些操作,但这样的代码只适用于该实现。程序甚至没有一种查询实现是否会引发信号或找出哪种信号的方法。并且它仅适用于转换时的溢出;关于算术运算符的类似溢出只是未定义。我认为它旨在提供一个实现定义的溢出处理钩子,这是C90中不存在的... - Keith Thompson
显示剩余13条评论

13

只有在计算算术表达式的中间结果时发生溢出,例如在二进制乘法、一元递减等过程中,带符号整数溢出才会导致未定义行为。如果将浮点值转换为整数类型并且该值超出范围,则会导致未定义行为。

将数字转换成带符号类型时发生溢出不会导致未定义行为。相反,它会产生实现定义的结果(可能会引发实现定义的信号)。


2
你能告诉我你是如何依赖它的吗? - user1042840

9
根据C11,6.3.1.3规定:
当将整数类型的值转换为非_Bool整数类型时,如果[...] 新类型是有符号的并且该值无法在其中表示; 结果要么是实现定义的,要么会引发实现定义的信号。

2
@haccks:你是什么意思? :-) - Kerrek SB
4
假设一个程序收到了一个“实现定义的信号”,那么其行为是未定义的。(该信号条款是由C99添加的,我不知道是否有任何实际的实现采用了该信号,而且我怀疑这是否是一个好主意。) - Keith Thompson
@Kerre SB: 这是真的,但在n1256.pdf中的3.4.3中说:“未定义行为的一个例子是整数溢出时的行为”。标准会在同一问题上说两件不同的事吗?也许我的溢出理解是错误的。 - user1042840
但是如果gcc警告“隐式常量转换中的溢出”,我认为这确实是一种溢出。 - user1042840
@Adam Rosenfield:那么在这种情况下,gcc的警告是误导性的吗? - user1042840
显示剩余3条评论

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