使用goto跳过变量声明?

我正在阅读C编程 – KNKing 的现代方法来学习C编程语言,并且注意到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改变了这一点。 在其中,您可以在块的任何部分声明变量,但声明语句仍然与常规语句不同。

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

这只是因为一个块==一组声明规则仍然成立。

这就是为什么你不能“跳过宣言”。 声明的变量仍然存在。

问题是初始化。 它不会在任何地方“移动”。 因此,从技术上讲,对于您的情况,以下程序可视为等效:

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

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

如您所见,声明仍然存在,正在跳过的是初始化。

这与VGA不兼容的原因是它们不同。 简而言之,这是因为这是有效的:

 int size = 7; int test[size]; 

与所有其他声明不同,VLA的声明在声明它们的块的不同部分中表现不同。 实际上,VLA可能具有完全不同的内存布局,具体取决于它的声明位置。 你只是不能将它“移动”到你跳过的地方之外。

你可能会问,“好吧,为什么不这样做才能使声明不受goto影响”? 好吧,你仍然会得到这样的案例:

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

你实际上期望这样做什么?..

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

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

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

对于嵌套在块中的VLA,堆栈指针通常在包含VLA的块的末尾被恢复。 如果允许goto跳转到这样的块并超过VLA的声明,那么恢复堆栈指针的代码将在没有运行相应的初始化代码的情况下运行,这可能会导致问题。 例如,堆栈指针可能会增加VLA的大小,即使它从未递减过,除其他外,这将使得当包含VLA的函数被调用时出现的返回地址出现在相对错误的位置到堆栈指针。

从纯语言语义的角度来看,它也很混乱。 如果你被允许跳过声明,那么数组的大小是多少? sizeof应该返回什么? 访问它意味着什么?

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

作为旁注,允许变量在C99中的块内的任何地方声明的动机之一(C89要求声明在块的开头,尽管至少GCC允许它们在块中作为扩展)是支持沃拉斯。 在声明VLA的大小之前能够在块中更早地执行计算是很方便的。

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

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

C区分变量的生命周期范围

对于在函数内部没有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),标识符的可见性是相同的,但对象的生命周期始于定义。 为什么? 因为在该点之前不一定知道arrays的长度。 在该示例中,不可能在块的开头为vla分配内存,因为我们还不知道y的值。

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

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

执行printf语句时, x存在且其名称可见,但其初始化已被绕过,因此它具有不确定的值。

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

goto语句确实有它们的用途 。 使用它们来跳过声明,虽然它是语言允许的,但不是其中之一。