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 循环以及函数本身具有未定义的行为。