【问题标题】:GOTO before local variable局部变量前的 GOTO
【发布时间】:2014-08-06 12:38:29
【问题描述】:

以下代码是否构成未定义的行为,因为我在变量声明之前跳转并通过指针使用它?如果有,标准之间是否存在差异?

int main() {
  int *p = 0;
label1: 
  if (p) {
    printf("%d\n", *p);
    return 0;
  }
  int i = 999;
  p = &i;
  goto label1;
  return -1;
}

【问题讨论】:

  • 我没有未定义的行为。
  • @ouah 你的措辞让我想知道你是如何得出这个结论的。
  • @MikeW 这无关。这个问题的关键部分是p = &i这一行,p是否可以读到printf()这一行
  • 对我来说看起来不错。请注意,以后的编译器可能将声明提升到函数的顶部,因此您的代码可能无论如何都无法编译为该结构。

标签: c declaration undefined-behavior goto


【解决方案1】:

您的程序中没有未定义的行为。

goto 语句有两个约束:

(c11, 6.8.6.1p1) “goto 语句中的标识符应命名位于封闭函数某处的标签。goto 语句不应从具有可变修改类型的标识符范围之外跳转到该标识符的范围。”

您没有违反,并且在约束之外没有其他应要求。

请注意,在 c99 和 c90 中它是相同的(在某种意义上没有额外的要求)。当然,在 c90 中,由于声明和语句的混合,程序将无效。

关于i 对象在goto 语句之后访问时的生命周期,C 说(请参阅我的重点,以下段落中的其他复制句子对于更棘手的程序会很有趣):

(c11, 6.2.4p6) "对于这样一个没有可变长度数组类型的对象,它的生命周期从进入与其关联的块开始,直到该块的执行以任何方式结束. [...] 如果递归进入块,则每次都会创建对象的新实例。[...] 如果为对象指定了初始化,则每次声明或在块的执行中达到复合文字;否则,每次达到声明时,值变得不确定。"

这意味着,当*p 被读取时,i 仍然存在;没有对象在其生命周期之外被访问。

【讨论】:

  • 你没抓住重点:p 现在指向的变量i 还能被读取吗?这个问题相当于编译器是否必须提升变量定义。
  • @Kay 我完全理解这个问题,并且在我的回答中,我认为p 没有理由指向i 对象以外的其他地方。
  • @Kay i 的生命周期以} 或函数返回结束。
  • 第二个引号使我们不清楚i 的值在其块的开头{ 之后但在int i = 999; 行之前访问时应该是什么值。此外,似乎还不清楚使用 goto 是否是“递归输入”。我会说它不是,并且 6.2.4/6 引用正在谈论,例如,一个函数被递归调用。
  • @MattMcNabb Derek M. Jones 的新 C 标准也谈到了这句话 “使用 goto 语句跳回到块的递归开头不是递归调用那个块。”
【解决方案2】:

我将尝试回答您可能一直在尝试提出的问题。

您的程序的行为是明确定义的。 (return -1; 有问题;只有0EXIT_SUCCESSEXIT_FAILURE 被明确定义为从main 返回的值。但这不是你要问的。)

这个程序:

#include <stdio.h>
int main(void) {
    goto LABEL;
    int *p = 0;
    LABEL:
    if (p) {
        printf("%d\n", *p);
    }
}

确实有未定义的行为。 goto 将控制权转移到p 范围内的某个点,但绕过了它的初始化,因此在执行if (p) 测试时p 具有不确定的值。

在您的程序中,p 的值在任何时候都是明确定义的。在goto 之前到达的声明将p 设置为0(一个空指针)。 if (p) 测试是假的,所以if 语句的主体不会第一次执行。 gotop 被赋予一个明确定义的非空值之后执行。在goto之后,if (p)测试为真,执行printf调用。

在您的程序中,pi生命周期 在达到 main 的开头 { 时开始,并在达到结尾 } 时结束,或者执行return 语句。每个的范围(即,程序文本的名称可见的区域)从其声明延伸到结束}。当goto 向后转移控制时,变量名称i 超出范围,但该名称所指的int object 仍然存在。名称 p 在范围内(因为它已在前面声明),并且指针对象仍指向同一个 int 对象(如果该名称可见,则其名称将为 i)。

