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

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

本文关键字:堆栈 栈溢出 无休止 析构函数 调用 自己 为什么      更新时间:2023-10-16

当我尝试通过静态函数调用堆构造一个对象(例如LeakySingleton)时,我对为什么析构函数无限次调用自己感到困惑create_instance()然后尝试通过delete操作显式删除它。

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

在有和没有 valgrind 的情况下调用它会导致分段错误(由于堆栈溢出而导致的内存访问无效):

Segmentation fault (core dumped)

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

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

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

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

(我目前的 dtor 和 ctor 理解:new函数/运算符为堆上的对象分配内存并调用构造函数,delete函数调用析构函数,在我的例子中还调用内部的delete运算符/函数。

源:

主.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 $@

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

main函数中的delete语句是调用LeakySingleton::~LeakySingleton,而 反过来又会尝试删除静态实例指针,然后再次调用析构函数。您的代码从未有机会将静态指针设置为 null。那里有一个无限循环。

附言恕我直言,在非静态方法中修改静态成员通常是一种不好的做法。我相信您可以将静态清理逻辑放在另一种静态方法中。

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::create_instance你有:

instance = new LeakySingleton{t_x};

那么在LeakySingleton的Destuctor中,你有:

delete instance;

这将在您将任何内容设置为 null 之前调用LeakySingleton的 destuctor :

instance = nullptr;

所以你有无限递归导致你的堆栈溢出。

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

首先,由于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:您可能会对本文感兴趣。它还警告不要使用公共析构函数,因为这可能会导致指针悬空。