27

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

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

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

4

12 回答 12

20

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

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

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

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

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

于 2008-11-08T10:50:20.660 回答
14

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

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

如果您在像 c++ 这样的语言中使用延迟分配的单例(即使用三重检查锁习语),使用真正的析构函数而不是终结器,那么您不能依赖于在程序关闭期间调用它的析构函数。如果您使用的是单个静态实例,则析构函数将在 main 完成后运行。

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

于 2008-11-07T21:47:00.163 回答
11

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

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

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

于 2008-11-07T22:07:32.087 回答
9

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

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

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

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

于 2008-11-07T22:21:40.903 回答
3

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

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

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

于 2008-11-07T21:48:10.053 回答
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
于 2008-11-08T14:47:54.953 回答
2

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

在 AmigaOS 等过去的好日子里,确实存在内存泄漏。当您忘记释放内存时,它将永远无法再次访问,直到系统重置。

这些天我不知道有任何自尊的桌面操作系统会允许内存泄漏从应用程序的虚拟地址空间中蔓延出来。当没有大量内存簿记时,您的里程可能会因嵌入式设备而异。

于 2008-11-08T15:06:29.910 回答
1

单例将是您的对象的一个​​实例。这就是它不需要计数器的原因。如果它会在您的应用程序的长度内存在,那么默认的析构函数就可以了。在任何情况下,内存都将在进程结束时由操作系统回收。

于 2008-11-07T21:45:55.173 回答
1

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

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

于 2008-11-07T21:49:00.597 回答
1

在像 C++ 这样没有垃圾收集的语言中,最好的做法是在终止之前进行清理。您可以使用析构函数朋友类来做到这一点。

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

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

于 2016-12-21T04:00:54.293 回答
0

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

*这并不意味着您应该绕过新指针并且永远不要清理它们。

于 2008-11-07T21:44:06.237 回答
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存活。

于 2018-09-21T16:31:40.817 回答