3

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

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

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

Segmentation fault (core dumped)

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

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

如果你删除你的对象,它会尝试删除自己,这将导致它尝试删除自己,这将导致它删除自己,这将...

delete当我简单地使用运算符/函数来释放LeakySingleton静态类成员变量指向的堆对象时,为什么它会尝试删除自己LeakySingleton::instance?堆分配的资源由指向对象的LeakySingleton::instance指针变量指向LeakySingleton。那么为什么显式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 $@
4

4 回答 4

2

你有一个讨厌的循环,LeakySingleton::create_instance因为你有:

instance = new LeakySingleton{t_x};

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

delete instance;

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

instance = nullptr;

所以你有无限递归导致你的stackoverflow。

于 2019-11-10T23:56:12.097 回答
2

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

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

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

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;
  }
}
于 2019-11-11T00:03:03.283 回答
0

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

于 2019-11-10T23:43:57.400 回答
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: 您可能会对这篇文章感兴趣。它还警告不要使用公共析构函数,因为这可能会导致悬空指针。

于 2019-11-11T00:04:19.887 回答