在另一个静态对象的析构函数内构造的静态对象的析构函数

8

我在以下代码中遇到了析构函数的问题:

#include <stdlib.h>
#include <cstdio>

class Foo2
{
    public:
        Foo2() { printf("foo2 const\n"); }

        ~Foo2()
        {
            printf("foo2 dest\n"); //  <--- wasn't called for bionic libc
        }
};

static Foo2& GetFoo2()
{
    static Foo2 foo2;
    printf ("return foo2\n");
    return foo2;
}

class Foo1
{
    public:
        Foo1() { printf("foo1 const\n"); }

        ~Foo1()
        {
            printf("foo1 dest\n");
            GetFoo2();
        }
};

int main( int argc, const char* argv[] )
{
        printf("main 1 \n");
        static Foo1 anotherFoo;
        printf("main 2 \n");
}

为什么bionic的foo2析构函数没有被调用,而glibc的被调用了呢? 编辑
bionic的输出结果:
main 1  
foo1 const  
main 2  
foo1 dest  
foo2 const  
return foo2  

调试信息:

(gdb) break 22
Breakpoint 1 at 0x8048858: file test.C, line 22.
(gdb) info breakpoints
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x08048858 in Foo2::~Foo2() at test.C:22
(gdb) cont
[    exited with code 0]

什么是Bionic和GLIBC? - Luchian Grigore
3
两个流行的标准C和C++库实现:glibc是GNU的,Bionic适用于Android。 - user529758
https://dev59.com/GnVC5IYBdhLWcg3wlyMo - bobah
1
我认为这是Bionic中的一个错误。 - Andy Prowl
2
似乎是bionic中的一个bug... - Kerrek SB
显示剩余2条评论
4个回答

6
我认为你的代码存在未定义行为,尽管标准并没有明确说明(或者我无法在标准中找到)。你的代码在静态对象的析构函数中构建了一个新的静态对象。标准没有涉及这种情况,但是:
1.它确实说必须按照构造的相反顺序调用析构函数。在你的情况下,这意味着必须在构建之前销毁GetFoo2中的静态对象,这是自相矛盾的。
2. §3.6/3中的文本描述了析构函数和使用atexit注册的函数的顺序。要求是必须为每个函数使用相同的注册机制。在调用了exit(或从主函数返回)后调用atexit是未定义的行为。
3.还有§3.6/2,其中说“如果一个函数包含已被销毁的具有静态或线程存储期的块作用域对象,并且在具有静态或线程存储期的对象的销毁过程中调用该函数,则如果控制流经过先前销毁的块作用域对象的定义,则程序产生未定义行为。”此句话谈论的是已经销毁的对象,但很容易想象缺乏“尚未构建”的对象只是一种疏忽。
最终,我认为我的第一个观点是有决定性意义的。在§ 1.3.24中,有一个注意事项(非规范的,但表明意图)“当国际标准省略任何行为的明确定义或程序使用错误的构造或错误的数据时,可能会预期未定义的行为。”在这种情况下,所需行为的唯一说明是不可能的(因为不能在构建之前销毁对象),而标准没有说明如何解决这个问题。

我不太确定。(1) 我认为意图是好像所有静态变量都在一个堆栈上,这对于这段代码不会造成问题。(2) 我不认为静态析构函数使用 atexit。我认为这是明确定义的行为。 - Mooing Duck
我可能会同意基于§1.3.24的未定义情况,但仅限于此。在n3337中,我没有看到§3.6 [终止]中涉及特定情况的任何内容。逐字逐句地阐述,只是指如果一个对象在另一个对象之后构造,则应首先调用其析构函数,然而我们经常在析构函数执行期间构造对象... - Matthieu M.
我没有看到任何未定义的行为或矛盾。Foo1的生命周期已经结束(因为其析构函数调用已经开始),所以Foo2的析构函数应该运行,并且应该在Foo1的析构函数之后运行。 - Ville Voutilainen

5

我看到这段代码中的所有实例都是静态的。

因此,它们的析构函数会在可执行文件的末尾,在 main 函数结束后被调用。

如果析构函数没有被调用,那么就是一个 bug。


3
在我看来,foo2似乎直到程序结束时,也就是anotherFoo被销毁的时候才会被构造。 - JasonD
我看到的所有实例都不应该在 main() 之前构造。它们都是局部静态变量,应该在第一次进入作用域时构造。 - James Kanze
你让我有些怀疑了,我会检查一下静态实例的构造方式……让我们确认一下。 - Stephane Rolland
@JasonD 和 JamesKanze 你们是对的。请参考 https://dev59.com/O07Sa4cB1Zd3GeqP4IAp 。我会相应地编辑我的答案。感谢你们指出这一点。 - Stephane Rolland

4

程序结束时,静态对象会被销毁。在~Foo2()处设置断点,您将看到它或者将日志写入文件应该有助于诊断。如果确实没有被调用,则可能是编译器错误。

enter image description here

上传图片回答问题也很有趣。


编程很有趣,但当编译器产生分歧时,答案应该参考更高的权威来支持它们的主张:标准。 - Matthieu M.

4
C++11 3.6.3/1规定:具有静态存储期的已初始化对象的析构函数是由从main返回导致调用的。
当程序从main返回时,anotherFoo已经初始化;但是foo2没有被初始化,因为它直到在销毁anotherFoo期间的第一次调用GetFoo2时才被初始化。因此,根据严格解释规则,其析构函数不应该被调用。

或者说在构造函数之前应该调用其析构函数。或者别的什么问题。我认为这是未定义行为的情况。 - James Kanze
@JamesKanze:这当然不意味着析构函数应该在构造函数之前被调用:“对于已初始化的对象,析构函数[...]会被调用”。 - Mike Seymour
对象在离开程序之前被初始化。(但是我同意标准并没有明确规定所需的行为。这就是为什么我会认为这是未定义的行为。) - James Kanze
1
@JamesKanze:“由于从main返回”而被调用,(或调用std::exit,但这在这里不相关),而不是由于离开程序。正如我所说,当程序从“main”返回时,它未被初始化。 - Mike Seymour

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