单身破坏者

时间:2008-11-07 21:37:42

标签: c++ memory-management singleton

不使用实例/引用计数器的Singleton对象是否应被视为C ++中的内存泄漏?

如果没有计数器在计数为零时要求显式删除单例实例,该对象如何被删除?当应用程序终止时,操作系统是否清理它?如果Singleton在堆上分配了内存怎么办?

简而言之,我是否必须调用Singelton的析构函数,还是可以依赖它在应用程序终止时进行清理?

12 个答案:

答案 0 :(得分:20)

经常,“它取决于”。在任何名副其实的操作系统中,当进程退出时,将释放进程内本地使用的所有内存和其他资源。你根本不需要担心。

但是,如果您的单例分配资源的生命周期超出了它自己的进程(可能是文件,命名的互斥体或类似的东西),那么您需要考虑适当的清理。

RAII将在这里为您提供帮助。如果你有这样的场景:

class Tempfile
{
Tempfile() {}; // creates a temporary file 
virtual ~Tempfile(); // close AND DELETE the temporary file 
};

Tempfile &singleton()
{
  static Tempfile t;
  return t;
}

...然后您可以放心,您的临时文件将被关闭并删除,但您的应用程序将退出。 However, this is NOT thread-safe, and the order of object deletion may not be what you expect or require.

但是,如果你的单身人像这样实施

Tempfile &singleton()
{
  static Tempfile *t = NULL;
  if (t == NULL)
    t = new Tempfile(); 
  return *t;
}

...那么你有不同的情况。 tempfile使用的内存将被回收,但不会删除该文件,因为不会调用析构函数。

答案 1 :(得分:14)

您可以依赖操作系统清理它。

也就是说,如果您使用的是垃圾收集语言而不是析构函数,那么您可能希望拥有一个优雅的关闭程序,可以直接干净地关闭您的单例,以便在使用系统资源时可以释放任何关键资源仅仅结束应用程序将无法正确清理。 这是因为终结器在大多数语言中都以“尽力而为”的方式运行。另一方面,很少有资源需要这种可靠性。无论如何,文件句柄,内存等都会干净利落地返回操作系统。

如果你使用一个懒惰分配的单例(即使用三重检查锁定成语),使用真正的析构函数而不是终结函数的c ++语言,那么你不能依赖于在程序关闭期间调用它的析构函数。如果您使用的是单个静态实例,则析构函数将在main完成某个时刻后运行。

无论如何,当进程结束时,所有内存都返回操作系统。

答案 2 :(得分:11)

您应该明确清理所有对象。永远不要依赖操作系统为您清理。

我通常使用单例来封装对文件,硬件资源等的控制。如果我没有正确清理该连接 - 我很容易泄漏系统资源。下次运行应用程序时,如果资源仍然被上一个操作锁定,则可能会失败。另一个问题可能是任何终结 - 例如将缓冲区写入磁盘 - 如果它仍然存在于单例实例拥有的缓冲区中,则可能不会发生。

这不是内存泄漏问题 - 问题更多的是你可能正在泄漏资源而不是内存,这可能不容易恢复。

答案 3 :(得分:9)

每种语言和环境都会有所不同,尽管我同意@Aaron Fisher的观点,即在整个过程中单身人士往往会存在。

在C ++的例子中,使用典型的单例习语:

Singleton &get_singleton()
{
   static Singleton singleton;
   return singleton;
}

Singleton实例将在第一次调用函数时构造,同一个实例将在程序关闭时在全局静态析构函数阶段调用析构函数。

答案 4 :(得分:3)

当进程终止时,操作系统会自动清除除共享内存中的分配之外的任何类型的分配。因此,您不必显式调用singleton析构函数。换句话说,没有泄漏 ......

此外,像Meyers的Singleton这样的典型单例实现不仅在第一次调用初始化期间是线程安全的,而且在应用程序退出时(调用析构函数)也保证正常终止。

无论如何,如果应用程序发送了一个unix信号(即: SIGTERM SIGHUP ),默认行为是终止进程而不调用静态分配对象的析构函数(单身)。为了克服这些信号的问题,可以配置一个调用exit的处理程序,或者将exit作为这样的处理程序 - signal(SIGTERM,exit);

答案 5 :(得分:3)

你是如何创建对象的?

如果您正在使用全局变量或静态变量,则会调用析构函数,假设程序正常退出。

例如,程序

#include <iostream>

class Test
{
    const char *msg;

public:

    Test(const char *msg)
    : msg(msg)
    {}

    ~Test()
    {
        std::cout << "In destructor: " << msg << std::endl;
    }
};

Test globalTest("GlobalTest");

int main(int, char *argv[])
{
    static Test staticTest("StaticTest");

    return 0;
}

打印

In destructor: StaticTest 
In destructor: GlobalTest

答案 6 :(得分:2)

在应用程序终止之前显式释放全局内存分配是民间传说。我想我们大多数人都是出于习惯而做的,因为我们觉得“忘记”一个结构是不好的。在C世界中,它是一种对称定律,任何分配都必须在某处释放。如果他们知道并实践RAII,C ++程序员会有不同的想法。

在过去的美好时光AmigaOS存在真正的内存泄漏。当您忘记释放内存时,在系统重置之前,它将永远不会再次访问。

我现在不知道任何自尊的桌面操作系统会导致内存泄漏蔓延到应用程序的虚拟地址空间。如果没有大量的记忆簿记,嵌入式设备的里程可能会有所不同。

答案 7 :(得分:1)

singleton将是您对象的一个​​实例。这就是为什么它不需要计数器。如果它将在你的应用程序的长度存在,那么默认的析构函数将没有问题。在任何情况下,当进程结束时,操作系统将回收存储器。

答案 8 :(得分:1)

取决于您对泄漏的定义。未绑定的内存增加是我书中的漏洞,单例未绑定。如果您不提供引用计数,则有意保持实例处于活动状态。不是意外,不是泄漏。

你的singleton包装器的析构函数应该删除实例,它不是自动的。如果只是分配内存而没有操作系统资源,那就没有意义了。

答案 9 :(得分:1)

在像C ++这样没有垃圾收集的语言中,最好在终止之前进行清理。您可以使用析构函数朋友类来完成此操作。

class Singleton{
...
   friend class Singleton_Cleanup;
};
class Singleton_Cleanup{
public:
    ~Singleton_Cleanup(){
         delete Singleton::ptr;
     }
};

在启动程序时创建清理类,然后在退出析构函数时将调用清理单例。这可能比让它进入操作系统更加冗长,但它遵循RAII原则,并且取决于您的单例对象中分配的资源,它可能是必要的。

答案 10 :(得分:0)

操作系统将回收您的进程分配但未释放(已删除)的任何堆内存。如果您使用的是使用静态变量的单例的最常见实现,那么在应用程序终止时也会清除它。

*这并不意味着你应该绕过新的指针而不要清理它们。

答案 11 :(得分:0)

我遇到了这样的问题,即使主线程先退出并带有静态对象,我也认为这应该起作用。代替这个:

Singleton &get_singleton() {
   static Singleton singleton;
   return singleton;
}

我在想

Singleton &get_singleton() {
   static std::shared_ptr<Singleton> singleton = std::make_shared<Singleton>();
   static thread_local std::shared_ptr<Singleton> local = singleton;
   return *local;
}

因此,当主线程退出并带走singleton时,每个线程仍然有自己的local shared_ptr,可以使一个Singleton保持生命。