你如何实现单例设计模式?

894
最近我遇到了一个用于C++的Singleton设计模式的实现。它看起来像这样(我从现实生活中的一个例子中采用了它):
// a lot of methods are omitted here
class Singleton
{
   public:
       static Singleton* getInstance( );
       ~Singleton( );
   private:
       Singleton( );
       static Singleton* instance;
};

从这个声明中,我可以推断出实例字段是在堆上初始化的。这意味着有一个内存分配。对我来说完全不清楚的是,内存何时会被释放?还是说有一个错误和内存泄漏?看起来实现上存在问题。
我主要的问题是,我应该如何正确地实现它?

19
请参考以下链接:https://dev59.com/JHVC5IYBdhLWcg3wsTZi#211307,https://dev59.com/XXVC5IYBdhLWcg3whBaj#271104,https://dev59.com/GnVC5IYBdhLWcg3wlyMo和https://dev59.com/AXRB5IYBdhLWcg3w-8A9#449823以及https://dev59.com/anRC5IYBdhLWcg3wUvUS#335746。 - Martin York
13
你可以在这篇文章中找到有关如何在C++中实现单例并确保线程安全的讨论。 http://www.aristeia.com/Papers/DDJ%5FJul%5FAug%5F2004%5Frevised.pdf - Matthieu N.
116
@sbi - 只有黑暗面的绝地武士才会以绝对的方式思考问题。大多数问题可以在没有单例的情况下解决吗?绝对可以。单例模式会带来自己的问题吗?是的。然而,我不能诚实地说它们是“坏”的,因为设计建立在考虑权衡和理解方法的细微差别之上。 - derekerdmann
13
@derekerdmann说:我没有说你永远不需要全局变量(当你需要时,单例有时更好)。我所说的是应该尽可能少地使用它们。把单例视为有价值的设计模式会给人留下使用它很好的印象,而不是它是一种“hack”,使代码难以理解、难以维护和难以测试。这就是为什么我发表了我的评论。到目前为止,你所说的都没有与此相矛盾。 - sbi
18
@sbi 说的是“不要使用它们”,而不是后来你改变的更加合理的“应该尽量少用它们” - 你一定看得出区别。 - jwd
显示剩余9条评论
24个回答

1388

2008年,我提供了一个C++98实现的单例设计模式,它是惰性求值、保证销毁,但不具备技术上的线程安全:
Can any one provide me a sample of Singleton in c++?

这里是一个更新的C++11实现的单例设计模式,它是惰性求值、正确销毁,并且具有线程安全性

class S
{
    public:
        static S& getInstance()
        {
            static S    instance; // Guaranteed to be destroyed.
                                  // Instantiated on first use.
            return instance;
        }
    private:
        S() {}                    // Constructor? (the {} brackets) are needed here.

        // C++ 03
        // ========
        // Don't forget to declare these two. You want to make sure they
        // are inaccessible(especially from outside), otherwise, you may accidentally get copies of
        // your singleton appearing.
        S(S const&);              // Don't Implement
        void operator=(S const&); // Don't implement

        // C++ 11
        // =======
        // We can use the better technique of deleting the methods
        // we don't want.
    public:
        S(S const&)               = delete;
        void operator=(S const&)  = delete;

        // Note: Scott Meyers mentions in his Effective Modern
        //       C++ book, that deleted functions should generally
        //       be public as it results in better error messages
        //       due to the compilers behavior to check accessibility
        //       before deleted status
};

请查看关于何时使用单例模式的文章:(不常用) 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

32
好的回答。但应注意这不是线程安全的。 https://dev59.com/QnI-5IYBdhLWcg3w0MDx - Varuna
7
很多人没有意识到你刚才所做的事情 :) - Johann Gerell
6
当一个变量被销毁的时间是非常明确的(没有歧义)。参考链接:https://dev59.com/GnVC5IYBdhLWcg3wlyMo - Martin York
7
我最不满意的是getInstance()中对隐藏布尔值进行的运行时检查,这是一种实现技术的假设。没有必要假设它是活动的。请参见https://dev59.com/anRC5IYBdhLWcg3wUvUS#335746。您可以强制使其始终处于活动状态(比“Schwarz计数器”开销小)。全局变量在初始化顺序方面存在更多问题(跨编译单元),因为您无法强制执行顺序。这种模型的优点是1)延迟初始化。2)能够强制执行顺序(Schwarz有所帮助但更加丑陋)。是的,`get_instance()`要丑陋得多。 - Martin York
3
@kol:不,这不是通常的情况。只是因为初学者复制和粘贴代码而没有思考,并不意味着这是通常的情况。您应该始终查看用例,并确保赋值运算符执行所期望的操作。复制和粘贴代码会导致错误。 - Martin York
显示剩余75条评论

