C++ 单例模式的几种实现方式研究

单例模式

单例模式,可以说设计模式中最常应用的一种模式了,据说也是面试官最喜欢的题目。但是如果没有学过设计模式的人,可能不会想到要去应用单例模式,面对单例模式适用的情况,可能会优先考虑使用全局或者静态变量的方式,这样比较简单,也是没学过设计模式的人所能想到的最简单的方式了。

一般情况下,我们建立的一些类是属于工具性质的,基本不用存储太多的跟自身有关的数据,在这种情况下,每次都去new一个对象,即增加了开销,也使得代码更加臃肿。其实,我们只需要一个实例对象就可以。如果采用全局或者静态变量的方式,会影响封装性,难以保证别的代码不会对全局变量造成影响。

考虑到这些需要,我们将默认的构造函数声明为私有的,这样就不会被外部所new了,甚至可以将析构函数也声明为私有的,这样就只有自己能够删除自己了。在Java和C#这样纯的面向对象的语言中,单例模式非常好实现,直接就可以在静态区初始化instance,然后通过getInstance返回,这种就被称为饿汉式单例类。也有些写法是在getInstance中new instance然后返回,这种就被称为懒汉式单例类,但这涉及到第一次getInstance的一个判断问题。

下面的代码只是表示一下,跟具体哪种语言没有关系。

单线程中:

Singleton* getInstance()
{
  if (instance == NULL)
    instance = new Singleton();

  return instance;
}

这样就可以了,保证只取得了一个实例。但是在多线程的环境下却不行了,因为很可能两个线程同时运行到if (instance == NULL)这一句,导致可能会产生两个实例。于是就要在代码中加锁。

Singleton* getInstance()
{
  lock();
  if (instance == NULL)
  {
    instance = new Singleton();
  }
  unlock();

  return instance;
}

但这样写的话,会稍稍映像性能,因为每次判断是否为空都需要被锁定,如果有很多线程的话,就爱会造成大量线程的阻塞。于是大神们又想出了双重锁定。

Singleton* getInstance()
{
  if (instance == NULL)
  {
	lock();
  	if (instance == NULL)
  	{
    		instance = new Singleton();
  	}
  	unlock();
  }

  return instance;
}

这样只够极低的几率下,通过越过了if (instance == NULL)的线程才会有进入锁定临界区的可能性,这种几率还是比较低的,不会阻塞太多的线程,但为了防止一个线程进入临界区创建实例,另外的线程也进去临界区创建实例,又加上了一道防御if (instance == NULL),这样就确保不会重复创建了。

常用的场景

单例模式常常与工厂模式结合使用,因为工厂只需要创建产品实例就可以了,在多线程的环境下也不会造成任何的冲突,因此只需要一个工厂实例就可以了。

优点
1.减少了时间和空间的开销(new实例的开销)。

2.提高了封装性,使得外部不易改动实例。

缺点
1.懒汉式是以时间换空间的方式。

2.饿汉式是以空间换时间的方式。

C++实现代码

#ifndef _SINGLETON_H_
#define _SINGLETON_H_

class Singleton{
public:
static Singleton* getInstance();

private:
Singleton();
//把复制构造函数和=操作符也设为私有,防止被复制
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);

static Singleton* instance;
};

#endif

#include "Singleton.h"

Singleton::Singleton(){

}

Singleton::Singleton(const Singleton&){

}

Singleton& Singleton::operator=(const Singleton&){

}

//在此处初始化
Singleton* Singleton::instance = new Singleton();
Singleton* Singleton::getInstance(){
return instance;
}

#include "Singleton.h"
#include <stdio.h>

int main(){
Singleton* singleton1 = Singleton::getInstance();
Singleton* singleton2 = Singleton::getInstance();

if (singleton1 == singleton2)
fprintf(stderr,"singleton1 = singleton2\n");

return 0;
}

1 g++ -o client Singleton.cpp client.cpp

运行结果

下面给大家补充一下

单例模式有两种实现模式:

1)懒汉模式: 就是说当你第一次使用时才创建一个唯一的实例对象,从而实现延迟加载的效果。

2)饿汉模式: 就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。

所以,从实现手法上看, 懒汉模式是在第一次使用单例对象时才完成初始化工作。因为此时可能存在多线程竞态环境,如不加锁限制会导致重复构造或构造不完全问题。

饿汉模式则是利用外部变量,在进入程序入口函数之前就完成单例对象的初始化工作,此时是单线程所以不会存在多线程的竞态环境,故而无需加锁。

以下是典型的几种实现

一、 懒汉模式,标准的 ”双检锁“ + ”自动回收“ 实现

class Singleton

{
public:
  static Singleton* GetInstance()
  {
    if (m_pInstance == NULL )
    {
      Lock(); // 加锁
      if (m_pInstance == NULL )
      {
        m_pInstance = new Singleton ();
      }
      UnLock(); // 解锁
    }
    return m_pInstance;
  }

