C语言中的goto和自动变量初始化器

7
在教程中提到:
如果使用goto语句跳转到块的中间,那么该块内的自动变量将不会被初始化。
那么在下面的代码中,如果可以访问/声明i,为什么它没有被初始化?
int main()
{
   goto here;
   {
     int i=10;
     here:
      printf("%d\n",i);
   }
   return 0;
}

提示:输出结果是一些垃圾值。


3
只需明确地写出初始化,就能更清楚地理解:int i;i=10; (直译) - Argeman
6个回答

12

你的问题“如果可以访问i,为什么......”没有任何逻辑。能够“访问i”不是支持或反对任何事情的论据。它只意味着printf语句与i处于相同的作用域。但是,由于你跳过了初始化程序,这个变量是未初始化的(就像你的教程中所说的一样)。

读取未初始化的变量是未定义行为,因此你的程序是不合法的。

变量i的内存已经在编译时设置好了,因为变量已知存在于内部块中。这个内存并没有动态分配,就像你想象的那样。它已经存在,但由于goto,它从未设置为任何确定的值。

经验法则:不要跨过初始化程序。


1
很有趣 - 因此编译器看到语句int i = 10;并在堆栈上为int分配足够的内存,但不会将其设置为10。如果语句被拆分,例如int i; i = 10;我们可以认为编译器对第一个语句进行操作,但跳过第二个语句。这是一个好问题 - 让你思考。 - bph
4
如果有帮助的话,请记住,在 C 语法中,变量声明不是语句。块中的所有声明都有助于编译器理解代码进入该块时需要在堆栈上提供多少空间,这就是为什么在 C89 中它们必须位于块的开头,在任何语句之前的原因。对这些变量的初始化和赋值与自动变量需要的总大小(例如4个字节)的知识有所区别处理。 - Steve Jessop
1
@vindhya:如果你能读汇编代码,你可以检查生成的代码,但可能发生的情况是,在“goto”处,编译器在跳转之前调整堆栈指针。另一种可能性是,编译器实际上已经将变量“i”的空间提取到块外部,因此在函数开始时为其留出空间,并且在进入和离开块时不再移动堆栈指针。 - Steve Jessop
@SteveJessop:您能否详细说明一下您刚才提到的关于变量空间提升的最后一点?我没有理解。 - Vindhya G
1
为了澄清“i的内存已经在编译时分配”(可能会误导):i是一个本地变量,每次调用函数时都会在堆栈上创建空间,并在函数退出时恢复堆栈 - 编译器添加了代码来在堆栈上创建空间并将变量替换为该空间的地址,在这种情况下是[rbp-4],其中4是int的大小。 - slashmais
显示剩余10条评论

3
变量在其声明的作用域内可见(在这种情况下是在 {} 之间),与该作用域内语句的执行顺序无关。goto 跳过了 i 的初始化,这意味着当调用 printf() 时它具有未定义的值。

2
考虑另一个显而易见的情况:
int main()
{
    int i; //i is declared, but not initialized
    goto here;
    {
       i=10;//i is initialized 
       here: //you've skipped the initialization
       printf("%d\n",i);//and got garbage
    }
return 0;
}

针对你的情况:

int main()
{

    goto here;
    {
       //printf("%d\n",i);  // i does not exist here yet
       int i; //from here until the end of the scope variable i exists
       i=10;  // i exists here and smth is written into it
   here:  // i exists here
       printf("%d\n",i); // i exists here and it's value is accessed
    }
return 0;
}

所以,int i = 5; 这其实是两件事情。第一是声明,它不能被任何东西跳过,包括 goto(就像打开新作用域也不受影响一样。你已经跳到了作用域的中间,但该作用域已经存在)。第二是操作赋值,由于它是正常的操作(程序流程),因此可以被 goto、'break'、'continue' 或 'return' 跳过。


