为什么析构函数会不断地自我调用(导致堆栈溢出)?

时间:2019-11-10 23:34:40

标签: c++ design-patterns singleton destructor self-destruction

当我尝试通过静态函数调用LeakySingleton在堆上构造一个对象create_instance()并尝试将其删除时,我对为什么析构函数调用自己无休止的次数感到困惑然后通过delete操作明确显示。

据我了解,考虑下面的源清单,leaky_singleton中的变量main()指向create_instance()返回的堆分配资源。因此,我们通过LeakySingleton函数间接地在堆上分配了对象create_instance。 现在,如果我显式调用leaky_singleton上的delete运算符或delete函数,则它将首先调用析构函数,并检查其是否满足instance != nullptr条件,然后删除{{1 }}点应删除。 如果此对象instance被删除,则dtor没有理由再次调用自身,或者我在这里丢失了什么东西吗?

使用和不使用valgrind调用它会导致分段错误(由于堆栈溢出而导致无效的内存访问):

LeakySingleton::instance

单步调试器会导致无休止的析构函数调用(堆栈溢出的元凶)。

从cplusplus.com(http://www.cplusplus.com/forum/general/40044/):

  

如果删除对象,它将尝试删除自身,这将删除   使它尝试删除自身,这将导致它删除   本身,这将会...

当我仅使用Segmentation fault (core dumped)运算符/函数来释放静态类成员变量delete所指向的堆对象LeakySingleton时,为什么它试图删除自身? 堆分配的资源由指向LeakySingleton::instance对象的LeakySingleton::instance指针变量指向。那么,为什么显式的LeakySingleton函数调用不会删除或取消分配已分配的堆对象,而是无限递归?我在这里想念什么?

(我目前对dtor和ctor的理解:delete函数/操作符为堆上的对象分配内存并调用构造函数,而new函数调用析构函数,在我的情况下还调用内部的delete运算符/函数。)

来源:

main.cpp

delete

制作文件

class Singleton final
{
    public:
        static Singleton & create_instance(int);
        ~Singleton() = default;
    private:
        int x;
        Singleton(int);

        Singleton(Singleton &) = delete;
        Singleton(Singleton &&) = delete;
        Singleton & operator=(Singleton &) = delete;
        Singleton & operator=(Singleton &&) = delete;
};

Singleton::Singleton(int t_x) : x{t_x}
{}

Singleton & Singleton::create_instance(int t_x)
{
    static Singleton instance{t_x};
    return instance;
}

// Potential endless dtor calls inside:
class LeakySingleton final
{
    public:
        static LeakySingleton * create_instance(int);
        ~LeakySingleton();
    private:
        int x;
        static LeakySingleton * instance;
        LeakySingleton(int);

        LeakySingleton(LeakySingleton &) = delete;
        LeakySingleton(LeakySingleton &&) = delete;
        LeakySingleton & operator=(LeakySingleton &) = delete;
        LeakySingleton & operator=(LeakySingleton &&) = delete;
};

LeakySingleton * LeakySingleton::instance = nullptr;

LeakySingleton::LeakySingleton(int t_x) : x{t_x}
{}

LeakySingleton::~LeakySingleton()
{
    if (instance != nullptr)
    {
        delete instance;
        instance = nullptr;
    }
}

LeakySingleton * LeakySingleton::create_instance(int t_x)
{
    if (instance == nullptr)
    {
        instance = new LeakySingleton{t_x};
    }
    return instance;
}

int main()
{ 
    // The correct implementation with no issues:
    {
        Singleton & singleton = Singleton::create_instance(42);
    }

    // The faulty implementation causing the dtor to recurse endlessly and resulting in a segfault:
    {
        LeakySingleton * leaky_singleton = LeakySingleton::create_instance(42);
        delete leaky_singleton;
    }

    return 0;
}

4 个答案:

答案 0 :(得分:2)

您的周期令人讨厌,在LeakySingleton::create_instance中,您有:

instance = new LeakySingleton{t_x};

然后在LeakySingleton的销毁器中拥有:

delete instance;

在将任何内容设置为null之前,它将调用LeakySingleton的析构函数:

instance = nullptr;

因此您的无限递归会导致堆栈溢出。

答案 1 :(得分:2)

在C ++中,delete将调用类析构函数。

您的delete函数中的main语句正在调用LeakySingleton::~LeakySingleton,这反过来又试图删除静态实例指针,然后再次调用析构函数。您的代码永远没有机会将静态指针设置为null。那里有无限循环。

P.S。恕我直言,在非静态方法中修改静态成员通常是一个坏习惯。我相信您可以将静态清除逻辑放在另一个静态方法中。

class LeakySingleton final {
public:
  static LeakySingleton& create_instance(int);
  static void destroy_instance();
  ~LeakySinglton() = default;
private:
  static LeakySingleton *instance;
  ...
};

void LeakySingleton::destroy_instance() {
  if (instance != nullptr) {
    delete instance;
    instance = nullptr;
  }
}

答案 2 :(得分:0)

在析构函数中删除实例,启动一个析构函数调用。

答案 3 :(得分:0)

首先,由于LeakySingleton不能直接创建,因此也不能直接销毁它:

  • 因此,它的析构函数应该与它的构造函数完全一样是私有的。
  • 如果单例可以删除:应使用删除实例的公共功能delete_instance()来删除单例
  • 析构函数不得删除自身(无休止的递归)
  • 此构造应避免这种无休止的析构函数递归

如果您希望实例指针泄漏并允许销毁它,则您不应执行两次(一次在析构函数之外,一次在析构函数内部),而应只在析构函数之外进行一次。由于只有一个实例,所以外面的析构函数意味着不需要在里面进行删除调用:

LeakySingleton::~LeakySingleton()
{
    if (instance != nullptr)
    {
         instance = nullptr;  // since there's only one, it's the instance and
    }                         // the instance pointer shall be reset
    // and do what's needed to clean the object
}   

注意:此实现不是线程安全的。

注意2:this article可能会让您感兴趣。它还警告不要破坏公共空间,因为这可能导致指针悬空。