  // 实现一个内嵌垃圾回收类
  class CGarbo
  {
  public:
    ~CGarbo()
    {
      if(Singleton::m_pInstance)
        delete Singleton::m_pInstance;
    }
  };

  static CGarbo Garbo; // 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象

private:
  Singleton(){};
  Singleton(Singleton const&);
  Singleton& operator=(Singleton const&); 

  static Singleton* m_pInstance;
};

Singleton* Singleton::m_pInstance = NULL;
Singleton::CGarbo Garbo;

二、静态局部变量的懒汉模式 ,而不是new在堆上创建对象,避免自己回收资源。

这里仍然要注意的是局部变量初始化的线程安全性问题,在C++0X以后,要求编译器保证静态变量初始化的线程安全性,可以不加锁。但C++ 0X以前,仍需要加锁。

class Singleton
{
public:
  static Singleton* GetInstance()
  {
    Lock(); // not needed after C++0x
    static Singleton instance;
    UnLock(); // not needed after C++0x 

    return &instance;
  }

private:
  Singleton() {};
  Singleton(const Singleton &);
  Singleton & operator = (const Singleton &);
};

在懒汉模式里,如果大量并发线程获取单例对象,在进行频繁加锁解锁操作时,必然导致效率低下。

三、饿汉模式,基础版本

因为程序一开始就完成了单例对象的初始化,所以后续不再需要考虑多线程安全性问题,就可以避免懒汉模式里频繁加锁解锁带来的开销。

class Singleton
{
public:

  static Singleton* GetInstance()
  {
    return &m_instance;
  }

private:
  Singleton(){};
  Singleton(Singleton const&);
  Singleton& operator=(Singleton const&); 

  static Singleton m_instance;
};

Singleton Singleton::m_instance; // 在程序入口之前就完成单例对象的初始化

虽然这种实现在一定程度下能良好工作,但是在某些情况下会带来问题 --- 就是在C++中 ”非局部静态对象“ 的 ”初始化“ 顺序 的 ”不确定性“, 参见Effective c++ 条款47。

考虑: 如果有两个这样的单例类,将分别生成单例对象A, 单例对象B. 它们分别定义在不同的编译单元(cpp中), 而A的初始化依赖于B 【 即A的构造函数中要调用B::GetInstance() ,而此时B::m_instance 可能还未初始化,显然调用结果就是非法的 】, 所以说只有B在A之前完成初始化程序才能正确运行,而这种跨编译单元的初始化顺序编译器是无法保证的。

四、饿汉模式,增强版本(boost实现)

在前面的方案中:饿汉模式中,使用到了类静态成员变量,但是遇到了初始化顺序的问题; 懒汉模式中,使用到了静态局部变量,但是存在着线程安全等问题。

boost 的实现方式是:单例对象作为静态局部变量,然后增加一个辅助类,并声明一个该辅助类的类静态成员变量,在该辅助类的构造函数中,初始化单例对象。以下为代码

class Singleton
{
public:
  static Singleton* GetInstance()
  {
    static Singleton instance;
    return &instance;
  }

protected:
  // 辅助代理类
  struct Object_Creator
  {
    Object_Creator()
    {
      Singleton::GetInstance();
    }
  };
  static Object_Creator _object_creator;

  Singleton() {}
  ~Singleton() {}
};

Singleton::Object_Creator Singleton::_object_creator;

首先,代理类这个外部变量初始化时,在其构造函数内部调用 Singleton::GetInstance();从而间接完成单例对象的初始化,这就通过该代理类实现了饿汉模式的特性。

其次,仍然考虑第三种模式的缺陷。 当A的初始化依赖于B, 【 即A的构造函数中要调用B::GetInstance() ,而此时B::m_instance 可能还未初始化,显然调用结果就是非法的 】 现在就变为【在A的构造函数中要调用B::GetInstance() ,如果B尚未初始化,就会引发B的初始化】,所以在不同编译单元内全局变量的初始化顺序不定的问题就随之解决。

最后,关于使用懒汉还是饿汉模式,我的理解:

如果这个单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,也是一种资源浪费吧。 所以这种情况懒汉模式(延迟加载)更好。

如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。

(0)

