for 循环在 switch case 之间拆分的行为
The behavior of the for loop split between switch cases
在玩代码时我注意到一个奇怪的行为,我不知道如何解释背后的逻辑
void foo(int n)
{
int m = n;
while (--n > 0)
{
switch (n)
{
case -1:
case 0:
for (int j = 0; j < m; ++j)
default:
printf(":-)");
break;
}
}
}
int main()
{
foo(10);
return 0;
}
我希望 printf
执行比方说 10
次。然后我看到它继续 运行(想象 100000 而不是 10)并且假设开发人员 (VS) 解释 printf
inside for
(非常符合预期),因此每次进入 switch
.
输出 n
次
但后来发现 j
从未被初始化。
所以我的问题是为什么?这是未定义的行为吗?这不是所谓的标准代码吗?
default
只是一个标签(如果 n
不是 -1 或 0,则代码跳转到的地址)。
因此,当 n
不是 -1 或 0 时,流程进入 for
循环的主体,跳过 j
的初始化。您也可以编写与此相同的代码,这样会更清楚这里发生的事情:
int m = n;
while (--n > 0)
{
switch (n)
{
case -1:
case 0:
for (int j = 0; j < m; ++j)
{
default: printf(":-)");
}
break;
}
}
(注意,正如@alagner 在评论中提到的,它不能用 C++ 编译器编译,但可以用 C 编译器完美编译,所以这足以说明我的观点并解释代码的外观)。
所以是的,因为 j
未初始化,所以它是未定义的行为。
如果您启用编译器警告,它会警告您 (https://godbolt.org/z/rzGraP):
warning: 'j' may be used uninitialized in this function [-Wmaybe-uninitialized]
12 | for (int j = 0; j < m; ++j)
| ^
一个switch
块实际上是一组美化的goto
语句。不同的情况不会向代码引入范围或任何逻辑结构。它们实际上只是 switch
语句要跳转到的目标。
在此程序中,default:
标签位于嵌套的 for
循环内。当遇到 default
情况时,程序跳转到循环内,就好像那里有一个 goto
语句一样。 switch
块等同于:
if (n == -1 || n == 0) {
goto before_loop;
}
else {
goto inside_loop;
}
before_loop:
for (int j = 0; j < m; ++j) {
inside_loop:
printf(":-)");
}
这很危险,因为跳转到 inside_loop:
会跳过 j = 0
。正如您所观察到的,j
仍被声明但未初始化,访问它会导致未定义的行为。
正如发布的那样,代码具有未定义的行为,因为当 switch
跳转到 default:
标签时,在 for
语句主体内,它跳过了 [=14= 的初始化] 在内部循环中,当测试 j
并在循环迭代时递减时导致未定义的行为。
在 C++ 中不允许直接跳转到新范围来跳过初始化。这种约束在 C 语言中不存在,为了与历史代码的兼容性不一定会导致问题,但现代编译器会检测到此错误并抱怨。我建议使用 -Wall -Wextra -Werror
以避免愚蠢的错误。
注意修改如下,变成完全定义,打印:)
90次(外循环9次迭代,内循环10次迭代)并成功完成:
#include <stdio.h>
void foo(int n) {
int m = n;
while (--n > 0) {
int j = 0;
switch (n) {
case -1:
case 0:
for (j = 0; j < m; ++j)
default:
printf(":-)");
break;
}
}
}
int main() {
foo(10);
printf("\n");
return 0;
}
很多很好的解释,但缺少关键点:编译器在 printf
语句之后放置了一条 jmp
指令,因为它只编译了一条 for
语句。 jmp
跳转到循环条件并在那里继续(使用未初始化的 j
)。
对于初学者来说,函数 foo
具有 return 类型 int
return 什么都没有。
while
循环:
while (--n > 0)
{
//..
}
仅在预减表达式--n
的值大于0
的情况下获得控制权。
也就是说,在while
循环中,变量n
既不等于0
也不等于-1
。
因此,控制将立即传递给 switch
语句中的标签 default
。
switch (n)
{
case -1:
case 0:
for (int j = 0; j < m; ++j)
default:
printf(":-)");
break;
}
您可以等效地重写没有 switch
语句的 while
循环,如下所示:
while (--n > 0)
{
goto Default;
for (int j = 0; j < m; ++j)
{
Default: printf(":-)");
}
}
也就是说,控制权在 for
循环内立即传递。根据 C 标准(6.8.5 迭代语句)
4 An iteration statement causes a statement called the loop body to be
executed repeatedly until the controlling expression compares equal to
0. The repetition occurs regardless of whether the loop body is entered
from the iteration statement or by a jump.
表示for
循环将包含一条语句:
printf(":-)");
将被执行。
但是绕过了for
循环中变量j
的初始初始化。来自 C 标准(6.2.4 对象的存储持续时间)
6 For such an object that does not have a variable length array type,
its lifetime extends from entry into the block with which it is
associated until execution of that block ends in any way. (Entering an
enclosed block or calling a function suspends, but does not end,
execution of the current block.) If the block is entered recursively,
a new instance of the object is created each time. The initial value
of the object is indeterminate. If an initialization is specified
for the object, it is performed each time the declaration or compound
literal is reached in the execution of the block; otherwise, the value
becomes indeterminate each time the declaration is reached.
因此,变量 j
具有不确定的值。这意味着 for
循环以及函数本身具有未定义的行为。
在玩代码时我注意到一个奇怪的行为,我不知道如何解释背后的逻辑
void foo(int n)
{
int m = n;
while (--n > 0)
{
switch (n)
{
case -1:
case 0:
for (int j = 0; j < m; ++j)
default:
printf(":-)");
break;
}
}
}
int main()
{
foo(10);
return 0;
}
我希望 printf
执行比方说 10
次。然后我看到它继续 运行(想象 100000 而不是 10)并且假设开发人员 (VS) 解释 printf
inside for
(非常符合预期),因此每次进入 switch
.
n
次
但后来发现 j
从未被初始化。
所以我的问题是为什么?这是未定义的行为吗?这不是所谓的标准代码吗?
default
只是一个标签(如果 n
不是 -1 或 0,则代码跳转到的地址)。
因此,当 n
不是 -1 或 0 时,流程进入 for
循环的主体,跳过 j
的初始化。您也可以编写与此相同的代码,这样会更清楚这里发生的事情:
int m = n;
while (--n > 0)
{
switch (n)
{
case -1:
case 0:
for (int j = 0; j < m; ++j)
{
default: printf(":-)");
}
break;
}
}
(注意,正如@alagner 在评论中提到的,它不能用 C++ 编译器编译,但可以用 C 编译器完美编译,所以这足以说明我的观点并解释代码的外观)。
所以是的,因为 j
未初始化,所以它是未定义的行为。
如果您启用编译器警告,它会警告您 (https://godbolt.org/z/rzGraP):
warning: 'j' may be used uninitialized in this function [-Wmaybe-uninitialized]
12 | for (int j = 0; j < m; ++j)
| ^
一个switch
块实际上是一组美化的goto
语句。不同的情况不会向代码引入范围或任何逻辑结构。它们实际上只是 switch
语句要跳转到的目标。
在此程序中,default:
标签位于嵌套的 for
循环内。当遇到 default
情况时,程序跳转到循环内,就好像那里有一个 goto
语句一样。 switch
块等同于:
if (n == -1 || n == 0) {
goto before_loop;
}
else {
goto inside_loop;
}
before_loop:
for (int j = 0; j < m; ++j) {
inside_loop:
printf(":-)");
}
这很危险,因为跳转到 inside_loop:
会跳过 j = 0
。正如您所观察到的,j
仍被声明但未初始化,访问它会导致未定义的行为。
正如发布的那样,代码具有未定义的行为,因为当 switch
跳转到 default:
标签时,在 for
语句主体内,它跳过了 [=14= 的初始化] 在内部循环中,当测试 j
并在循环迭代时递减时导致未定义的行为。
在 C++ 中不允许直接跳转到新范围来跳过初始化。这种约束在 C 语言中不存在,为了与历史代码的兼容性不一定会导致问题,但现代编译器会检测到此错误并抱怨。我建议使用 -Wall -Wextra -Werror
以避免愚蠢的错误。
注意修改如下,变成完全定义,打印:)
90次(外循环9次迭代,内循环10次迭代)并成功完成:
#include <stdio.h>
void foo(int n) {
int m = n;
while (--n > 0) {
int j = 0;
switch (n) {
case -1:
case 0:
for (j = 0; j < m; ++j)
default:
printf(":-)");
break;
}
}
}
int main() {
foo(10);
printf("\n");
return 0;
}
很多很好的解释,但缺少关键点:编译器在 printf
语句之后放置了一条 jmp
指令,因为它只编译了一条 for
语句。 jmp
跳转到循环条件并在那里继续(使用未初始化的 j
)。
对于初学者来说,函数 foo
具有 return 类型 int
return 什么都没有。
while
循环:
while (--n > 0)
{
//..
}
仅在预减表达式--n
的值大于0
的情况下获得控制权。
也就是说,在while
循环中,变量n
既不等于0
也不等于-1
。
因此,控制将立即传递给 switch
语句中的标签 default
。
switch (n)
{
case -1:
case 0:
for (int j = 0; j < m; ++j)
default:
printf(":-)");
break;
}
您可以等效地重写没有 switch
语句的 while
循环,如下所示:
while (--n > 0)
{
goto Default;
for (int j = 0; j < m; ++j)
{
Default: printf(":-)");
}
}
也就是说,控制权在 for
循环内立即传递。根据 C 标准(6.8.5 迭代语句)
4 An iteration statement causes a statement called the loop body to be executed repeatedly until the controlling expression compares equal to 0. The repetition occurs regardless of whether the loop body is entered from the iteration statement or by a jump.
表示for
循环将包含一条语句:
printf(":-)");
将被执行。
但是绕过了for
循环中变量j
的初始初始化。来自 C 标准(6.2.4 对象的存储持续时间)
6 For such an object that does not have a variable length array type, its lifetime extends from entry into the block with which it is associated until execution of that block ends in any way. (Entering an enclosed block or calling a function suspends, but does not end, execution of the current block.) If the block is entered recursively, a new instance of the object is created each time. The initial value of the object is indeterminate. If an initialization is specified for the object, it is performed each time the declaration or compound literal is reached in the execution of the block; otherwise, the value becomes indeterminate each time the declaration is reached.
因此,变量 j
具有不确定的值。这意味着 for
循环以及函数本身具有未定义的行为。