【问题标题】:Skip variable declaration using goto?使用 goto 跳过变量声明?
【发布时间】:2015-07-05 00:37:51
【问题描述】:

我正在阅读 K.N.King 的 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]);

【问题讨论】:

  • @Mints97 啊,即使没有复合语句,if 语句也有自己的块?我认为这就是答案:) 太糟糕了,我不能接受 cmets
  • 你这是什么意思? if 语句与此有什么关系?块和复合语句或多或少相同,IIRC
  • @Mints97 我的意思是有条件声明的变量不会移到整个函数的开头,而只是移到它们存在的条件“块”的开头吗?因此,没有复合语句的 if 语句也将代表这样的块。我的理解正确吗?
  • 没有。块是块,别名是复合语句,以 { 开头,以 } 结尾。我关于声明被“移到声明它们的块的开头”的评论非常不清楚,所以我删除了评论并试图用一个答案来详细说明这个主题。是的,“条件声明”不是一回事。

标签: c arrays goto variable-declaration


【解决方案1】:

我很乐意在没有血腥内存布局细节的情况下解释这一点(相信我,使用 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 声明是有原因的 - 完全禁止它们是处理上述情况的最合乎逻辑的决定。

【讨论】:

  • 即使在 C89/C90 中,跳入块也是合法的:goto LABEL; { int n = 42; LABEL: printf("%d\n", n); }
  • @KeithThompson:哇,谢谢!我完全忘记了那个=)我会立即编辑答案!
  • @KeithThompson 提到的一个惊人/可怕的例子被称为Duff's device。它使用开关标签跳转到 while 循环的中间。
  • "但声明语句仍然不同于常规语句"——一个术语上的狡辩。在 C 中,声明不是语句。 (只是为了增加混淆,在 C++ 中,“语句”一词包括 C 所谓的“语句”和“声明”。)
【解决方案2】:

不允许您跳过可变长度数组 (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++ 不允许 gotos 跳过对象声明(或普通旧数据类型的初始化,例如 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 的值。

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

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

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-01-13
      • 1970-01-01
      • 2016-04-04
      • 1970-01-01
      • 2020-08-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多