单例模式析构函数

27

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

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

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


操作系统会在进程退出时清理应用程序使用的任何内存,但它不会运行任何对象析构函数。因此,在退出时不必担心内存泄漏 - 操作系统会很好地处理这个问题(就像关闭任何打开的文件等一样),泄漏只在程序仍在运行时才是真正的问题。 - Jesper Juhl
12个回答

20
常见的情况是“这要看情况”。在任何值得称道的操作系统中,当您的进程退出时,进程内部使用的所有内存和其他资源都将被释放。您根本不需要担心这个问题。
但是,如果您的单例对象正在分配与其自身进程的生命周期外相同的资源(例如文件、命名的互斥体或类似物)则需要考虑适当的清理。
在这里RAII可以帮助您处理这种情况:
```c++ class Singleton { public: Singleton() { // Acquire the resource here. }
~Singleton() { // Release the resource here. } }; ```
这样,您就可以确保在单例对象生命周期结束时释放资源。
class Tempfile
{
Tempfile() {}; // creates a temporary file 
virtual ~Tempfile(); // close AND DELETE the temporary file 
};

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

如果你的临时文件在应用程序退出时会被关闭和删除,那么你可以放心。但是这种方法不是线程安全的,对象删除的顺序可能不符合您的预期或要求。然而,如果您的单例实现像这样

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

如果您有不同的情况,那么就需要注意了。您的临时文件使用的内存将被回收,但是文件不会被删除,因为析构函数不会被调用。


14

你可以信任操作系统清理它。

话虽如此,如果你使用的是有终结器而非析构函数的垃圾收集语言,那么你可能需要一个优雅的关闭过程,可以直接干净地关闭你的单例,以便释放任何关键资源,以防止使用系统资源仅通过结束应用程序无法正确清理。这是因为在大多数语言中,终结器都是在“尽力而为”的基础上运行的。另一方面,只有极少数资源需要这种可靠性。文件句柄、内存等所有资源都会干净地返回到操作系统。

如果你在像C++这样具有真正析构函数而非终结器的语言中使用懒惰分配(即三重检查锁定惯用法)的单例,则不能指望其析构函数在程序关闭时被调用。如果你使用的是单个静态实例,则析构函数将在程序主函数完成后的某个时刻运行。

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


11

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

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

这不是内存泄漏问题-问题更多地是您可能会泄漏除内存以外的其他资源,这些资源可能不太容易恢复。


+1 的确没错,但有些单例确保在应用程序退出时调用析构函数。 - Nicola Bonelli
1
如果你依赖于运行时库在 main 函数返回后销毁静态对象,并希望能够在(Windows)DLL 中使用代码,那么你就是在 DllMain 函数期间运行代码,而你想要做的大多数事情都是不安全的。 - Integer Poet

9

每种语言和环境都会有所不同,但我同意@Aaron Fisher的观点,单例往往存在于整个进程的生命周期中。

以C++为例,使用典型的单例惯用法:

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

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


但是如果工作线程在主线程拆除单例之后调用get_singleton()会发生什么? - Ben

3
除了在共享内存中的分配,其他任何类型的分配都会在进程终止时由操作系统自动清除。因此,您不必显式调用单例析构函数。换句话说,没有泄漏...
此外,像 Meyers 单例这样的典型实现不仅在线程安全的情况下在第一次调用期间初始化,而且在应用程序退出时也保证优雅地终止(析构函数被调用)。
但是,如果应用程序收到 Unix 信号(例如:SIGTERM 或 SIGHUP),默认行为是终止进程而不调用静态分配对象(单例)的析构函数。为了解决这些信号的问题,可以使用处理程序调用 exit 并将 exit 作为此处理程序处理 -- signal(SIGTERM,exit);

如果你依赖于运行时库在main函数返回后销毁静态对象,并且希望能够在(Windows) DLL中使用该代码,那么你正在DllMain期间运行代码,并且你可能想做的大多数事情都是不安全的。 - Integer Poet

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

2

在应用程序终止之前明确释放全局内存分配是一种传统习惯。我认为大多数人都出于习惯和因为觉得“忘记”一个结构有点不好。在C世界中,任何分配必须有一个相应的释放是对称法则。如果C++程序员知道并实践RAII,他们会有不同的看法。

在AmigaOS等早期系统的好日子里,存在真正的内存泄漏问题。当您忘记释放内存时,它将永远无法再次访问,直到系统被重置。

在现在这个时代,我不知道有哪个自重的桌面操作系统允许内存泄漏从应用程序的虚拟地址空间中泄露出来。在嵌入式设备上,如果没有广泛的内存记录,您的情况可能会有所不同。


2
如果你在应用程序开发过程中编写可重用的代码,那么这并不是神话。如果你不知道未来所有可能使用可重用代码的上下文,那么最好保持谨慎。我的项目通常都是希望重复使用的代码为主,再加上一层薄薄的应用逻辑。 - Integer Poet
此外,即使是熟悉RAII的C++程序员,在单例模式的上下文中也不能轻易放过。如果你依赖于运行时库在main函数返回后销毁静态对象,并希望能够在(Windows)DLL中使用代码,那么你正在DllMain期间运行代码,而你想做的大多数事情都是不安全的。 - Integer Poet
你对库中可重复使用的代码是正确的。但对于应用程序来说则不然。在任何现代操作系统中,全局代码(即库)之外没有内存泄漏。这种情况根本不存在。当应用程序终止时,整个内存空间都被清除。无论是调用 new() 还是 malloc(),都会变为尘土,无论是否调用 free() 或 delete()。 - Thorsten79

1
一个singleton将成为您的对象的一个实例。这就是为什么它不需要计数器的原因。如果它将在应用程序的整个生命周期内存在,那么默认析构函数就可以了。无论如何,当进程结束时,操作系统都会回收内存。

1

这取决于您对泄漏的定义。在我看来,未绑定内存增加是一种泄漏,但单例模式不会未绑定。如果您不提供引用计数,则会有意地保持该实例处于活动状态。这不是偶然事件,也不是泄漏。

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


1
在像C++这样没有垃圾回收机制的语言中,最佳实践是在终止前清理。您可以使用析构函数友元类来实现此操作。
class Singleton{
...
   friend class Singleton_Cleanup;
};
class Singleton_Cleanup{
public:
    ~Singleton_Cleanup(){
         delete Singleton::ptr;
     }
};

程序启动时创建清理类,然后在退出时调用析构函数清理单例。这种做法可能比让操作系统处理更冗长,但它遵循 RAII 原则,根据单例对象分配的资源情况,这可能是必要的。

有趣的回答。将Singleton::ptr设置为null值是一个好主意。 - Joshua Breeden

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接