GOTO语句在局部变量之前的问题

26

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

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

1
不要给我带来未定义行为。 - ouah
1
可能是 [Hoisting / Reordering in C、C++和Java:在上下文中变量声明必须始终处于顶部吗?] 的重复问题 (https://dev59.com/833aa4cB1Zd3GeqPithz)。 - user1864610
4
你的措辞让我想知道你是怎么得出那个结论的。 - user395760
9
@MikeW与此无关。这个问题的关键部分是p = &i这一行,以及在printf()中是否可以读取p - Kijewski
1
我觉得看起来不错。请注意,后续的编译器可能会将声明提升到函数顶部,因此您的代码可能无法编译成这种结构。 - user1864610
3个回答

17
你的程序中不存在未定义行为。goto语句有两个限制:

(c11, 6.8.6.1p1) "goto语句中的标识符应命名在封闭函数的某处的标签。goto语句不得从具有可变类型的标识符的作用域外跳到该标识符的作用域内。"

你没有违反这两个要求,除此之外,没有其他的规定。

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

关于goto语句后访问i对象的生存期,C语言规定如下(请参考我的强调,下面的其他复制语句对于更棘手的程序可能会很有趣):

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

这意味着,当读取*p时,i仍然存在,没有访问任何对象的生命周期之外的内容。


@Kay,我完全理解这个问题,并且正如我在答案中所说,我认为p指向的对象应该是i,没有其他原因。 - ouah
5
@Kay 变量 i 的生命周期在 } 或函数返回时结束。 - ouah
2
@MattMcNabb Derek M. Jones 的《新 C 标准》还指出了这句话的含义:“使用goto语句跳回块的递归开头,不是对该块的递归调用。” - ouah
给定 { unsigned char *p; goto LATE; EARLY: printf("%d",(int)*p); return; unsigned char i; printf("%d", (int)*p); return; LATE: p=&i; i=123; goto EARLY; },第一个 printf 会显示 123,第二个则会显示不确定的值,因为后退跳转会使 *p 的值变得不确定。请注意,代码从未真正离开 i 的作用域。 - supercat
@supercat,你如何到达你示例中的第二个printf?我可以建议您提出一个新问题来继续讨论吗? - ouah
显示剩余4条评论

9

我会尝试回答您可能一直想问的问题。

您的程序行为是明确定义的。(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 设置为零(空指针)。if (p) 检查为假,因此第一次不执行语句的主体。在给出定义良好的非空值后,执行 goto. goto 后,if (p) 检查为真,然后执行 printf 调用。

在您的程序中,pi生命周期 从打开 { 的地方进入,到达闭合 } 或执行 return 语句时结束。每个变量的 作用域(即名称可见的程序文本区域)从其声明到闭合 }。当 goto 向后传递控件时,变量名称 i 已经超出范围,但是该名称引用的 int 对象仍然存在。名称 p 是在作用域内(因为它早已声明),并且指针对象仍然指向同一个 int 对象(如果该名称可见,则其名称将为 i)。

请记住,作用域 指的是程序文本区域,在其中名称可见,生命周期 指的是程序执行��间保证对象存在的时间跨度。

通常,如果对象的声明具有初始化项,则可以保证每次其名称可见时都具有有效值(除非稍后将某个无效值分配给它)。这可以通过使用 gotoswitch 来绕过(但是必须小心使用)。


@ryyker:如果那并不是 OP 实际询问的内容,那么这个猜测就是一个错误的猜测。 - Keith Thompson
“并且执行printf调用。” - 它是否打印999?我没有清楚的解释变量在声明之前的值是什么。此时,i的生命周期已经开始,但i超出了范围。另一个例子,如果将return 0;替换为*p = 998;,第二次“循环”会回到999吗? - M.M
至少具有指导意义。而且,我总是喜欢你对任何与void main(void)相关的事情充满热情。哦,抱歉,我的意思是:int main(void) - ryyker
在变量声明之前,我没有看到对变量值的清晰解释。实际上,生命周期已经开始,在达到int i = 999;之前对象的值是不确定的。这在6.2.4p6中有明确规定。 - ouah

6
这段代码不存在未定义的行为。我们可以在《国际标准-编程语言-C的理论基础》的第6.2.4对象的存储期中找到一个很好的例子,它说:

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

变量的作用域始于其声明。因此,尽管变量在进入块时存在,但在到达其声明之前不能用名称引用它。

并提供以下示例:

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 段的规定:

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接