最近我碰到了C的Singleton设计模式的实现/实现 . 看起来像这样(我从现实生活中采用了它):
// a lot of methods are omitted here
class Singleton
{
public:
static Singleton* getInstance( );
~Singleton( );
private:
Singleton( );
static Singleton* instance;
};
从这个声明我可以推断出实例字段是在堆上启动的 . 这意味着存在内存分配 . 对我来说完全不清楚的是,什么时候内存将被解除分配?还是有漏洞和内存泄漏?好像在实施中存在问题 .
我的主要问题是,如何以正确的方式实施它?
18 回答
与上面链接的论文描述了双重检查锁定的缺点是编译器可以在调用对象的构造函数之前为对象分配内存并设置指向已分配内存的地址的指针 . 但是在c中很容易使用分配器手动分配内存,然后使用构造调用来初始化内存 . 使用这个appraoch,双重检查锁定工作正常 .
如何使用这样的新位置:
在2008年,我提供了Singleton设计模式的C 98实现,该模式是惰性评估,保证破坏,非技术上线程安全的:
Can any one provide me a sample of Singleton in c++?
这是Singleton设计模式的更新C 11实现,它是惰性评估,正确销毁和thread-safe .
请参阅此文章,了解何时使用单例:(不经常)
Singleton: How should it be used
请参阅这两篇关于初始化顺序以及如何应对的文章:
Static variables initialisation order
Finding C++ static initialization order problems
请参阅此文章描述生命周期:
What is the lifetime of a static variable in a C++ function?
请参阅本文,讨论对单身人士的一些线程影响:
Singleton instance declared as static variable of GetInstance method, is it thread-safe?
请参阅此文章,解释为什么双重检查锁定不适用于C:
What are all the common undefined behaviours that a C++ programmer should know about?
Dr Dobbs: C++ and The Perils of Double-Checked Locking: Part I
作为一个单身人士,你通常不希望它被破坏 .
当程序终止时,它将被拆除并解除分配,这是单例的正常,期望的行为 . 如果你想能够明确地清理它,那么向类中添加一个静态方法是非常容易的,它允许你将它恢复到干净状态,并在下次使用它时重新分配它,但这超出了范围 . “经典”单身人士 .
你可以避免内存分配 . 存在许多变体,在多线程环境的情况下都存在问题 .
我更喜欢这种实现(实际上,我没有正确地说我更喜欢,因为我尽可能地避免单身人士):
它没有动态内存分配 .
@Loki Astari's answer非常好 .
但是,有时会出现多个静态对象,您需要能够保证在使用单例的所有静态对象不再需要它之前不会销毁单例 .
在这种情况下,即使在程序结束时调用静态析构函数,
std::shared_ptr
也可用于为所有用户保持单例存活:另一个非分配替代方案:根据需要创建一个单例,例如类
C
:运用
这个和Cătălin的答案在当前的C中都不是自动线程安全的,而是在C 0x中 .
如果要在堆中分配对象,为什么不使用唯一指针 . 由于我们使用唯一指针,因此内存也将被释放 .
我想你应该写一个静态函数,其中你的静态对象被删除 . 当您要关闭应用程序时,应该调用此函数 . 这将确保您没有内存泄漏 .
接受的答案中的解决方案有一个明显的缺点 - 在控件离开
main()
函数后调用单例的析构函数 . 当在main
中分配一些依赖对象时,可能确实存在问题 .我试图在Qt应用程序中引入Singleton时遇到了这个问题 . 我决定,我所有的设置对话框都必须是Singletons,并采用上面的模式 . 不幸的是,Qt的主类
QApplication
在main
函数的堆栈上分配,Qt禁止在没有应用程序对象可用时创建/销毁对话框 .这就是为什么我更喜欢堆分配的单例 . 我为所有单例提供了一个明确的
init()
和term()
方法,并在main
中调用它们 . 因此,我可以完全控制单身人士创造/毁灭的顺序,而且我保证会创建单身人士,无论是否有人叫getInstance()
.这是一个简单的实现 .
每次创建后只创建一个对象并返回此对象引用 .
这里00915CB8是单例对象的内存位置,对于程序的持续时间是相同的,但每次运行程序时(通常!)都不同 .
注:这不是线程安全的 . 您必须确保线程安全 .
它确实可能是从堆中分配的,但如果没有源,就无法知道 .
典型的实现(取自我已经在emacs中的一些代码)将是:
......并且依赖于超出范围的程序事后清理干净 .
如果您在必须手动完成清理的平台上工作,我可能会添加一个手动清理例程 .
这样做的另一个问题是它不是线程安全的 . 在多线程环境中,两个线程可以在有机会分配新实例之前通过“if”(因此两者都可以) . 如果依靠程序终止进行清理,这仍然不是什么大不了的事 .
我没有在答案中找到CRTP实现,所以这里是:
要使用,只需从中继承您的类,例如:
class Test : public Singleton<Test>
这是关于对象生命周期管理 . 假设您的软件中不仅仅有单例 . 他们依靠Logger单身人士 . 在应用程序销毁期间,假设另一个单例对象使用Logger记录其销毁步骤 . 您必须保证最后清理Logger . 因此,请查看本文:http://www.cs.wustl.edu/~schmidt/PDF/ObjMan.pdf
有没人提到
std::call_once
和std::once_flag
?大多数其他方法 - 包括双重检查锁定 - 都被打破了 .单例模式实现中的一个主要问题是安全初始化 . 唯一安全的方法是使用同步障碍来保护初始化序列 . 但这些障碍本身需要安全地启动 .
std::once_flag
是保证安全初始化的机制 .例:
除了这里的其他讨论之外,值得注意的是,您可以拥有全局性,而不限制对一个实例的使用 . 例如,考虑引用计数的情况......
现在在函数内部(例如
main
)可以执行以下操作:refs不需要将指针存储回各自的
Store
,因为该信息是在编译时提供的 . 您也不必担心Store
的生命周期,因为编译器要求它是全局的 . 如果确实只有Store
的一个实例那么's no overhead in this approach; with more than one instance it'直到编译器才能聪明地生成代码 . 如果有必要,ItemRef
类甚至可以成为Store
的Store
(你可以有模板化的朋友!) .如果
Store
本身是一个模板化的类,那么事情变得更加混乱,但仍然可以使用此方法,可能通过实现具有以下签名的帮助程序类:用户现在可以为每个全局
Store
实例创建StoreWrapper
类型(和全局实例),并始终通过其包装器实例访问存储(因此忘记了使用Store
所需的模板参数的详细信息) .简单的单例类,这必须是您的头类文件
像这样访问您的单身人士: