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
循环条件因此始终为真并以无限循环结束?
如果 i
是 signed int
怎么办?它不包含与数组访问相关的陷阱吗?
更新
我 运行 多次使用此代码和类似代码,并且运行良好。此外,它是向后迭代数组和向量的流行方式。我问这个问题是为了确保从标准的角度来看这种方式是可行的。
一看,显然不是无穷大。另一方面,有时编译器可以 "optimize" 去除一些条件和代码,假设代码不包含未定义的行为。它可能导致无限循环和其他不良后果。参见 。
此代码不会调用未定义的行为。一旦 i
变为 0
,循环将终止。
对于unsigned int
,没有整数over/underflow。 i
的效果与 signed
相同,只是在这种情况下不会换行。
i
将环绕到 ~0
(0XFFFFFFFF
用于 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
.
中的所有有效索引
s
和 a
的所有元素都初始化为零。所以所有的加法都是把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
,或者循环将无限继续.
如果 i
是 signed int
怎么办?
以上两种转换都是 C++ 内置的。它们都为所有整数类型定义,有符号和无符号。所以最终这扩展为:
while(static_cast<bool>(operator--(i, 1)))
它不包含与数组访问相关的陷阱吗?
这又是在调用内置运算符 subscript operator:T& 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,无论 i
是 unsigned int
还是 signed int
,您的代码都表现良好。 (如果 N 为负且 i
为 signed int
,则循环将继续递减,直到在 INT_MIN
处发生未定义的行为。)
考虑简单的代码:
#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
循环条件因此始终为真并以无限循环结束?
如果 i
是 signed int
怎么办?它不包含与数组访问相关的陷阱吗?
更新
我 运行 多次使用此代码和类似代码,并且运行良好。此外,它是向后迭代数组和向量的流行方式。我问这个问题是为了确保从标准的角度来看这种方式是可行的。
一看,显然不是无穷大。另一方面,有时编译器可以 "optimize" 去除一些条件和代码,假设代码不包含未定义的行为。它可能导致无限循环和其他不良后果。参见
此代码不会调用未定义的行为。一旦 i
变为 0
,循环将终止。
对于unsigned int
,没有整数over/underflow。 i
的效果与 signed
相同,只是在这种情况下不会换行。
i
将环绕到 ~0
(0XFFFFFFFF
用于 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
.
s
和 a
的所有元素都初始化为零。所以所有的加法都是把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 becomefalse
. All other values becometrue
.
因此,如果 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
,或者循环将无限继续.
如果 i
是 signed int
怎么办?
以上两种转换都是 C++ 内置的。它们都为所有整数类型定义,有符号和无符号。所以最终这扩展为:
while(static_cast<bool>(operator--(i, 1)))
它不包含与数组访问相关的陷阱吗?
这又是在调用内置运算符 subscript operator:T& 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,无论 i
是 unsigned int
还是 signed int
,您的代码都表现良好。 (如果 N 为负且 i
为 signed int
,则循环将继续递减,直到在 INT_MIN
处发生未定义的行为。)