C 块作用域

C block scoping

我正在尝试了解 C 中块作用域的含义。

我意识到在一个范围内定义的标识符在范围外是不可见的,但是在指令级别上块作用域的含义是什么?进入或退出块作用域是否暗示任何指令,或者它在指令值上是否完全透明?在作用域内定义的变量是否像在循环构造中一样被销毁?

在指令层面,优化后如下:

initialise:
    int a = 0;
block_entry:
    a += 1;
    /* on first pass (initialisation): a == 1 */
    /* on second pass (entry by goto): a==2 ? */
    if (a==2): goto done

goto block_entry
done:

任何不同于:

{
initialise:
    int a = 0;
block_entry:
    a += 1;
    /* on first pass (initialisation): a == 1 */
    /* on second pass (entry by goto): a==2 ? */
    if (a==2): goto done
}

goto block_entry
done:

或来自:

while(1){
initialise:
    int a = 0;
block_entry:
    a += 1;
    /* on first pass (initialisation): a == 1 */
    /* on second pass (entry by goto): a == 2 ? */
    if (a==2): goto done
    goto main_code
}

main_code:
goto block_entry
done:

这个问题主要是学术性的,受到 Eli Bendersky's post "Computed goto for efficient dispatch tables" 的启发,他似乎使用 while(1) {...} 循环纯粹用于视觉结构。 (具体在 interp_cgoto(...) 函数中。)

如果他使用块作用域进行可视化结构化或根本不使用作用域,他的代码执行编译时会有什么不同吗? (即删除 while(1) {...} 循环。)

C语言不支持构造函数和析构函数。所以进入和离开作用域不会导致调用 "destructors"。

不同作用域内的变量可以共享同一个内存或寄存器,所以下面的代码

{
    char buffer[2048];
    /*...*/
}
{
   char stuff[2048];
   /*....*/
}

可能会用完 2k 或 4k 的堆栈,具体取决于编译器的决定。理论上它可以创建

union {
   char buff[2048];
   char stuff[2048];
};

因此,如果编译器认为有必要,创建作用域可以缩小函数的堆栈和寄存器要求。我在你的代码中看不到这样的优势。

在您的第二个和第三个片段中,goto block_entry; 导致未定义的行为;而第一个片段没问题(第二遍a == 2)。

如果你goto从一个块外进入一个块,在一个带有初始化器的变量声明之后;那么该变量存在但未应用初始化程序;该变量的行为类似于未初始化的变量。

块内定义的变量在概念上会在块退出时销毁。这通常不会转化为任何实际的汇编指令,它只会反映在堆栈中您会在函数中找到不同变量的位置。

片段二和片段三的行为未定义,因为变量 a 的生命周期在声明它的块退出时结束(即使退出是通过 goto ).重新输入该块时,将创建一个新的 a,其初始值不确定。由于 goto 跳过声明语句,a 的值仍然不确定。随后尝试 使用 该值 (a += 1;) 会导致未定义的行为。

这里有一个例子,实际上似乎证明了实践中的未定义行为:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv) {
    {
initialise:;
        int a[10] = {0};
block_entry:
        a[0] += 1;
        printf("a is %d\n", a[0]);
        /* on first pass (initialisation): a == 1 */
        /* on second pass (entry by goto): a==2 ? */
        if (a[0]>=2) goto done;
    }
    {
        int x[10];
        x[0] = argc > 1 ? atoi(argv[1]) : 42;
        printf("x is %d\n", x[0]);
    }

    goto block_entry;
done:
    puts("Done");
    return 0;
}

(Live on coliru)

我修正了几个拼写错误(其中伪代码是 C 和 Python 的混合),并添加了另一个可以重用堆栈的块。出于显而易见的原因,我将终止条件更改为 >=

在 gcc 等精确版本中,这导致 a[0]x[0] 共享存储,因此第二次循环 a 是 43 而不是 2。

如果您将数组的大小更改为更小的值,则 gcc 不会将它们放在相同的堆栈位置,您会得到原始片段的行为,其中 a 是 2第二遍.

另一方面,如果您使用 -O3 而不是 -O0,则 gcc 会编译一个无限循环,其中 a 始终为 1。

所有这些结果都是可以接受的,因为未定义的行为对编译器没有约束

简而言之,不要那样做 (sm)。