while(i--) s+= a[i];在 C 和 C++ 中包含未定义的行为?

Does while(i--) s+= a[i]; contain undefined behavior in C and C++?

考虑简单的代码:

#include "stdio.h"

#define N 10U

int main() {
    int a[N] = {0};
    unsigned int i = N;
    int s = 0;

    // Fill a

    while(i--)
        s += a[i];

    printf("Sum is %d\n", s);

    return 0;
}

while 循环是否因整数下溢而包含未定义的行为?编译器是否有权假设 while 循环条件因此始终为真并以无限循环结束?

如果 isigned int 怎么办?它不包含与数组访问相关的陷阱吗?

更新

我 运行 多次使用此代码和类似代码,并且运行良好。此外,它是向后迭代数组和向量的流行方式。我问这个问题是为了确保从标准的角度来看这种方式是可行的。

一看,显然不是无穷大。另一方面,有时编译器可以 "optimize" 去除一些条件和代码,假设代码不包含未定义的行为。它可能导致无限循环和其他不良后果。参见

此代码不会调用未定义的行为。一旦 i 变为 0,循环将终止。

对于unsigned int,没有整数over/underflow。 i 的效果与 signed 相同,只是在这种情况下不会换行。

i 将环绕到 ~00XFFFFFFFF 用于 32 位)但循环将终止,因此没有 UB。

Does while loop contain undefined behavior because of integer underflow?

不,overflow/underflow 只是在有符号整数的情况下的未定义行为。

Do compilers have right to assume that while loop condition is always true because of that and end up with endless loop?

不,因为表达式最终会变成零。

What if i is signed int? Doesn't it contain pitfalls related to array access?

如果它已签名并且 over/underflows,您将调用未定义的行为。

由于以下原因,循环不会产生未定义的行为。

i初始化为10,并在循环中递减。当 i 的值为零时,递减它会产生等于 UINT_MAX 的值 - unsigned 可以表示的最大值,但循环将终止。 a[i] 只会在 N-1(即 9)和 0 之间访问(在循环内)i 的值。这些都是数组 a.

中的所有有效索引

sa 的所有元素都初始化为零。所以所有的加法都是把0加到0。这永远不会溢出也不会下溢 int,因此永远不会导致未定义的行为。

如果把i改成signed int,递减永远不会下溢,循环结束时i会变成负值。唯一的净变化是 i 在循环后的值。

while 循环是否包含因整数下溢而导致的未定义行为?

首先这是 Boolean Conversion:

A prvalue of integral, floating-point, unscoped enumeration, pointer, and pointer-to-member types can be converted to a prvalue of type bool.

The value zero (for integral, floating-point, and unscoped enumeration) and the null pointer and the null pointer-to-member values become false. All other values become true.

因此,如果 i 正确初始化,将不会出现整数下溢,它将达到 0U 并将转换为 false 值。

所以编译器在这里有效地做的是:while(static_cast<bool>(i--))

编译器是否有权假设 while 循环条件总是 true 并因此以无限循环结束?

这不是无限循环的关键原因是 postfix decrement operator returns 递减变量的值。它被定义为 T operator--(T&, int) 并且如前所述,编译器将评估该值是否为 0U 作为转换为 bool.

的一部分

编译器在这里有效地做的是:while(0 != operator--(i, 1))

在您的更新中,您引用此代码作为提出问题的动机:

void fn(void)
{
    /* write something after this comment so that the program output is 10 */
    int a[1] = {0};
    int j = 0;
    while(a[j] != 5) ++j;  /* Search stack until you find 5 */
    a[j] = 10;             /* Overwrite it with 10 */
    /* write something before this comment */
}

经检查,整个程序具有未定义的行为,只有 a 的 1 个元素并且它被初始化为 0。因此对于 0 以外的任何索引,a[j] 正在寻找离开数组的末尾。将继续直到找到 5 或直到 OS 错误,因为程序已从受保护的内存中读取。这与您的循环条件不同,当后缀递减运算符 returns 的值为 0 时,循环条件将退出,因此不能假设这总是 true,或者循环将无限继续.

如果 isigned int 怎么办?

以上两种转换都是 C++ 内置的。它们都为所有整数类型定义,有符号和无符号。所以最终这扩展为:

while(static_cast<bool>(operator--(i, 1)))

它不包含与数组访问相关的陷阱吗?

这又是在调用内置运算符 subscript operatorT& operator[](T*, std::ptrdiff_t)

所以a[i]相当于调用operator(a, static_cast<ptrdiff_t>(i))

所以显而易见的后续问题是什么是 ptrdiff_t?它是一个实现定义的整数,但因此标准的每个实现都负责定义与此类型之间的转换,因此 i 将正确转换。

您的代码运行良好,不包含任何 N ≥ 0 的未定义行为。

让我们关注 while 循环,并跟踪一个 N = 2 的执行示例

// Initialization
#define N 2U
unsigned int i = N;  // i = 2

// First iteration
while(i--)  // condition = i = 2 = true; i post-decremented to 1
    s += a[i];  // i = 1 is in bounds

// Second iteration
while(i--)  // condition = i = 1 = true; i post-decremented to 0
    s += a[i];  // i = 0 is in bounds

// Third iteration
while(i--)  // condition = i = 0 = false; i post-decremented to 0xFFFFFFFF
// Loop terminated

我们可以从这个跟踪中看到 a[i] 总是在边界内,并且 i 在从 0 递减时经历了 unsigned 环绕,这是完美的定义明确。

为了回答您的第二个问题,如果我们将 i 的类型更改为 signed int,示例跟踪中唯一发生变化的行为是循环在同一位置终止,但 i 递减为 -1,这也是定义明确的。

因此总而言之,假设 N ≥ 0,无论 iunsigned int 还是 signed int,您的代码都表现良好。 (如果 N 为负且 isigned int,则循环将继续递减,直到在 INT_MIN 处发生未定义的行为。)