38

C11 §6.5.7 第 5 段:

结果E1 >> E2E1右移的E2位位置。如果 E1有无符号类型或E1有符号类型和非负值,则结果的值是 的商的整数部分E1 / 2*^E2如果E1具有带符号类型和负值,则结果值是实现定义的。

但是,viva64参考文档说:

int B;
B = -1 >> 5; // unspecified behavior

我在GCC上运行了这段代码,它总是给出一个输出-1

因此,标准说“如果 E1 具有带符号类型和负值,则结果值是实现定义的”,但该文档说的-1>>5;未指定的行为

那么,-1>>5;C 中有未指定的行为吗?哪个是对的?

4

4 回答 4

39

两者都是正确的。实现定义的行为是一种特定类型的未指定行为。

引用定义“实现定义的行为”的 C 标准的第 3.4.1 节:

1 实现定义的行为

未指定的行为,其中每个实现都记录了如何做出选择

2示例 实现定义行为的一个示例是当有符号整数右移时高位的传播。

从第 3.4.4 节定义“未指定的行为”:

1 未指明的行为

使用未指定的值,或本国际标准提供两种或多种可能性的其他行为,并且在任何情况下都没有强加进一步的要求

2示例 未指定行为的一个示例是评估函数参数的顺序。

至于 GCC,您将始终得到相同的答案,因为操作是实现定义的。它通过符号扩展实现负数的右移

GCC 文档

对有符号整数(C90 6.3、C99 和 C11 6.5)的一些按位运算的结果。

位运算符作用于值的表示,包括符号位和值位,其中符号位被认为是紧邻最高值位的上方。Signed>>通过符号扩展作用于负数。

作为对 C 语言的扩展,GCC 不使用 C99 和 C11 中给出的纬度仅将带符号的某些方面<<视为未定义。但是,-fsanitize=shift(and -fsanitize=undefined) 将诊断此类情况。在需要常量表达式的地方也会对它们进行诊断。

于 2017-10-16T19:02:12.543 回答
14

“未指定的行为”和“定义的实现”并不矛盾。这只是意味着 C 标准没有指定需要发生什么,并且各种实现可以做他们认为“正确”的事情。

在一个编译器上多次运行它并获得相同的结果仅意味着该特定编译器是一致的。您可能会在不同的编译器上得到不同的结果。

于 2017-10-16T19:02:50.693 回答
2

我没有得到任何目前的答案。C 标准清楚地表明右移负数是实现定义的行为。这不是未指定的行为,这意味着其他东西。正如您正确引用的那样(C17 6.5.7 §5):

E1 >> E2 的结果是 E1 右移 E2 位位置。/--/
如果 E1 具有带符号类型和负值,则结果值是实现定义的。

这意味着编译器必须记录它的行为方式。时期。

在实践中:文档必须说明编译器是使用算术右移还是逻辑右移。


这与未指定行为相反,未指定行为是特定于实现的行为,不需要记录。在两种情况下使用未指定的行为:

  • 当编译器行为可能是编译器供应商不应被迫向其竞争对手透露的实现秘密时。
  • 当编译器懒得记录底层细节(如 OS 和 RAM 内存单元)如何工作时。

例如,编译器不需要在如下代码中记录评估顺序:

a  = f1() + f2();
a += f1() + f2();

记录评估子表达式的顺序将揭示有关编译器内部表达式树和优化器如何工作的详细信息,这反过来将揭示为什么编译器产生更好的代码或编译速度比竞争对手快。在最初编写 C 标准时,这是一件大事。如今,当有一些很棒的开源编译器时,情况就不那么好了,所以它不再是一个秘密。

同样,编译器不需要记录此代码打印的内容:

int a;
int ptr = &a;
printf("%d", *ptr);

a是一个不确定的值并且输出是未指定的 - 实际上输出取决于之前存储在该特定 RAM 单元中的内容。我们称之为“垃圾价值”。(在大喊“UB”之前,请参阅(Why) is using an uninitialized variable undefined behavior?)。

于 2018-08-30T09:15:23.600 回答
2

实现定义的行为是未指定行为的子类,即标准未指定的行为。

C89 的缺陷报告 #154 向委员会询问了实现定义的行为的限制是什么;委员会回答说,实现可以定义它想要的任何行为,并且不需要保持不变。

一个实现需要做的是记录这个选择是如何做出的,而不是其他类别的未指定行为,其中符合的实现甚至不需要费心告诉如何做出选择,可能是因为对于这些大多数实现,文本会说“随机”或“取决于编译器优化级别”或“取决于局部变量的寄存器分配”。

于 2018-08-27T09:16:22.620 回答