一、设计模式
设计模式:代表的是最佳实践,是软件开发过程中面临一般问题的解决方案,是面向对象语言所共有的。(类似于打仗时,使用的兵法)。设计模式一共有二十三种,本文重点介绍单例模式。
常见的设计模式:
适配器模式、迭代器模式、工厂模式、单例模式、生成器模式、装饰器模式、门面模式、代理模式、桥接模式
适配器模式
在之前学习栈和队列时,我们发现栈和队列的底层,并非自己实现,而是通过调用栈和队列(库中默认是双端队列)来实现的;以栈为例:我们在使用时不在乎,栈的底层实现,只要它能够满足尾插和尾删即可。通过这种方法,更好的体现了STL库的良好的复用性
迭代器模式
迭代器是STL库中算法和容器的纽带,使用迭代器便可以用同一种方法对不同的容器进行访问;迭代器类似与指针。迭代器的存在很好的保护了,面向对象的封装性,减少了使用成本。
假如在没有迭代器的情况下,如果要访问红黑树的某一个结点时,就必须满足以下条件:
1、必须把其根节点暴露出来(这样破坏了其的封装性)
2、树的访问往往是通过遍历来实现,不能直接访问某个结点
3、要访问某个结点,必须了解其底层实现(学习成本太大,不够方便)
二、单例模式(Singleton)
1、概念
一个类始终只能创建一个实例,则这个类被称为单例类,这种模式就被称为单例模式。(线程安全且高效)
2、特点
- 单例类保证全局只有一个唯一实例对象
- 单例类提供获取这个唯一实例的接口(GetInstance :获得实例)
3、实现
方法一、不考虑线程安全
#define _CRT_SECURE_NO_WARNIN 1
#pragma once
class Singleton
{
public:
static Singleton* GetInstance() //定义成静态方法,使得这个属性属于整个类
{
if(_inst == NULL) //保证只有一份
{
_inst = new Singleton;
}
return _inst;
}
void Print()
{
printf("%d\n",_a);
}
private:
Singleton() //必须自己定义构造函数,且设为私有
:_a(0) //1.防止调用默认的构造函数 2、防止利用构造函数创建对象
{
}
Singleton(const Singleton&); //防拷贝,只声明不定义
Singleton& operator=(const Singleton&); //防止进行深浅拷贝
private:
int _a;
static Singleton* _inst; //静态变量全局只存一份 ,此处的变量可以是 threadpool memorypool
};
Singleton* Singleton:: _inst = NULL; //静态变量必须在类外面,进行初始化
注意:
单例类的名字可以根据实际使用适当的名字,而非只能使用singleton;
单例类中的变量可以是线程池、内存池等(全局只有一份)等;
赋值运算符的重载不一定要设为私有,只在在一些情况下会存在深浅拷贝问题,因此最好设为私有;
方法二、考虑线程安全
上面所实现的单例类,在多线程的情况下,会出现线程安全问题
主要是由于下面的代码引起:
static Singleton* GetInstance()
{
if(_inst == NULL)
{
_inst = new Singleton;
}
return _inst;
}
在多核情况下:
当多个线程同时执行这段代码时,每个线程都创建了一个对象;这样便不是全局唯一的实例对象了;
在只有一个CPU 的情况下:
说明:每一个被CPU调度的线程,只能在时间片内执行;
由于以上代码不是原子的,假设有两个线程执行上述代码:
首先线程一被调度 :在执行完 if(_inst == NULL) ,时间片用完了;这时线程二被调度: 在执行完 new Singleton ,时间片用完了;线程一被再次调度,会执行 _inst = new Singleton ,那么此时线程一创建了一个对象,并且线程二创建的对象被丢弃了,这时出现了问题。
普通版:
class Singleton
{
public:
static Singleton* GetInstance()
{
_mtx.lock(); //加锁
if(_inst == NULL)
{
_inst = new Singleton;
}
_mtx.unlock(); //解锁
return _inst;
}
void Print()
{
printf("%d\n",_a);
}
private:
Singleton()
:_a(0)
{
}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
private:
int _a;
static Singleton* _inst;
static mutex _mtx; //只有一个锁
};
mutex Singleton::_mtx ; //调用mutex的构造函数
Singleton* Singleton:: _inst = NULL;
上面所实现的单例类,存在一定的缺陷。如果在锁之间的代码出现异常时,便会导致没有解锁,导致大量的线程阻塞在_mtx.lock() 处,进而可能会出现死锁。出现死锁便是很危险的事情。可以对锁资源,利用RAII思想来解决此问题。
RAII:资源获得即初始化,资源在构造函数时初始化,自动调用析构函数,释放资源。
class Lock()
{
public:
Lock(mutex& mtx)
:_mtx(mtx) //引用必须在初始化列表中初始化
{
_mtx.lock();
}
~Lock()
{
_mtx.unlock();
}
private:
mutex& _mtx; //保证只有一把锁
}
优化版:
class Singleton
{
public:
static Singleton* GetInstance()
{
Lock lock(_mtx); //使用RAII思想
if(_inst == NULL)
{
_inst = new Singleton;
}
return _inst;
}
void Print()
{
printf("%d\n",_a);
}
private:
Singleton()
:_a(0)
{
}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
private:
int _a;
static Singleton* _inst;
static mutex _mtx;
};
mutex Singleton::_mtx ;
Singleton* Singleton:: _inst = NULL;
注意:
1.此处可以自己通过RAII思想实现一个锁,在C++11中提供了lock_guard类,也可以实现自动释放
2.lock_guard是一个模板类,因为不同的平台中互斥资源的命名不同,例如:在windows中称为临界资源 、在Linux中称为mutex(本文用到的是mutex)
方法三、考虑安全和效率
方法二虽然解决了线程安全,但并不高效。
高效版:
class Singleton
{
public:
static Singleton* GetInstance()
{
if(_inst == NULL) //双检查
{
lock_guard<mutex> lock(_mtx);
if(_inst == NULL)
{
_inst = new Singleton;
}
}
return _inst;
}
void Print()
{
printf("%d\n",_a);
}
private:
Singleton()
:_a(0)
{
}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
private:
int _a;
static Singleton* _inst;
static mutex _mtx; //只有一个锁
};
mutex Singleton::_mtx ; //调用mutex的构造函数
Singleton* Singleton:: _inst = NULL;
单例模式分类:
- 懒汉模式 lazy load : 在调用GetInstance()时才创建对象
- 饿汉模式 一开始 load : 一开始就创建
由分类可知:上面的单列模式都属于懒汉模式。
饿汉模式:
class Singleton
{
public:
static Singleton* GetInstance() //此处也可以返回对象的引用
{
assert(_inst);
return _inst;
}
void Print()
{
printf("%d\n",_a);
}
private:
Singleton()
:_a(0)
{
}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
static Singleton* _inst = new Singleton;
private:
int _a;
static mutex _mtx;
};
mutex Singleton::_mtx ;
比较:
懒汉模式:相对复杂,但在各种场景下都使用;
饿汉模式:简结,但适用性有限制 例如:动态库:只有在运行时才加载