【问题标题】:Why does the destructor call itself endlessly (causing a stack overflow)?为什么析构函数无休止地调用自己(导致堆栈溢出)?
【发布时间】:2020-03-06 16:49:55
【问题描述】:

当我尝试通过静态函数调用create_instance() 在堆上构造一个对象说LeakySingleton,然后尝试通过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里面的操作符/函数。)

来源:

ma​​in.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 $@

【问题讨论】:

  • delete 调用 dtor,它调用 delete,它 clls dtor,... 在 delete 返回之前,您不会将 instance 设置为 nullptr(它永远不会)。
  • @stark,感谢您的输入,但我仍然对为什么会这样感到困惑。当 dtor 被调用时,delete 发出到目前为止这么好,但是为什么 delete 会导致另一个 dtor 调用呢?
  • 当你调用new时,它会分配内存,然后调用ctor。当你调用 delete 时,它​​首先调用 dtor,然后释放内存。
  • 是的,到目前为止我可以关注你,但是为什么它会被多次调用?如果您考虑这个简单的源代码列表,那么 dtor 只会被调用一次:privatebin.net/…
  • 实例 == 这里。您首先调用 delete (调用析构函数),然后使指针无效 - 所以在每次调用时,指针都不为 null [还]。另外,您是否有理由不使用带有函数级静态变量的典型单例实现?

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


【解决方案1】:

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

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

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

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

【讨论】:

  • 感谢您的输入@xiaofeng.li,但我仍然不确定为什么它在“尝试”删除静态实例指针后再次调用 dtor。这里 dtor 被调用一次:privatebin.net/…
  • @user12197465 -- 您的示例不是deleting 类型为A 的对象。因此,它与您看到的问题不相似甚至不接近。
  • @PaulMcKenzie 啊,真的。所以它与它的自引用行为有关。该评论显然已被删除,但如果我没记错的话,评论的人暗示我这是由于 'this == LeakySingelton::instance'。所以,我正在解构“这个”——对象本身?澄清一下:我在静态功能块中调用“new”,new 为“LeakySingleton”分配了内存,到目前为止一切都很好。然后,我打电话给 dtor,dtor 在 'LeakySingleton::instance' 上调用 delete,由于某种原因,它被多次调用。这是我不完全理解的:多次调用。
  • *Sopel 的评论还在。它没有像我错误地假设的那样被删除。
  • 我想我终于明白了,因为我在 dtor 中执行“删除 this”,而“this”是对象的 ptr,delete 再次调用对象的 dtor,因此它永远不会结束,因为“删除对象”构造总是会调用它的 dtor。谢谢你们! :)
【解决方案2】:

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

instance = new LeakySingleton{t_x};

那么在LeakySingleton的析构函数中你有:

delete instance;

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

instance = nullptr;

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

【讨论】:

    【解决方案3】:

    Delete instace in destructor 启动一个析构函数调用。

    【讨论】:

      【解决方案4】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-09-11
        • 2014-12-20
        • 2019-10-05
        • 2020-09-11
        • 2011-12-21
        • 2018-06-19
        • 2011-02-26
        相关资源
        最近更新 更多