请记住,scope 指的是程序文本的一个区域,其中名称是可见的,而 lifetime 指的是程序执行期间对象存在的时间跨度。保证存在。

通常,如果一个对象的声明有一个初始值设定项,则保证它在其名称可见时具有有效值(除非稍后为其分配了一些无效值)。这可以通过gotoswitch 绕过(但如果小心使用它们就不行)。

【讨论】:

  • @ryyker:如果这实际上不是 OP 所要问的,那是一个错误的猜测。
  • "并执行 printf 调用。" - 它是否打印999?在声明之前,我没有清楚地解释变量的值是什么。 (此时i 的生命周期已经开始,但i 超出范围)。另一个例子,如果 return 0; 被替换为 *p = 998; ,它是否会在“循环”周围第二次回到 999 ?
  • 至少有指导意义。而且,我总是喜欢你对与void main(void) 相关的任何事情的热情。 哦,对不起,我的意思是:int main(void)
  • 在声明变量之前,我没有清楚地解释变量的值是什么。 生命周期确实已经开始,对象的值在 @987654364 之前是不确定的@ 到达了。它在 6.2.4p6 中指定。
【解决方案3】:

此代码没有未定义的行为。我们可以在6.2.4 部分的 Rationale for International Standard—Programming Languages—C 中找到一个很好的例子对象的存储持续时间 它说:

[...]有一个简单的经验法则:声明的变量被创建 输入块时具有未指定的值,但 初始化器被评估并且值被放置在变量中时 在正常执行过程中达到声明。这样一跳 向前超过声明使其未初始化,而跳转 向后将导致它被多次初始化。如果 声明不会初始化变量,它会将其设置为 未指定的值,即使这不是第一次声明 已到达。

变量的作用域从它的声明开始。因此,虽然 变量一进入块就存在,不能 在到达声明之前按名称引用。

并提供以下示例:

int j = 42;
{
   int i = 0;
 loop:
   printf("I = %4d, ", i);
   printf("J1 = %4d, ", ++j);
   int j = i;
   printf("J2 = %4d, ", ++j);
   int k;
   printf("K1 = %4d, ", k);
   k = i * 10;
   printf("K2 = %4d, ", k);
   if (i % 2 == 0) goto skip;
    int m = i * 5;
skip:
  printf("M = %4d\n", m);
  if (++i < 5) goto loop;
}

输出是:

 I = 0, J1 = 43, J2 = 1, K1 = ????, K2 = 0, M = ????
 I = 1, J1 = 44, J2 = 2, K1 = ????, K2 = 10, M = 5
 I = 2, J1 = 45, J2 = 3, K1 = ????, K2 = 20, M = 5
 I = 3, J1 = 46, J2 = 4, K1 = ????, K2 = 30, M = 15
 I = 4, J1 = 47, J2 = 5, K1 = ????, K2 = 40, M = 15

它说:

在哪里“??????”表示一个不确定的值(以及任何使用 不确定的值是未定义的行为)。

此示例与草案 C99 标准部分 6.2.4 对象的存储持续时间5 段一致,其中说:

对于这样一个没有变长数组类型的对象, 它的生命周期从进入它所在的块开始 关联,直到该块的执行以任何方式结束。 (输入一个 封闭的块或调用函数挂起,但没有结束, 执行当前块。)如果块是递归进入的, 每次都会创建一个新的对象实例。初始值 对象的不确定性。如果指定了初始化 对象,每次到达声明时都会执行 块的执行;否则,值变得不确定 每次达到声明。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-08-02
    • 2011-11-06
    • 2018-01-12
    • 1970-01-01
    相关资源
    最近更新 更多