8

我知道以下是未定义的,因为我试图在同一个表达式中读取和写入变量的值,即

int a=5;
a=a++;

但如果是这样,那么为什么下面的代码片段不是未定义的

int a=5;
a=a+1;

就像这里一样,我也在尝试修改它的值a并同时写入它。

还要解释为什么标准没有解决这个问题或删除这个未定义的行为,尽管他们知道它是未定义的?

4

5 回答 5

6

为什么下面的代码片段不是未定义的

int a=5;
a=a+1;  

该标准指出

在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的评估修改一次。此外,只能访问先验值以确定要存储的值。

的情况下a = a + 1a仅修改一次,并且仅a访问的先前值以确定要存储的值a
而在 , 的情况下a=a++;a修改了不止一次——通过++子表达式中a++=运算符和将结果分配给 left的运算符a。现在没有定义哪个修改,无论是 by++还是 by =,将首先发生

几乎所有带有标志的现代编译器-Wall都会在编译第一个片段时发出警告,例如:

[Warning] operation on 'a' may be undefined [-Wsequence-point]

进一步阅读:如何理解本节中的复杂表达式,并避免编写未定义的表达式?

于 2014-03-24T07:56:44.683 回答
6

它未定义的原因不是你读写,而是你写了两次。

a++表示读取 a 并在读取后递增它,但我们不知道 ++ 是在赋值之前发生=(在这种情况下,=将用 a 的旧值覆盖)还是之后,在这种情况下,a 将递增。

只需使用a++;:)

a = a + 1没有问题,因为 a 只写了一次。

于 2014-03-24T08:00:59.080 回答
3

++ 运算符将为 a 加一,这意味着变量 a 将变为 a+1。实际上,以下两个语句是相等的:

a++;
a = a + 1;

最后一条语句 a + 1 不会增加 a - 它会生成一个值为 a + 1 的结果。如果您希望 a 变为 a+1,则必须将 a + 1 的结果分配给 a

a = a + 1;

您所做的第一个语句不起作用的原因是因为您编写了类似的内容

a = (a = a + 1);
于 2014-03-24T08:02:17.037 回答
3

长话短说,您可以在标准中找到每个定义的行为。此处未按定义提及的所有内容 - 都是未定义的。

对您的示例的直观解释:

a=a++;

您想a在单个语句中修改变量两次。

1) a= //first time
2) a++ //second time

如果你看这里:

a=a+1;

您只修改变量 a 一次:

a= // (a+1) - doesn't change the value of a

为什么标准不定义a=a++行为?

可能的原因之一是: 编译器可以执行优化。您在标准中定义的案例越多,编译器优化代码的自由度就越小。因为不同的架构可以有不同的递增指令实现,编译器不会使用所有处理器指令,以防它们破坏标准行为。或者在某些情况下编译器可以更改评估顺序,但是如果您想修改两次,此限制将强制编译器禁用此类优化。

于 2014-03-24T08:05:23.100 回答
2

其他人已经讨论了您的特定示例的详细信息,因此我将添加一些有助于捕获未定义行为的一般信息和工具。

没有最终的工具或方法可以捕获未定义的行为,因此即使您使用所有这些工具,也不能保证您的代码中没有未定义的内容。但是 IME 这些将捕获相当多的常见问题。我没有列出您应该使用的软件开发的标准良好实践,例如单元测试。

  • clang(-analyze) 有几个选项可以帮助在编译时和运行时捕获未定义的行为。它有 -ftrapv,它新获得了对金丝雀值的支持,它的地址清理器,--fcatch-undefined-behaviour 等等。

  • gcc 也有几个选项来捕获未定义的行为,例如挡泥板、它的地址清理器、堆栈保护器。

  • valgrind 是一个出色的工具,用于在运行时查找与内存相关的未定义行为。

  • frama-c 是一个静态分析工具,可以发现和可视化未定义的行为。它能够找到死代码(未定义的行为通常会导致代码的其他部分变为死代码)是追踪潜在安全问题的非常有用的工具。frama-c 具有许多更高级的功能,但可以说比...更难使用

  • 存在其他可以捕获未定义行为的商业静态分析工具,例如 PVS-studio、klocwork 等。不过,这些通常要花很多钱。

  • 使用不同的编译器和奇怪的架构进行编译。如果可以,为什么不在 8 位 AVR 芯片上编译和运行代码?树莓派(32 位 ARM)?使用 emscripten 将其编译为 javascript 并在 V8 中运行?这样做往往是一种捕获可能导致崩溃的未定义行为的实用方式(但对于捕获可能导致安全问题的潜伏 UB 几乎/没有作用)。

现在,关于为什么存在未定义行为的本体论原因......基本上是出于性能和易于实现的原因。C 中的许多 UB 允许编译器优化其他语言无法优化的某些内容。如果你比较一下 java、python 和 C 如何处理有符号整数类型的溢出,你会发现在一个极端,python 以一种方便程序员的方式对它进行了很好的定义——int 实际上可以变得无限大。频谱另一端的 C 未定义 - 您有责任永远不会溢出您的有符号整数。Java 介于两者之间。

但另一方面,这意味着在 python 中不知道“int + int”操作在执行时将实际执行什么工作。它可能执行数百条指令,通过操作系统来回分配一些内存,等等。如果您非常关心性能,或者更具体地说,是一致的性能,这将非常糟糕。另一方面,C 允许编译器将“+”映射到添加整数的 CPU 本地指令(如果存在)。当然,不同的 CPU 可能会以不同的方式处理溢出,但由于 C 未定义,这很好 -作为程序员,您必须注意不要溢出您的整数。这意味着 C 为编译器提供了编译“int + int”的选项

请注意,C 不保证 + 实际上直接映射到本机 CPU 指令,它只是为编译器留下了以这种方式进行操作的可能性——显然这是任何编译器编写者都渴望利用的东西。Java 定义有符号整数溢出的方法比 python 更不可预测(在性能方面),但可能不会导致 + 在 C 允许的许多 CPU 类型上变成单个 CPU 指令。

所以本质上,C 试图接受未定义的行为,并选择(一致的)速度和易于实现,而其他语言选择安全或可预测的行为(从程序员的角度来看)。这不是一个好的决定,例如尊重到安全/安保,但这就是 C 的立场。它归结为“了解手头工作的合适工具”,并且在很多情况下,C 为您提供的性能可预测性绝对是必不可少的。

于 2014-03-24T08:37:11.610 回答