65

您可以避免内存分配。有许多变体,但在多线程环境下都存在问题。

我更喜欢这种实现方式(实际上,正确地说,我尽可能避免使用单例):

class Singleton
{
private:
   Singleton();

public:
   static Singleton& instance()
   {
      static Singleton INSTANCE;
      return INSTANCE;
   }
};

它没有动态内存分配。


4
在某些情况下,这种延迟初始化不是理想的模式。一个例子是当单例的构造函数从堆中分配内存,并且您希望该分配是可预测的,例如在嵌入式系统或其他严格控制的环境中。我更喜欢,在单例模式是最适合使用的模式时,将实例创建为类的静态成员。 - dma
3
对于许多较大的程序,特别是那些包含动态库的程序。任何非基本类型的全局或静态对象都可能导致程序退出时在许多平台上发生段错误/崩溃,这是由于库卸载时销毁顺序的问题所致。这是许多编码约定(包括Google的约定)禁止使用非平凡的静态和全局对象的原因之一。 - obecalp
似乎这种实现中的静态实例具有内部链接,并且在不同的翻译单元中具有唯一和独立的副本,这将导致混淆和错误行为。但是我看到很多这样的实现,我错过了什么吗? - FaceBro
什么阻止用户将此分配给多个对象,而编译器在幕后使用自己的复制构造函数? - Tony
@Tony,没有任何阻止复制的方法,你是对的。应该删除复制构造函数。 - ebrahim
2
@FaceBro 这里有两个关键字 static 的实例,但从链接的角度来看,它们都没有问题。第一个 static 出现在静态成员函数上,与链接无关。第二个 static 是关于 INSTANCE 存储期的。该对象将在程序的整个生命周期中存在于内存中,虽然你不能在 TU 之外通过名称访问它,但你可以通过具有外部链接的成员函数 instance 访问它,这并不重要。 - ebrahim

55

作为一个 Singleton,通常你不希望它被销毁。

当程序终止时,它会被关闭和释放,这是单例的正常期望行为。如果你想要显式地清除它,很容易在类中添加一个静态方法,使你能够将其恢复到干净的状态,并在下次使用时重新分配内存,但这超出了“经典”单例的范围。


9
这不是内存泄漏,就像简单声明一个全局变量一样。 - ilya n.
18
为了澄清一件事情...关于单例模式的"内存泄漏"问题完全没有意义。如果你有状态资源,其中析构顺序很重要,那么单例可能是危险的;但是在程序终止时,操作系统会完全回收所有内存...在99.9%的场景中,这个完全学术性的观点被否定了。如果你想争论什么是"内存泄漏"的语法,那没问题,但请注意这会分散实际设计决策的注意力。 - jkerian
12
在C++语境中,内存泄漏和销毁并不仅仅是关于内存泄漏的问题。实际上,它更多地涉及到资源控制。如果你泄露了内存,析构函数就不会被调用,因此与对象相关联的任何资源都不会被正确释放。内存只是我们在教授编程时使用的简单示例,但实际上还有更复杂的其他资源存在。 - Martin York
9
@Martin,我完全同意你的观点。即使唯一的资源是内存,如果你必须浏览泄漏列表并过滤掉“无关紧要”的泄漏,试图找到程序中的真正泄漏也会陷入麻烦。最好将这些全部清理干净,以便任何报告泄漏的工具只报告确实存在问题的情况。 - Dolphin
8
值得考虑的是,存在一些 C++ 实现(甚至可能是托管实现),在这些实现中,“操作系统”并不在程序退出时恢复所有资源,但它们确实有一些“再次运行程序”的概念,这会给您一个全新的全局和静态局部变量集合。在这样的系统上,未释放的单例满足任何明智定义下的真正泄漏:如果您的程序运行足够多次,它将使系统崩溃。是否关心到这种系统的可移植性是另一回事 - 只要您不编写库,几乎肯定不需要考虑它。 - Steve Jessop
显示剩余10条评论

