为什么析构函数会无休止地调用自己(导致堆栈溢出)?

Why does the destructor call itself endlessly (causing a stack overflow)?

我很困惑为什么析构函数会无限次地调用自己,当我尝试通过静态函数调用 create_instance() 在堆上构造一个对象时说 LeakySingleton 然后尝试之后通过 delete 操作明确删除它。

据我了解,考虑到下面的源列表,main() 中的变量 leaky_singleton 指向由 create_instance() 返回的堆分配资源。这样,我们就通过create_instance函数在堆上间接分配了一个对象LeakySingleton。 现在,如果我在 leaky_singleton 上显式调用 delete 运算符或 delete 函数,那么它首先调用析构函数并检查它是否满足 instance != nullptr 条件,然后删除 [=22] 的对象=] 指向的内容应删除。 如果这个对象 LeakySingleton::instance 被删除,那么 dtor 就没有理由再次调用自己,或者我在这里遗漏了什么?

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

Segmentation fault (core dumped)

单步执行调试器,导致无休止的析构函数调用(堆栈溢出的罪魁祸首)。

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

If you delete your object, it attempts to delete itself, which will cause it to attempt to delete itself, which will cause it to delete itself, which will...

当我简单地使用 delete operator/function 来释放静态 class 成员变量指向的堆对象 LeakySingleton 时,为什么它会尝试删除自身LeakySingleton::instance? 堆分配的资源由指向 LeakySingleton 对象的 LeakySingleton::instance 指针变量指向。那么,为什么显式 delete 函数调用不会删除或释放已分配的堆对象,而是无休止地递归?我在这里错过了什么?

(我目前对dtor和ctor的理解:new function/operator为堆上的对象分配内存并调用构造函数,delete函数调用析构函数并在我的案例也在里面调用了一个delete operator/function。)

来源:

main.cpp:

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;
}

生成文件:

CC = g++
CFLAGS = -g -Wall -Wextra -pedantic -std=c++11
SRC = main.cpp
TARGET = app
RM = rm -rf

.PHONY: all clean

all: $(TARGET)

clean:
    $(RM) $(TARGET)

$(TARGET): $(SRC)
    $(CC) $(CFLAGS) $^ -o $@

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

你有一个讨厌的周期,在 LeakySingleton::create_instance 你有:

instance = new LeakySingleton{t_x};

然后在 LeakySingleton 的析构函数中你有:

delete instance;

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

instance = nullptr;

所以你有无限递归导致你的计算器溢出。

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

main 函数中的 delete 语句正在调用 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;
  }
}

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

  • 因此它的析构函数应该是私有的,就像它的构造函数一样。
  • 如果可以删除单例:应使用删除实例的 public 函数 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 您可能会感兴趣。它还针对 public 析构函数发出警告,因为这可能导致悬空指针。