2015-04-26 135 views
5

我正在阅读K.N.King的C Programming - A Modern Approach学习C编程语言,并注意到goto语句不得跳过可变长度数组声明。使用goto跳过变量声明?

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

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

2.

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

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]); 
+0

@ Mints97嗯,所以如果语句有自己的块,即使没有复合语句?我认为这就是答案:)太糟糕我不能接受评论 – MinecraftShamrock

+0

你是什么意思?如果陈述与此有什么关系呢?块和复合语句差不多,IIRC – Mints97

+0

@ Mints97我的意思是条件声明的变量不会移动到整个函数的开始处,而是仅移动到条件“块”的开头,它们存在于右侧?所以没有复合语句的if语句也会代表这样的块。我的理解是否正确? – MinecraftShamrock

回答

9

我的心情解释这一点没有血淋淋的内存 - 啦(相信我,他们得到非常血腥时使用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); 

正如你可以看到,该声明仍然存在,被跳过的是初始化。

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

int size = 7; 
int test[size]; 

沃拉斯的声明会,与所有其他声明,行为不同,他们被宣布块的不同部分。事实上,根据VLA声明的位置,VLA可能有完全不同的内存布局。你无法将它“移动”到你刚刚跳过的地方以外。

您可能会问,“为什么不这样做,以便声明不受goto的影响”?那么,你仍然会得到类似案例:

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

你怎么居然想到这个做..

所以,禁止跳跃过VLA声明是有原因的 - 它是最?通过完全禁止它们来处理类似上述情况的逻辑决策。

+3

即使在C89/C90中,跳转到块也是合法的:goto LABEL ; {int n = 42; LABEL:printf(“%d \ n”,n); }' –

+0

@KeithThompson:哇,谢谢!我完全忘了那个=)我马上编辑答案! – Mints97

+1

@KeithThompson提到的惊人/可怕的例子被称为[Duff's device](https://en.wikipedia.org/wiki/Duff%27s_device)。它使用开关标签跳到一个while循环的中间。 –

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)。这是因为跳过调用构造方法的代码是不安全的,但仍然在块的末尾运行析构函数。

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的值。

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

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

当执行printf声明,x存在,它的名称是可见的,但它的初始化被忽视,所以它有一个不确定的值。

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

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