相关推荐

  • C++构造函数和析构函数的使用与讲解

    构造函数(constructor) 1.构造函数是种特殊的类成员函数,遵循如下规则: a.函数名与类名必须相同. b.没有返回值 例如: class Obj { ... public: Obj() { ... } }; 2.构造函数可以带参数,也可以重载 class Obj { ... public: Obj() { ... } Obj(int x, int y) { ... } }; 3.构造函数和普通成员函数不一样,一般不显示调用.在创建一个对象时,构造函数自动调用(编译器来完成). 析构函

  • C++利用链表模板类实现简易队列

    本文实例为大家分享了C++利用链表模板类实现一个队列的具体代码,供大家参考,具体内容如下 设计思想:MyQueue.h中对模板类进行声明和实现.首先定义结点的结构体,包含数据和指针域两部分.队列类定义中声明和实现了元素入队,出队,打印队首元素和队列等方法. 注意: 1)模板类的声明和定义不能分开(即不能分别放在.h和.cpp文件里). 2)声明新节点时,如果声明的节点是辅助操作的,可以不用new关键字,例如在析构函数中,直接用:Node<T>* temp:定义即可.如果声明一个新节点加入队列,

  • C++11并发编程关于原子操作atomic的代码示例

    一:概述 项目中经常用遇到多线程操作共享数据问题,常用的处理方式是对共享数据进行加锁,如果多线程操作共享变量也同样采用这种方式. 为什么要对共享变量加锁或使用原子操作?如两个线程操作同一变量过程中,一个线程执行过程中可能被内核临时挂起,这就是线程切换,当内核再次切换到该线程时,之前的数据可能已被修改,不能保证原子操作. C++11提供了个原子的类和方法atomic,保证了多线程对变量原子性操作,相比加锁机制mutex.lock(),mutex.unlock(),性能有几倍的提升. 所需头文件<a

  • C++小知识:不要节约代码行数

    本篇案例来自 KDE4 源码. 错误代码: void LDAPProtocol::del( const KUrl &_url, bool ) { .... if ( (id = mOp.del( usrc.dn() ) == -1) ) { LDAPErr(); return; } ret = mOp.waitForResult( id, -1 ); .... } 说明: 一些程序员竭尽全力想把更多的代码压缩到一行.他们尤其热衷于"if"条件语句,把赋值和比较一次性完成. 一个

  • C/C++语言printf命令使用方法

    在C语言以及其它语言中我们经常看到printf命令,那么怎么使用该命令输出想要的结果呢?下面就为大家演示. 1.printf()函数是一个格式化输出函数, 用户可以通过该命令向标准输出设备输出指定的格式信息.使用的方法是: printf("格式化字符串", 变量).在使用前需要注意的是该命令被定义在stdio.h头文件中(C语言),所以需要在程序中加入#include<stdio.h>.printf()函数是一个格式化输出函数, 用户可以通过该命令向标准输出设备输出指定的格

  • C++实现连连看游戏核心代码

    这两天研究了一下连连看游戏的源代码,感觉它挺简单的,主要就是判断选中的两张图片能否消去.我参考了网上的源代码(抱歉的是,不记得当时下载的网址了,在此对原作者表示深深的歉意!),然后自己把核心代码整理如下,与大家共享.需要说明的是,这只是核心算法的代码,界面设计和操作的代码均已略去. #include <stdlib.h> #include <time.h> //图片类 class picture { public: int type;//图片的编号,共有n种,从0到n-1 bool

  • C++ 编写DLL文件给易语言调用方法

    摸索了两天了,终于解决了所有问题,在此跟大家分享. 需要三个文件,dll_demo.h.dll_demo.cpp.dll_dome.def 直接上代码: 头文件如下: #ifndef _DLL_DEMO_H_ #define _DLL_DEMO_H_ #ifdef DLLDEMO_EXPORTS #define DLL_DEMO extern "C" __declspec(dllexport) #else #define DLL_DEMO extern "C" __

  • C++实现连连看消除算法

    C++连连看消除代码的实现,过程比较复杂. #include<iostream> #include<vector> using namespace std; bool CheckRemove(int x1,int y1,int x2,int y2,int rows,int cols,int ***pArray); void Swap(int &a,int &b); typedef struct { int x; int y; }point; int main() {

  • C++人工模拟栈实现方法

    在C++中,栈作为一种无可替代的重要数据结构,对于程序员来说异常重要.那么,我们该如何用代码人工模拟栈呢? 首先,设置栈的最大深度(因为这里的栈是由数组实现的,在函数引用传递的过程中必须设置大小). 第二步,初始化栈,就是只要把栈顶指针设置为-1(为什么设置为-1在注意事项里说). 第三步,是判断栈满和判断栈空.这很简单,只要对应判断栈顶指针的状态即可. 第四步,压栈.压栈的函数也不难,就是把栈顶指针上移,然后在新的空间里存放数据. 第五步,弹栈与读栈.弹栈与读栈的唯一区别就是弹栈弹出栈顶元素后

  • C++实现递归函数的方法

    递归函数通俗来讲就是自己调用自己本身.这样有很大的好处,代码很方便简洁,把复杂的有规律的运算交给计算机去做. 1.首先定义问题.递归函数(recursion)需要设置一个函数,然后再可以循环往复的执行下去. 2.把问题换成公式. 如把阶乘之和定义为f(n)=n*f(n-1).也就是说n*f(n-1)=n*(n-1)*f(n-2)=...=n*(n-1)*(n-2)*...*1 3.用C++公式编写程序 4.再把递归函数累加 5.完整公式如下 6.输入输出检查之后完全正确 总结:以上就是关于C++

随机推荐