43

Loki Astari的回答非常好。

然而,当存在多个静态对象且需要确保单例模式在所有使用它的静态对象都不再需要它之前不会被销毁时,需要采取一些措施。

在这种情况下,可以使用std::shared_ptr来保持单例模式在程序结束时调用静态析构函数时仍对所有用户有效。

class Singleton
{
public:
    Singleton(Singleton const&) = delete;
    Singleton& operator=(Singleton const&) = delete;

    static std::shared_ptr<Singleton> instance()
    {
        static std::shared_ptr<Singleton> s{new Singleton};
        return s;
    }

private:
    Singleton() {}
};

你能解释一下带有= delete的这两行代码吗?作为一个C#程序员,这个语法看起来有点奇怪。或者你能提供一个链接让我了解这个确切的语法吗? - Mohammed Noureldin
2
默认情况下,C++ 会自动生成复制对象的函数。如果您想防止对象被复制,可以“删除”这些函数。因此,= delete 告诉编译器不要生成它们。 - Galik
这是否实现了未完成的 faq https://isocpp.org/wiki/faq/ctors#nifty-counter-idiom 中提到的 Nifty Counter 模式? - RexYuan
@RexYuan 是的,我相信是这样的。它将确保您的单例对象在最后一个需要它的组件被销毁之后才被销毁。但是,您需要确保单例本身在销毁期间不需要任何全局静态对象,并且只要您没有像在std::shared_ptr之外保留原始指针或原始引用一样做任何愚蠢的事情,就可以了。 - Galik
2
我已经开始在维护一些过于复杂(单例重度)的遗留软件中使用这种方法。当我调查我们在终止期间遇到的神秘崩溃时,我发现有几个单例持有指向其他单例的指针,并在销毁期间调用它们的方法,导致整个静态去初始化顺序混乱。采用这种方法消除了单例被运行时以错误的顺序销毁时发生的使用后释放错误。它真的值得更多的推荐,谢谢! - Joseph Riesen
已经涵盖了保持单例的存活时间:https://dev59.com/anRC5IYBdhLWcg3wUvUS#335746 - Martin York

16

还有一种不需要分配内存的替代方法:按需创建一个单例,比如说一个类C

singleton<C>()

使用

template <class X>
X& singleton()
{
    static X x;
    return x;
}

目前的C++中,无论是这个答案还是Cătălin的答案都没有自动实现线程安全,但在C++0x中将会实现。


目前在gcc下,它是线程安全的(而且已经有一段时间了)。 - Martin York
16
这个设计的问题在于,如果在多个库中使用它,每个库都有自己单独的单例副本。因此,它不再是一个单例。 - Martin York

13

我在答案中没有找到CRTP实现,所以在这里提供一个:

template<typename HeirT>
class Singleton
{
public:
    Singleton() = delete;

    Singleton(const Singleton &) = delete;

    Singleton &operator=(const Singleton &) = delete;

    static HeirT &instance()
    {
        static HeirT instance;
        return instance;
    }
};

要使用这个,请将您的类继承自此类,如:class Test : public Singleton<Test>


4
除非我将默认构造函数设为受保护的并使用“= default”进行定义,否则我无法让它在C++17中起作用。 - wfranczyk

10

我们最近在我的EECS课堂上讨论了这个主题。如果你想详细查看讲义,请访问http://umich.edu/~eecs381/lecture/IdiomsDesPattsCreational.pdf。这些笔记(以及我在这个回答中提供的引用)是由我的教授David Kieras创建的。

我知道两种正确创建Singleton类的方法。

第一种方法:

将其实现方式与您示例中的方式相似。至于销毁,“单例通常持续整个程序运行时间;大多数操作系统将在程序终止时恢复内存和其他大部分资源,因此有一个不用担心这个问题的方法。”

然而,在程序终止时清理是一个好的习惯。因此,您可以使用一个辅助的静态SingletonDestructor类,并在您的Singleton中声明它为友元。

class Singleton {
public:
  static Singleton* get_instance();
  
  // disable copy/move -- this is a Singleton
  Singleton(const Singleton&) = delete;
  Singleton(Singleton&&) = delete;
  Singleton& operator=(const Singleton&) = delete;
  Singleton& operator=(Singleton&&) = delete;

