应用实例:线程池,日志类,windows系统的任务管理器
一个单例模式应具备以下特征:
- 只能实例化同一个对象
- 可以全局访问
- 禁止拷贝
- 线程安全
针对第一条,可以将构造函数权限设为private,如果是public那么每次实例化调用构造函数,对象的内存地址都不同,也就是说只准调用一次构造函数。那问题来了,构造函数都private了,怎么实例化对象?显然要用某个public方法来调用,这又有问题:都没实例化对象,怎么调用public方法?所以这个public方法只能是静态的了。
针对第二条,全局性很容易想到静态函数,它是属于类的,而不是属于某个对象的。
第三条很容易,将拷贝构造函数和复制运算符声明为private即可。
综上,单例类的雏形应该是这样的:1
2
3
4
5
6
7
8
9
10
11
12class 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
5static Singleton instance()
{
Singleton s;
return s;
}
结果报错构造函数和析构函数是private
,第一行就编译不过
测试2
测试下面这种方法1
2
3
4
5
6
7
8
9static Singleton* instance1()
{
Singleton *s = new Singleton();
return s;
}
Singleton* s1 = Singleton::instance1();
Singleton *s2 = Singleton::instance1();
delete s1;
delete s2;
结果发现有两个构造函数,而且无法析构。
有缺陷的懒汉模式
1 | class Singleton |
测试: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 | class SingleTon |
改用智能指针做静态类型,
1 | // 在类外要初始化静态变量: SingleTon::Ptr SingleTon::instance = nullptr; |
结果二者地址相同,也运行了析构函数。 加了锁,使用互斥量来达到线程安全。这里使用了两个if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用getInstance
的方法都加锁,锁的开销毕竟还是有点大的。
缺点:使用智能指针会要求用户也得使用智能指针;使用锁也有开销; 同时代码量也增多了;在某些平台,双检锁会失效
饿汉模式
优点:不需要加锁,执行效率高,线程安全的
缺点:初始化即实例化,浪费内存1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class 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 |
|
这种方式所用到的特性是在C++11标准中的Magic Static特性。如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。
在 MeyersInstance() 函数内定义局部静态变量的好处是,构造函数只会在第一次调用MeyersInstance() 时被初始化, 保证了成员变量和 Singleton 本身的初始化顺序。
它还有一个潜在的安全措施, MeyersInstance() 返回的是对局部静态变量的引用, 如果返回的是指针, MeyersInstance() 的调用者很可能会误认为他要检查指针的有效性, 并负责销毁。
Qt中的全局指针
Qt里有一个全局指针qApp
,在任意地方都能使用,看看是不是单例模式。
QApplication中:1
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//头文件中
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
18class SingleTon;
typedef boost::shared_ptr<SingleTon> SingleTonPtr;
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
中。这样一来就不能实现单例了,看来这种做法不可行。