1.C++中的设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
2.单例模式
2.1什么是单例模式
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
优点:
(1)由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显
(2)减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
(3)避免对资源的多重占用。如避免对同一个资源文件的同时写操作
(4)单例模式可以在系统设置全局的访问点,优化和共享资源访问
缺点:
单例模式一般没有接口,扩展困难。不利于测试
2.2C++中单例模式的实现
单例模式是只能有一个实例化的对象的类。这个类就要禁止别人new出来,或者通过直接定义。在C++中,类对象被创建时需要操作系统为对象分配内存空间,并自动调用构造函数初始化对象。所以我们需要把类的构造函数私有化,禁止生成其他的实例化对象。构造函数被私有化后,就只能被类内部的成员函数调用,所以我们还需要一个公有函数供类外部调用。然后这个函数返回一个对象。为了保证多次调用这个函数返回的是一个对象,我们可以把类内部要返回的对象设置为静态变量。且应该把这个静态成员设置为 null,在共有函数里去判断,只有在静态实例成员为 null时,也就是没有被初始化的时候,才去初始化它,且只被初始化一次。
单例模式的特征总结:
1、一个类只有一个实例
2、提供一个全局访问点
3、禁止拷贝
实现步骤:
1、实现只有一个实例,将构造函数声明为私有
2、提供一个全局访问点,类中创建静态成员和静态成员方法
3、禁止拷贝,把拷贝构造函数声明为私有,并且不提供实现,将赋值运算符声明为私有,防止对象的赋值。
2.3懒汉模式
懒汉模式,故名思意,一个人很懒不愿意主动做事,只有你催他他才会动起来。即只有在主动调用静态成员函数的时候才会实例化的对象,以时间换空间。
优点:简单
缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定
#include<iostream>
using namespace std;
class Singleton
{
public:
static Singleton* GetInstance()
{
//第一次调用获取实例的函数时,静态类的变量指针空,所以会创建一个对象出来,第二次调用就不是空了,直接返回第一次的对象指针(地址)
if (instance == nullptr)
{
instance = new Singleton();
}
return instance;
}
private:
Singleton()//构造函数私有化
{
cout << "实例化了" << count << "个对象" << endl;
count++;
}
// C++98防拷贝
//Singleton(Singleton const&);
//Singleton& operator=(Singleton const&);
// C++11防拷贝
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
int count = 1;
static Singleton* instance;//全局访问点
};
Singleton* Singleton::instance = nullptr;
int main()
{
//只有我们调用GetInstance时才会生成对象(懒汉)
Singleton* p=Singleton::GetInstance();
cout << p << endl;
Singleton* p2=Singleton::GetInstance();
cout << p2 << endl;
Singleton* p3=Singleton::GetInstance();
cout << p3 << endl;
system("pause");
return 0;
}
三个地址一样,证明我们的单例类的正确的。
2.3.1多线程下的懒汉模式
上述代码在单线程的情况下,运行正常,但是遇到了多线程就出问题,假设有两个线程同时运行了这个单例类,同时运行到了判断 if 语句,并且当时instance 为空,那么两个线程都会去运行并创建初始化实例,此时就不满足单例类的要求了。这种情况我们需要通过加一个锁🔒去解决问题。
这里我们使用的时双重检查锁(double-check),能够保证效率和安全。
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
class Singleton
{
public:
static Singleton* GetInstance()
{
// 注意这里一定要使用Double-Check的方式加锁,才能保证效率和线程安全
//双重检测机制,提高了单例模式在多线程下的效率,因为这样的代码,只需要在第一次创建实例的时候,需要加锁,
//其他的时候,线程无需排队等待加锁之后,再去判断了,比较高效。
if (instance==nullptr) {
m_mtx.lock();
if (instance==nullptr) {
instance = new Singleton();
}
m_mtx.unlock();
}
return instance;
}
class CGarbo //实现一个内部类来完成析构 防止内存泄漏
{
public:
~CGarbo() {
if (Singleton::instance)
delete Singleton::instance;
}
};
private:
// 构造函数私有
Singleton()
{
cout << "实例化了" << count << "个对象" << endl;
count++;
}
int count = 1;
// 防拷贝
Singleton(Singleton const&);
Singleton& operator=(Singleton const&);
static Singleton* instance; // 单例对象指针
static mutex m_mtx; //互斥锁
//单例类中声明一个触发垃圾回收类的静态成员变量,它的唯一工作就是在析构函数中删除单例类的实例,
static CGarbo Garbo; //程序运行结束时,系统会调用Singleton的静态成员garbage的析构函数,该析构函数会删除单例的唯一实例
};
Singleton* Singleton::instance = nullptr; //静态成员类外声明
Singleton::CGarbo Singleton::Garbo;
mutex Singleton::m_mtx;
void func(int n) {
cout << Singleton::GetInstance() << endl;
}
// 多线程环境下演示上面GetInstance()加锁和不加锁的区别。
int main()
{
thread t1(func, 10);//thread创建线程t1 调用GetInstance()
thread t2(func, 10);//thread创建线程t2 调用GetInstance()
t1.join();//join()等待模式,执行完再执行下一个
t2.join();
cout << Singleton::GetInstance() << endl;//
cout << Singleton::GetInstance() << endl;
system("pause");
return 0;
}
地址一样,证明我们的单例类的正确的。
有两个线程同时到达,即同时调用 GetInstanc(),此时Singleton==nullptr,所以两个线程都可以通过第一重if语句。进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重singleton = = null ,而另外的一个线程则会在 lock 语句的外面等待。
当第一个线程执行new Singleton()语句后,便会退出锁定区域,此时,第二个线程便可以进入 lock 语句块,此时,如果没有第二重 Singleton = = nullptr 的话,那么第二个线程还是可以调用 new Singleton()语句,这样第二个线程也会创建一个 Singleton 实例,这样也还是违背了单例模式的初衷的。所以这里必须要使用双重检查锁定。
考虑在没有第一重 if(Singleton = = nullptr)的情况下,当有两个线程同时到达,此时,由于 lock 机制的存在,第一个线程会进入 lock 语句块,并且可以顺利执行 new Singleton(),当第一个线程退出 lock 语句块时, Singleton 这个静态变量已不为 null 了,所以当第二个线程进入 lock 时,还是会被第二重singleton = = nullptr 挡在外面,而无法执行 new Singleton()。
所以在没有第一重 singleton = = nullptr的情况下,也是可以实现单例模式的。那么为什么需要第一重 singleton = = nullptr 呢?
这里就涉及一个性能问题了,因为对于单例模式的话,new Singleton()只需要执行一次就可以了,而如果没有第一重 singleton = = nullptr 的话,每一次有线程进入 GetInstance()时,均会执行锁定操作来实现线程同步,这是非常耗费性能的,而如果我加上第一重 Singleton = = nullptr 的话,那么就只有在第一次,也就是 Singleton = =nullptr 成立时的情况下执行一次锁定以实现线程同步,而以后的话,便只要直接返回 Singleton 实例就 OK 了而根本无需再进入 lock 语句块了,这样就可以解决由线程同步带来的性能问题了。
2.4饿汉模式
饿汉模式,故名思意:人饿了就要吃东西来填饱肚子,所以需要提前准备好食物。即在初始化静态成员的时候进行实例化对象,以空间换时间。
由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。
class Singleton
{
private:
Singleton(){}//构造函数私有
static Singleton *instance;//提供全局访问点 静态成员
// C++98防拷贝
//Singleton(Singleton const&);
//Singleton& operator=(Singleton const&);
// C++11防拷贝
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
public:
static Singleton* GetInstance() {
return instance;
}
};
Singleton* Singleton::instance = new Singleton();//饿汉模式的关键:初始化即实例化
类中的静态变量在外部声明的时候就可以new一个对象出来,因为instance是Singleton的成员,它是可以调用构造函数。锁也不用加了,因为我们调用Singleton::GetInstance()之前这个类就已经被实例化了,属于线程安全。我们调用这个函数的目地只是为了得到这个对象的地址。以空间换时间。