for 循环在 switch case 之间拆分的行为

dEm*_*gOd 22 c c++ duffs-device

在玩代码时,我注意到一个奇怪的行为,我不知道解释背后的逻辑

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;
}
Run Code Online (Sandbox Code Playgroud)

我希望printf执行比方说的10时间。然后,我看到它继续运行(想象一下100000,而不是10),并假定开发(VS)解释了printf 内部for(漂亮的预期),因此输出由n次,每次入口switch

但后来证明j从未初始化。

所以我的问题是为什么?这是未定义的行为吗?这不是一个,据说,标准代码?

Ale*_*op. 27

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

(注意,正如@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)
      |                          ^                  
Run Code Online (Sandbox Code Playgroud)

  • 或者甚至在编译为 C++ 时无法构建。我不是“语言律师”,但看起来 C 与 C++ 之间的微妙之处开始显现:https://godbolt.org/z/MhjxGG (3认同)

Joh*_*ica 11

一个switch块实际上是一组美化的goto语句。不同的情况不会向代码引入范围或任何逻辑结构。它们实际上只是switch语句跳转到的目标。

在此程序中,default:标签位于嵌套for循环内。当default遇到 case 时,程序会跳转到循环内,就好像那里有一条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(":-)");
}
Run Code Online (Sandbox Code Playgroud)

这是危险的,因为跳转到inside_loop:跳过j = 0。正如您所观察到的,j仍然是声明的,但没有初始化,访问它会导致未定义的行为。


chq*_*lie 7

正如发布的那样,代码具有未定义的行为,因为当switch跳转到语句体default:内部的标签时,for它会跳过j内部循环中的初始化,在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;
}
Run Code Online (Sandbox Code Playgroud)


Vla*_*cow 5

对于初学者来说,foo具有返回类型的函数不int返回任何内容。

while循环:

while (--n > 0)
{
    //..
}
Run Code Online (Sandbox Code Playgroud)

仅在 pre-decrement 表达式的--n值大于的情况下获得控制权0

也就是说,在while循环中,变量n既不等于0也不到-1

因此,控制将立即向标签传递default的范围内switch声明。

    switch (n)
    {
        case -1:
        case 0:
            for (int j = 0; j < m; ++j)
        default:
                printf(":-)");
            break;
    }
Run Code Online (Sandbox Code Playgroud)

您可以通过以下方式等效地重写while没有switch语句的循环,以使其更清晰:

while (--n > 0)
{
    goto Default;

    for (int j = 0; j < m; ++j)
    {
        Default: printf(":-)");
    } 
}
Run Code Online (Sandbox Code Playgroud)

也就是说,控制立即在for循环内部传递。根据 C 标准(6.8.5 迭代语句)

4 迭代语句使称为循环体的语句重复执行,直到控制表达式比较等于 0。无论循环体是从迭代语句输入还是通过跳转,都会发生重复

这意味着for循环将包含一个语句:

printf(":-)");
Run Code Online (Sandbox Code Playgroud)

将被执行。

然而,该变量的初始初始化jfor环路被旁路。来自 C 标准(6.2.4 对象的存储持续时间)

6 对于这种没有可变长度数组类型的对象,其生命周期从进入与其关联的块开始,直到该块的执行以任何方式结束。(进入封闭块或调用函数会挂起,但不会结束当前块的执行。)如果递归进入块,则每次都会创建对象的一个​​新实例。对象的初始值是不确定的。如果为对象指定了初始化,则在块的执行中每次到达声明或复合文字时都会执行它;否则,每次到达声明时,该值都会变得不确定。

因此,该变量j 具有不确定的值。这意味着for循环以及函数本身具有未定义的行为。