  friend class Singleton_destroyer;

private:
  Singleton();  // no one else can create one
  ~Singleton(); // prevent accidental deletion

  static Singleton* ptr;
};

// auxiliary static object for destroying the memory of Singleton
class Singleton_destroyer {
public:
  ~Singleton_destroyer { delete Singleton::ptr; }
};

// somewhere in code (Singleton.cpp is probably the best place) 
// create a global static Singleton_destroyer object
Singleton_destoyer the_destroyer;

Singleton_destroyer将在程序启动时创建,当程序终止时,所有全局/静态对象都会被运行时库关闭代码(由链接器插入)销毁,因此destroyer将被销毁;它的析构函数将删除Singleton并运行其析构函数。

第二种方法

这被称为Meyers Singleton,是由C++巨匠Scott Meyers创建的。只需以不同的方式定义get_instance()即可。现在您还可以摆脱指针成员变量。

// public member function
static Singleton& Singleton::get_instance()
{
  static Singleton s;
  return s;
}

这很方便,因为返回的值是引用,您可以使用 . 语法而不是 -> 来访问成员变量。

"编译器会自动构建代码,首次声明时创建's',之后不再创建,并在程序终止时删除静态对象。"

请注意,对于Meyers单例模式,如果对象在终止时相互依赖,则可能会遇到非常困难的情况-相对于其他对象,Singleton何时消失?但对于简单的应用程序,这很有效。


相对简单(且直观)的解决静态初始化/销毁顺序问题的方法。https://stackoverflow.com/a/335746/14065 - undefined

8
这是一个简单的实现方法。
#include <Windows.h>
#include <iostream>

using namespace std;


class SingletonClass {

public:
    static SingletonClass* getInstance() {

    return (!m_instanceSingleton) ?
        m_instanceSingleton = new SingletonClass : 
        m_instanceSingleton;
    }

private:
    // private constructor and destructor
    SingletonClass() { cout << "SingletonClass instance created!\n"; }
    ~SingletonClass() {}

    // private copy constructor and assignment operator
    SingletonClass(const SingletonClass&);
    SingletonClass& operator=(const SingletonClass&);

    static SingletonClass *m_instanceSingleton;
};

SingletonClass* SingletonClass::m_instanceSingleton = nullptr;



int main(int argc, const char * argv[]) {

    SingletonClass *singleton;
    singleton = singleton->getInstance();
    cout << singleton << endl;

    // Another object gets the reference of the first object!
    SingletonClass *anotherSingleton;
    anotherSingleton = anotherSingleton->getInstance();
    cout << anotherSingleton << endl;

    Sleep(5000);

    return 0;
}

每次调用后都只创建一个对象并返回该对象的引用。

SingletonClass instance created!
00915CB8
00915CB8

这里的00915CB8是单例对象的内存位置,在程序运行期间保持不变,但每次运行程序时通常会不同。

注意:这不是线程安全的。您需要确保线程安全。


8

有人提到过std::call_oncestd::once_flag吗?大多数其他方法(包括双重检查锁定)都是有问题的。

单例模式实现中的一个主要问题是安全初始化。唯一安全的方式是使用同步屏障来保护初始化顺序。但这些屏障本身也需要被安全地初始化。std::once_flag就是确保安全初始化的机制。


7
被接受答案中的解决方案有一个明显的缺点 - 单例的析构函数在控制离开main()函数后才被调用。当在main内部分配了某些依赖对象时,可能会出现问题。
我在尝试在Qt应用程序中引入Singleton时遇到了这个问题。我决定将所有设置对话框设置为单例,并采用上述模式。不幸的是,Qt的主类QApplication在main函数中以堆栈方式分配了,而且Qt禁止在没有可用应用程序对象的情况下创建/销毁对话框。
这就是为什么我更喜欢堆分配单例。我为所有单例提供了显式的init()和term()方法,并在main内部调用它们。因此,我完全可以控制单例的创建/销毁顺序,并且我可以保证单例将被创建,无论是否有人调用getInstance()。

4
如果你指的是目前被接受的答案,那么你的第一个陈述是错误的。析构函数只有在所有静态存储期对象被销毁之后才会被调用。 - Martin York

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