单例模式的多种形态

应用实例:线程池,日志类,windows系统的任务管理器

一个单例模式应具备以下特征:

  1. 只能实例化同一个对象
  2. 可以全局访问
  3. 禁止拷贝
  4. 线程安全

针对第一条,可以将构造函数权限设为private,如果是public那么每次实例化调用构造函数,对象的内存地址都不同,也就是说只准调用一次构造函数。那问题来了,构造函数都private了,怎么实例化对象?显然要用某个public方法来调用,这又有问题:都没实例化对象,怎么调用public方法?所以这个public方法只能是静态的了。

针对第二条,全局性很容易想到静态函数,它是属于类的,而不是属于某个对象的。

第三条很容易,将拷贝构造函数和复制运算符声明为private即可。

综上,单例类的雏形应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton
{
public:
// 单例方法

private: //构造函数或析构函数为私有函数,所以该类是无法被继承的,
Singleton(){std::cout<<"单例构造"<<endl; }
~Singleton(){std::cout<<"单例析构"<<endl; }
Singleton(const Singleton &);
Singleton& operator=(const Singleton&);
static Singleton* m;
};

关键就是怎么实现public static方法。

测试1

首先想到这样的方法:

1
2
3
4
5
static Singleton instance()
{
Singleton s;
return s;
}

结果报错构造函数和析构函数是private,第一行就编译不过

测试2

测试下面这种方法

1
2
3
4
5
6
7
8
9
static Singleton* instance1()
{
Singleton *s = new Singleton();
return s;
}
Singleton* s1 = Singleton::instance1();
Singleton *s2 = Singleton::instance1();
delete s1;
delete s2;

结果发现有两个构造函数,而且无法析构。

有缺陷的懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Singleton
{
public:
static Singleton* LazyInstance()
{
if(!m)
m = new Singleton();
return m;
}
~Singleton(){std::cout<<"单例析构"<<endl; }

private:
Singleton(){std::cout<<"单例构造"<<endl; }
Singleton(const Singleton &)=delete;
Singleton& operator=(const Singleton&)=delete;

static Singleton* m;
};

测试:

1
2
3
4
5
6
7
// 在类外要初始化静态变量: SingleTon* SingleTon::instance = nullptr;
Singleton* s = Singleton::LazyInstance();
std::cout<<s<<endl;
Singleton* s2 = s;
std::cout<<s2<<endl;
delete s;
// delete s2; 不能这样,否则二次析构

s和s2的内存地址相同,说明是单例。但类中只负责new出对象,却没有负责delete对象,结果发现只调用一次构造函数,还需要手动delete s。可以用智能指针修正

如果有两个线程,假设pthread_1刚判断完 intance 为NULL 为真,准备创建实例的时候,切换到了pthread_2, 此时pthread_2也判断intance为NULL为真,创建了一个实例,再切回pthread_1的时候继续创建一个实例返回,那么此时就不再满足单例模式的要求

双检锁+智能指针的懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SingleTon
{
public:
typedef boost::unique_ptr<SingleTon> Ptr;
static Ptr getInstance()
{
if(instance==nullptr)
{
std::lock_guard<std::mutex> lk(m_mutex);
if(instance==nullptr)
instance = boost::shared_unique<SingleTon>(new SingleTon);
}
return instance;
}
~SingleTon(){ qDebug()<<"destruct"; }
private:
SingleTon(){ qDebug()<<"construct"; }
SingleTon(const SingleTon& s){}
SingleTon& operator=(const SingleTon& s){}

static std::mutex m_mutex;
static Ptr instance;
};

改用智能指针做静态类型,

1
2
3
4
5
6
// 在类外要初始化静态变量: SingleTon::Ptr SingleTon::instance = nullptr;
SingleTon::Ptr s = SingleTon::getInstance();
qDebug() << s.get();

SingleTon::Ptr s1 = SingleTon::getInstance();
qDebug() << s1.get();

结果二者地址相同,也运行了析构函数。 加了锁,使用互斥量来达到线程安全。这里使用了两个if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用getInstance的方法都加锁,锁的开销毕竟还是有点大的。

缺点:使用智能指针会要求用户也得使用智能指针;使用锁也有开销; 同时代码量也增多了;在某些平台,双检锁会失效

饿汉模式

优点:不需要加锁,执行效率高,线程安全的

缺点:初始化即实例化,浪费内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Singelton
{
private:
Singelton(){}
static Singelton *single;
public:
static Singelton *GetSingelton();
};
// 饿汉模式的关键:初始化即实例化
Singelton *Singelton::single = new Singelton;

Singelton *Singelton::GetSingelton()
{
// 不再需要进行实例化
//if(single == nullptr){
// single = new Singelton;
//}
return single;
}

Meyers模式

1
2
3
4
5
6
#define App SingleTon::MeyersInstance()
static SingleTon& MeyersInstance()
{
static SingleTon s;
return s;
}

这种方式所用到的特性是在C++11标准中的Magic Static特性。如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。

在 MeyersInstance() 函数内定义局部静态变量的好处是,构造函数只会在第一次调用MeyersInstance() 时被初始化, 保证了成员变量和 Singleton 本身的初始化顺序。
它还有一个潜在的安全措施, MeyersInstance() 返回的是对局部静态变量的引用, 如果返回的是指针, MeyersInstance() 的调用者很可能会误认为他要检查指针的有效性, 并负责销毁。

Qt中的全局指针

Qt里有一个全局指针qApp,在任意地方都能使用,看看是不是单例模式。

QApplication中:

1
#define qApp (static_cast<QApplication *>(QCoreApplication::instance()))

QCoreapplication中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//头文件中
#define qApp QCoreApplication::instance()

static QCoreApplication *instance() { return self; }

static QCoreApplication *self;

//源文件中
QCoreApplication *QCoreApplication::self = 0;
// 构造函数
QCoreApplication::QCoreApplication()
{
d_func()->q_ptr = this;
d_func()->init();
QCoreApplicationPrivate::eventDispatcher->startingUp();
}

// 就是 d_func()->init();
void QCoreApplicationPrivate::init()
{
......
Q_ASSERT_X(!QCoreApplication::self, "QCoreApplication", "there should be only one application object");
QCoreApplication::self = q;
......
}

从QCoreapplication来看,qApp是个宏,实际是函数QCoreApplication::instance(),QCoreApplication这个类十分关键,构造函数肯定不能是private。从这个self来看,特别像懒汉模式,self是在QCoreApplication构造函数里赋值,赋给它的q指针实际就是QCoreApplication的this指针。

但是在程序里使用qApp,你会发现其地址都一样,也就是同一个全局指针,这就在于Q_ASSERT_X这句限定了只能有一个QCoreApplication对象,再加上拷贝构造函数和赋值运算符都在QObject限定为private,因此qApp也是一种单例模式。所以我们可以说单例模式不一定限定构造和析构是private,这个使用了Qt特有的d指针和q指针,技巧性太高,还是用meyers模式吧

ROS中的单例模式

看ROS源码中的类TopicManager,它用到了单例模式,我模仿写了一个类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SingleTon;
typedef boost::shared_ptr<SingleTon> SingleTonPtr;

#define Ptr SingleTon::getInstance()

class SingleTon
{
public:
SingleTon() {}
static const SingleTonPtr& getInstance()
{
static SingleTonPtr f = boost::make_shared<SingleTon>();
return f;
}
void out() { cout<<" out put "<<endl; }
private:
int m_num;
};

结果发现构造函数只能是public,如果是private,就会报错,原因在make_shared中。这样一来就不能实现单例了,看来这种做法不可行。


参考:探究 C++ Singleton(单例模式)