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;
}
我修正了几个拼写错误(其中伪代码是 C 和 Python 的混合),并添加了另一个可以重用堆栈的块。出于显而易见的原因,我将终止条件更改为 >=
。
在 gcc 等精确版本中,这导致 a[0]
和 x[0]
共享存储,因此第二次循环 a
是 43 而不是 2。
如果您将数组的大小更改为更小的值,则 gcc 不会将它们放在相同的堆栈位置,您会得到原始片段的行为,其中 a
是 2第二遍.
另一方面,如果您使用 -O3
而不是 -O0
,则 gcc 会编译一个无限循环,其中 a
始终为 1。
所有这些结果都是可以接受的,因为未定义的行为对编译器没有约束。
简而言之,不要那样做 (sm)。
我正在尝试了解 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;
}
我修正了几个拼写错误(其中伪代码是 C 和 Python 的混合),并添加了另一个可以重用堆栈的块。出于显而易见的原因,我将终止条件更改为 >=
。
在 gcc 等精确版本中,这导致 a[0]
和 x[0]
共享存储,因此第二次循环 a
是 43 而不是 2。
如果您将数组的大小更改为更小的值,则 gcc 不会将它们放在相同的堆栈位置,您会得到原始片段的行为,其中 a
是 2第二遍.
另一方面,如果您使用 -O3
而不是 -O0
,则 gcc 会编译一个无限循环,其中 a
始终为 1。
所有这些结果都是可以接受的,因为未定义的行为对编译器没有约束。
简而言之,不要那样做 (sm)。