使用 goto 跳过变量声明?

Skip variable declaration using goto?

我正在阅读 C Programming - A Modern Approach 以学习 C 编程语言 K.N.King 并且注意到 goto 语句不能跳过可变长度数组声明。

但现在的问题是:为什么goto跳转允许跳过​​定长数组声明和普通声明?更准确地说,根据 C99 标准,这些示例的行为是什么?当我测试这些案例时,似乎声明实际上没有被跳过,但这是正确的吗?声明可能被跳过的变量可以安全使用吗?

1.

goto later;
int a = 4;
later:
printf("%d", a);

2.

goto later;
int a;
later:
a = 4;
printf("%d", a);

3.

goto later;
int a[4];
a[0] = 1;
later:
a[1] = 2;
for(int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
  printf("%d\n", a[i]);

我想在没有血淋淋的内存布局细节的情况下解释这一点(相信我,当使用 VLA 时,它们会变得 非常 血腥;有关详细信息,请参阅@Ulfalizer 的回答) .

所以,最初,在 C89 中,必须在块的开头声明所有变量,如下所示:

{
    int a = 1;
    a++;
    /* ... */
}

这直接暗示了一件非常重要的事情:一个块==一组不变的变量声明。

C99 改变了这个。在里面,你可以在块的任何部分声明变量,但是声明语句和普通语句还是有区别的。

事实上,为了理解这一点,您可以想象所有变量声明都被隐式移动到声明它们的块的开头,并且使其对前面的所有语句不可用。

这仅仅是因为 一个块 == 一组声明 规则仍然有效。

这就是为什么你不能 "jump over a declaration"。声明的变量仍然存在。

问题是初始化。它不会在任何地方得到 "moved"。因此,从技术上讲,对于您的情况,可以认为以下程序是等效的:

goto later;
int a = 100;
later:
printf("%d", a);

int a;
goto later;
a = 100;
later:
printf("%d", a);

如你所见,声明还在,跳过的是初始化。

这不适用于 VLA 的原因是它们不同。简而言之,这是因为这是有效的:

int size = 7;
int test[size];

与所有其他声明不同,VLA 的声明在声明它们的块的不同部分表现不同。事实上,一个 VLA 可能有完全不同的内存布局,这取决于它声明的位置。你不能 "move" 它超出你刚刚跳过的地方。

你可能会问,"all right, then why not make it so that the declaration would be unaffected by the goto"?好吧,你仍然会遇到这样的情况:

goto later;
int size = 7;
int test[size];
later:

您实际上希望它做什么?..

因此,禁止跳过 VLA 声明是有原因的 - 通过简单地完全禁止它们来处理上述情况是最合乎逻辑的决定。

不允许您跳过可变长度数组 (VLA) 声明的原因是它会扰乱 VLA 的通常实现方式,并且会使语言的语义复杂化。

VLA 在实践中可能实现的方式是通过动态(在 运行 时间计算)量递减(或递增,在堆栈向上增长的体系结构上)堆栈指针,以便为堆栈上的 VLA。这发生在声明 VLA 的地方(至少在概念上,忽略优化)。这是必需的,以便以后的堆栈操作(例如,将参数推送到堆栈以进行函数调用)不会占用 VLA 的内存。

对于嵌套在块中的 VLA,堆栈指针通常会在包含 VLA 的块的末尾恢复。如果允许 goto 跳入这样的块并通过 VLA 的声明,则恢复堆栈指针的代码将 运行 而相应的初始化代码已 运行,这可能会导致问题。例如,堆栈指针可能会增加 VLA 的大小,即使它从未减少过,除其他外,这会使调用包含 VLA 的函数时被压入的 return 地址出现在相对于堆栈指针的错误位置。

从纯语言语义的角度看也是乱七八糟的。如果允许您跳过声明,那么数组的大小是多少?什么应该sizeofreturn?访问它是什么意思?

对于非 VLA 情况,您只需跳过值初始化(如果有),这本身并不一定会导致问题。如果您跳过像 int x; 这样的非 VLA 定义,那么存储空间仍将保留给变量 x。 VLA 的不同之处在于它们的大小是在 运行 时计算的,这使事情变得复杂。

附带说明一下,允许在 C99 块内的任何位置声明变量的动机之一(C89 要求声明位于块的开头,但至少 GCC 允许它们在块内作为扩展名)是为了支持 VLA。能够在声明 VLA 的大小之前在块中更早地执行计算是很方便的。

出于某些相关原因,C++ 不允许 gotos 跳过对象声明(或对普通旧数据类型的初始化,例如 int)。这是因为跳过调用构造函数但仍在块末尾 运行 析构函数的代码是不安全的。

使用 goto 跳过变量声明几乎可以肯定是一个非常糟糕的主意,但它是完全合法的。

C 区分变量的 lifetime 和它的 scope.

对于函数内未使用 static 关键字声明的变量,其范围(其名称可见的程序文本区域)从定义扩展到最近的封闭块的末尾。它的生命周期(存储持续时间)从进入区块开始,到退出区块结束。如果它有一个初始化器,它会在(如果)达到定义时执行。

例如:

{  /* the lifetime of x and y starts here; memory is allocated for both */
    int x = 10; /* the name x is visible from here to the "}" */
    int y = 20; /* the name y is visible from here to the "}" */
    int vla[y]; /* vla is visible, and its lifetime begins here */
    /* ... */
}

对于可变长度数组 (VLA),标识符的可见性是相同的,但对象的生命周期从定义开始。为什么?因为在该点之前不一定知道数组的长度。在示例中,不可能在块的开头为 vla 分配内存,因为我们还不知道 y 的值。

跳过对象定义的 goto 会绕过该对象的任何初始值设定项,但仍会为其分配内存。如果 goto 跳入一个块,则在进入该块时分配内存。如果没有(如果 goto 和目标标签在同一块中处于同一级别),则该对象已经分配。

...
goto LABEL;
{
    int x = 10;
    LABEL: printf("x = %d\n", x);
}

printf语句执行时,x存在并且名字可见,但是初始化被绕过了,所以是一个不确定的值。

该语言禁止 goto 跳过可变长度数组的定义。如果允许,它将跳过为对象分配内存,任何引用它的尝试都会导致未定义的行为。

goto 语句 do have their uses。使用它们来跳过声明,尽管语言允许这样做,但这不是其中之一。