虽然我猜想您在口语上使用了"initialize",但在C语言中,"assignment"和"initialization"被正式视为两种不同的事情,即使它们都可以用=表示。只需尝试int a[3]; a = { 1, 2, 3};int a[3] = {1, 2, 3};进行比较就可以了。 - dmckee --- ex-moderator kitten
@dmckee,我在使用“initialize”一词时是指“对一个从未被赋值且包含垃圾数据的变量进行赋值”。数组初始化器本身就是一个非常巧妙的例外。因此,不考虑它可以让问题更加清晰明了。 - Agent_L
我理解,但标准使用“赋值”和“初始化”这些词,在正式的上下文中它们是不同的。当你非正式地写作时,这会带来困扰,因为总会有引入混淆的风险。 - dmckee --- ex-moderator kitten
@dmckee 标准使用了许多与常识相反的词语,并坚持在每个其他行为都被实现完美定义时称之为 UB。因此,在我看来,标准最好用于使互联网对手眼花,而不是真正帮助某人理解某些东西。 - Agent_L

0

C编译器将解析源文件并“记录”任何变量初始化。
当它到达

printf("%d\n", i)

它将知道变量i已经存在,因此应该能够使用它,因为它在作用域内。
在执行空间中,在调用main函数之后,在main()代码的任何执行之前,栈上为i变量保留了空间。


0

因为语言标准规定:

6.7.8 初始化

语义

如果一个具有自动存储期的对象没有被显式初始化,则其值是不确定的。

J.2 未定义行为

在以下情况下,行为是未定义的:

在具有自动存储期的对象的值不确定时使用该对象的值。

6.8.4.2 switch语句

例子:在人工程序片段中

switch (expr)
{
  int i = 4;
  f(i);
  case 0:
    i = 17;
    /* falls through into default code */
  default:
    printf("%d\n", i);
}

标识符为 i 的对象具有自动存储期(在块内),但从未被初始化。因此,如果控制表达式具有非零值,则对 printf 函数的调用将访问一个不确定的值。类似地,无法访问函数 f。


附录J仅供参考。实际上,根据C11标准,6.3.2.1 p2更加精确和规范。只有当“auto”变量可以使用“register”声明且其地址从未被获取时,才会出现未定义行为。 - Jens Gustedt
@JensGustedt 值仍未确定,最好避免潜在的未定义行为。 - Alexey Frunze
在大多数体系结构中(没有陷阱表示),它只是一个未指定的值,这应该已经足够让每个人相信不要做这样的事情 :) - Jens Gustedt
@JensGustedt 我知道,有符号溢出也是一样的。 - Alexey Frunze

-1

C语言允许您访问地址空间中的任何内容,无论它是否被初始化。有时候这样做会导致程序崩溃或显示垃圾数据,有时候也可能会打印出一些有用的信息,但这都是未定义的行为。这是一个方便的技巧,但也是破坏程序的好方法,所以不要认为只要得到结果就意味着您的技巧有效。


4
当然,C语言并不会让你随意地在任何地方访问任何内容。 - Kerrek SB
1
你可以将任何数字的指针放入printf中,它会愉快地打印该内存位置的内容,就像它是一个本地变量一样。也许我应该加上“在您的地址空间内”的限定词,但是您肯定知道我的意思吧? - SilverbackNet
2
变量仍然有作用域,所以它不像许多其他语言那样,你曾经定义的所有内容都可以从任何其他地方读取。你仍然必须遵守规则。此外,指针语义有严格的规则,你很可能在谈论某种未定义的行为... - Kerrek SB
3
如果你在提问者的代码中写入return i;而不是return 0;,那么C语言将不允许你从任何地方访问到任何东西。这个问题似乎是关于如何让变量i处于作用域但未初始化,而不是关于使用指针漫游地址空间时C语言慷慨地为你提供自我脚射的机会。 - Steve Jessop
好吧,我只是想解释一下为什么他最终得到了结果,而他可能预期会出现编译器错误的情况。嗯。 - SilverbackNet
显示剩余2条评论

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