在C++中,不使用实例/引用计数器的单例对象是否应该被视为内存泄漏?
如果没有一个调用显式删除单例实例的计数器,当计数为零时,对象如何被删除?当应用程序终止时,操作系统会清理吗?如果该单例在堆上分配了内存怎么办?
简而言之,我是否必须调用单例的析构函数,还是可以依赖它在应用程序终止时被清理?
在C++中,不使用实例/引用计数器的单例对象是否应该被视为内存泄漏?
如果没有一个调用显式删除单例实例的计数器,当计数为零时,对象如何被删除?当应用程序终止时,操作系统会清理吗?如果该单例在堆上分配了内存怎么办?
简而言之,我是否必须调用单例的析构函数,还是可以依赖它在应用程序终止时被清理?
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;
}
如果您有不同的情况,那么就需要注意了。您的临时文件使用的内存将被回收,但是文件不会被删除,因为析构函数不会被调用。
你可以信任操作系统清理它。
话虽如此,如果你使用的是有终结器而非析构函数的垃圾收集语言,那么你可能需要一个优雅的关闭过程,可以直接干净地关闭你的单例,以便释放任何关键资源,以防止使用系统资源仅通过结束应用程序无法正确清理。这是因为在大多数语言中,终结器都是在“尽力而为”的基础上运行的。另一方面,只有极少数资源需要这种可靠性。文件句柄、内存等所有资源都会干净地返回到操作系统。
如果你在像C++这样具有真正析构函数而非终结器的语言中使用懒惰分配(即三重检查锁定惯用法)的单例,则不能指望其析构函数在程序关闭时被调用。如果你使用的是单个静态实例,则析构函数将在程序主函数完成后的某个时刻运行。
无论如何,当进程结束时,所有内存都会返回到操作系统。
您应该明确清理所有对象。不要依赖操作系统来为您清理。
我通常使用单例来封装对文件、硬件资源等的控制。如果我没有正确清理该连接,我很容易泄漏系统资源。下次应用程序运行时,如果资源仍由前一操作锁定,则可能会失败。另一个问题可能是任何最终化-例如将缓冲区写入磁盘-如果它仍存在于单例实例拥有的缓冲区中,则可能不会发生。
这不是内存泄漏问题-问题更多地是您可能会泄漏除内存以外的其他资源,这些资源可能不太容易恢复。
每种语言和环境都会有所不同,但我同意@Aaron Fisher的观点,单例往往存在于整个进程的生命周期中。
以C++为例,使用典型的单例惯用法:
Singleton &get_singleton()
{
static Singleton singleton;
return singleton;
}
第一次调用该函数时,Singleton实例将被构造,同一实例将在程序关闭时的全局静态析构阶段调用其析构函数。
get_singleton()
会发生什么? - Bensignal(SIGTERM,exit);
你是如何创建对象的?
如果你使用了全局变量或静态变量,假设程序正常退出,析构函数将会被调用。
比如下面这个程序:
#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
在应用程序终止之前明确释放全局内存分配是一种传统习惯。我认为大多数人都出于习惯和因为觉得“忘记”一个结构有点不好。在C世界中,任何分配必须有一个相应的释放是对称法则。如果C++程序员知道并实践RAII,他们会有不同的看法。
在AmigaOS等早期系统的好日子里,存在真正的内存泄漏问题。当您忘记释放内存时,它将永远无法再次访问,直到系统被重置。
在现在这个时代,我不知道有哪个自重的桌面操作系统允许内存泄漏从应用程序的虚拟地址空间中泄露出来。在嵌入式设备上,如果没有广泛的内存记录,您的情况可能会有所不同。
这取决于您对泄漏的定义。在我看来,未绑定内存增加是一种泄漏,但单例模式不会未绑定。如果您不提供引用计数,则会有意地保持该实例处于活动状态。这不是偶然事件,也不是泄漏。
您的单例包装器析构函数应删除实例,它不是自动执行的。如果它只分配内存而没有操作系统资源,那么就没有意义。
class Singleton{
...
friend class Singleton_Cleanup;
};
class Singleton_Cleanup{
public:
~Singleton_Cleanup(){
delete Singleton::ptr;
}
};
Singleton::ptr
设置为null值是一个好主意。 - Joshua Breeden