使用goto跳过变量声明?

时间:2015-04-26 17:18:57

标签: c arrays goto variable-declaration

我正在阅读KNKing的 C Programming - A Modern Approach 来学习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]);

3 个答案:

答案 0 :(得分:9)

我有心情解释这个没有血腥的记忆布局细节(相信我,他们在使用VLA时会得到非常 gory;请参阅@ Ulfalizer的答案了解详情)。

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

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

这直接意味着一件非常重要的事情:一个块==一组不变的变量声明。

C99改变了这一点。在其中,您可以在块的任何部分声明变量,但声明语句仍然与常规语句不同。

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

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

这就是为什么你不能跳过宣言&#34;。声明的变量仍然存在。

问题是初始化。它没有被移动&#34;任何地方。因此,从技术上讲,对于您的情况,以下程序可视为等效:

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可能具有完全不同的内存布局,具体取决于它的声明位置。你无法移动&#34;它就在你跳过的地方之外。

你可能会问,&#34;好吧,为什么不这样做,以便声明不受goto&#34;?的影响?好吧,你仍然可以得到这样的案例:

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

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

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

答案 1 :(得分:5)

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

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

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

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

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

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

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

答案 2 :(得分:3)

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

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

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

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

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

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