22

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

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 for非常预期),因此输出是n每次进入switch.

但后来竟然j从来没有初始化过。

所以我的问题是为什么?这是未定义的行为吗?这不是所谓的标准代码吗?

4

5 回答 5

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;
    }
}

(注意,正如@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)
      |                          ^                  
于 2021-03-08T07:48:24.723 回答
11

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的,它仍然被声明但它没有被初始化,并且访问它会导致未定义的行为。

于 2021-03-08T07:49:49.677 回答
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;
}
于 2021-03-08T08:30:14.893 回答
5

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

while循环:

while (--n > 0)
{
    //..
}

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

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

因此,控制将立即传递给语句default中的标签。switch

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

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

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

    for (int j = 0; j < m; ++j)
    {
        Default: printf(":-)");
    } 
}

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

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

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

printf(":-)");

将被执行。

j但是,绕过了for循环中变量的初始初始化。来自 C 标准(6.2.4 对象的存储持续时间)

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

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

于 2021-03-08T09:23:55.643 回答
3

很多很好的解释,但缺少关键点:编译器jmp在语句之后放置一条指令,printf因为它只是编译了一条for语句。jmp跳转到循环的条件并在那里继续(使用未初始化的)j

于 2021-03-08T08:38:03.907 回答