可以在不调用析构函数的情况下跳过goto跳转函数吗?

99

是不是真的goto可以跳过代码的一部分而不调用析构函数和其他东西?

例如:

void f() {
   int x = 0;
   goto lol;
}

int main() {
   f();
lol:
   return 0;
}

不会泄漏x吗?

相关链接:https://dev59.com/FHM_5IYBdhLWcg3wt1rD(但我想要从头开始,干净利落地完成!) - Lightness Races in Orbit
16
“Won't x be leaked” 是什么意思?这里的 x 是一种内置的数据类型。为什么不选择一个更好的例子呢? - Nawaz
2
@Nawaz:这个例子已经很完美了。几乎每次我和别人谈到“goto”时,他们都认为即使是自动存储期变量也会被“泄漏”。你和我知道的情况完全无关紧要。 - Lightness Races in Orbit
1
@David:我同意,当变量具有非平凡析构函数时,这个问题就更有意义了……我查看了Tomalak的答案,并找到了这样一个例子。此外,虽然int不能泄漏,但它可以被泄漏。例如:void f(void) { new int(5); }会泄漏一个int - Ben Voigt
为什么不将问题改为类似于“在给定的示例中,代码执行路径是否会从f()转移到main()而不清除堆栈和其他返回函数功能?如果需要调用析构函数,这会有影响吗?在C语言中是否相同?”这样既能保持问题的意图,又能避免可能的误解呢? - Jack V.
显示剩余2条评论
1个回答

216

警告:本答案仅适用于C++;在C中规则完全不同。


x 不会泄漏吗?

不会,绝对不会。

goto 是一种低级构造的神话,它让您可以覆盖C++的内置作用域机制。(如果有什么问题,那可能是 longjmp 的问题。)

请考虑以下机制,以防止您在标签上做出“坏事”(包括case标签)。


1. 标签作用域

您不能跨函数进行跳转:

void f() {
   int x = 0;
   goto lol;
}

int main() {
   f();
lol:
   return 0;
}

// error: label 'lol' used but not defined

[n3290: 6.1/1]: [..] 标签的作用域是它所在的函数。[..]


2. 对象初始化

你不能跨越对象初始化进行跳转:

int main() {
   goto lol;
   int x = 0;
lol:
   return 0;
}

// error: jump to label ‘lol’
// error:   from here
// error:   crosses initialization of ‘int x’

如果你跳过对象初始化而返回,则会销毁对象的先前“实例”:链接

struct T {
   T() { cout << "*T"; }
  ~T() { cout << "~T"; }
};

int main() {
   int x = 0;

  lol:
   T t;
   if (x++ < 5)
     goto lol;
}

// Output: *T~T*T~T*T~T*T~T*T~T*T~T

[n3290: 6.6/2]: [..] 跳出循环、块或回到一个已初始化自动存储期变量之前的位置,将导致具有自动存储期并处于跳转点作用域内但在跳转目标点不在作用域内的对象被销毁。[..]

即使未显式初始化,也不能跳入对象的作用域:

int main() {
   goto lol;
   {
      std::string x;
lol:
      x = "";
   }
}

// error: jump to label ‘lol’
// error:   from here
// error:   crosses initialization of ‘std::string x’

除了某些特定类型的对象,这些对象不需要“复杂”的构造,因此语言可以处理它们:

int main() {
   goto lol;
   {
      int x;
lol:
      x = 0;
   }
}

// OK

[n3290: 6.7/3]: 可以将控制流转移到块内,但不能绕过带初始化的声明。一个程序从一个自动存储期变量不在其作用域的点跳到其作用域所在的点是不合法的,除非该变量具有标量类型、具有平凡的默认构造函数和平凡的析构函数的类类型、这些类型中的一个的const或volatile限定版本或一个不带初始化符号的前述类型的数组。[..]


3. 跳转遵守其他对象的作用域

同样地,当你使用 goto 跳出它们的作用域时,具有自动存储期的对象 不会被“泄漏” :

struct T {
   T() { cout << "*T"; }
  ~T() { cout << "~T"; }
};

int main() {
   {
      T t;
      goto lol;
   }

lol:
   return 0;
}

// *T~T

[n3290: 6.6/2]: 无论是通过何种方式退出作用域,该作用域中构造的具有自动存储期限(3.7.3)的对象将按照其构造顺序的相反顺序进行销毁。[..]


结论

上述机制确保了 goto 不会让你破坏语言。

当然,这并不意味着你在解决任何问题时都应该使用 goto,但这意味着它并没有常见神话所认为的那么 "邪恶"。


8
请注意,C语言并不能防止所有这些危险事件的发生。 - Daniel
14
@Daniel:这个问题和答案非常具体地涉及到C ++,但你说得没错。也许我们可以再做一个FAQ来澄清C语言和C++是不同的,而不是相同的谬论。 - Lightness Races in Orbit
3
@Tomalak:我认为我们在这里并没有意见分歧。许多在SO上得到的答案都是明确记录在某处的。我只是想指出,对于C程序员来说,看到这个答案可能会很诱人,并假设如果在C++中起作用,那么在C中也应该类似地起作用。 - Daniel
3
你可能也想补充说明,所有跳过初始化的事情对于 case 标签也是一样的。 - PlasmaHH
16
哇,我之前一直认为C++的goto语义是有问题的,但它们竟然非常合理!很棒的答案。 - Joseph Garvin
显示剩余9条评论

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