设计模式
设计模式简单来说就是在解决某一类问题场景时,有既定的,优秀的代码框架可以直接使用,与我们自己摸索出来的问题解决之道相比较,有以下优点可取:
1、代码更易于维护,代码的可读性,复用性,可移植性,健壮性会更好。
2、当软件原有需求有变更或者增加新的需求时,合理的设计模式的应用,能够做到软件设计要求的“开-闭原则”,即对修改关闭,对扩展开放,使软件原有功能修改,新功能扩充非常灵活。
3、合理的设计模式的选择,会使软件设计更加模块化,积极的做到软件设计遵循的根本原则“高内聚,低耦合”。
文章目录
单例模式
参考 “C++设计模式—单例模式”
参考 “C++ 线程安全的单例模式总结”
概念
单例模式指在整个系统生命周期里,保证一个类只能产生一个实例,并提供一个全局访问点,确保该类的唯一性。
这种模式的实现方式是通过将类的构造函数私有化,从而防止外部直接创建对象,而是通过类内部的静态方法来返回唯一的实例。单例模式在需要控制资源的使用情况下非常有用,例如数据库连接池、线程池等。同时,单例模式也可以用来实现全局状态的共享和访问。
如果一个类的对象是单例模式,也可以使用这些语句来防止对象被复制或赋值。单例模式是指一个类只能有一个实例,如果允许对象被拷贝或赋值,就有可能会创建多个实例,违反单例模式的原则。
分类
单例模式可以分为懒汉式和饿汉式,两者之间的区别在于创建实例的时间不同:
- 懒汉式:指系统运行中,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例。(这种方式要考虑线程安全)
- 饿汉式:指系统一运行,就初始化创建实例,当需要时,直接调用即可。(本身就线程安全,没有多线程的问题)
单例类特点
- 构造函数和析构函数为private类型,目的禁止外部构造和析构。
- 拷贝构造和赋值构造函数为private类型,目的是禁止外部拷贝和赋值,确保实例的唯一性。
- 类里有个获取实例的静态函数,可以全局访问。
代码示例
饥汉模式
//饥汉模式
class Object{
private:
int value;
static Object objx; //ok
private:
Object(int x=0):value(x) {cout<<"create object"<<endl;}
Object(const Object&) = delete;//表示禁止使用拷贝构造函数来复制对象。
Object& operator=(const Object&) = delete;//禁止使用赋值运算符来赋值对象
public:
~Object(){cout<<"destroy object"<<endl;}
void SetValue(int x=0) {value=x;}
//void SetValue(Object * const this,int x=0); //const修饰this指针,进入函数后this指针自身不能被修改
int GetValue()const {return value;}//常方法
//int GetValue(const Object* const this); //第一个const修饰this指针的解引用,即其所指之物
//静态函数没有this指针,可以拿“对象名/类型名::”调用,不能用const修饰因为其没有this指针
static Object& GetObject()//类内的成员函数用static修饰,可以直接那类型名调用
{
//全局对象objx在函数结束后仍存在,所以可以以引用的形式返回
return objx;//必须以引用的形式返回,不能以值的形式返回,因为会建立一个将亡值返回(需要调动拷贝构造函数)
}
};
//初始化静态成员变量
Object Object::objx(10);
//第一个Object表示对象objx的类型,第二个Object::表示objx是Object的静态成员
//Object类中定义了一个名为objx的静态成员变量,并在类外部进行了初始化。在C++中,静态成员变量的初始化需要在类外部进行,而不是在构造函数中进行。
//objx是Object类类域中定义的一个静态成员变量,则可以调用私有的构造函数,
Object Objz(10);//error 定义一个全局对象不能通过,无法访问private成员,构造函数为私有
int main()
{
//Object::objx.value = 0; //error 在主函数中无法访问objx的私有成员value,只能在类内访问
Object& obja = Object::GetObject();
Object& objb = Object::GetObject();
obja.SetValue(100);
cout << obja.GetValue() << endl;//100
cout << objb.GetValue() << endl;//100
}
运行结果如下:
可见只调用了一次构造函数与析构函数,对象obja
与objb
的地址也相同。
饿汉式单例模式,顾名思义,就是程序启动时就实例化了该对象,并没有推迟到第一次使用该对象时再进行实例化;如果运行过程中没有使用到,该实例对象就被浪费掉了。
注意
需要将构造函数设置为私有,且delete拷贝构造与赋值运算符来构建对象(也可以将其设置为私有):
#include<iostream>
using namespace std ;
class Object{
private:
int value;
private:
Object(int x=0):value(x) {cout<<"create object"<<endl;}
Object(const Object&) = delete;//表示禁止使用拷贝构造函数来复制对象。
Object& operator=(const Object&) = delete;//禁止使用赋值运算符来赋值对象
public:
~Object(){cout<<"destroy object"<<endl;}
};
//简单类型,没有指针、没有动态的获取资源、没有虚函数、没有文件指针,只有基本类型属性。可以通过赋值构造函数
//如果一个类的对象是单例模式,也可以使用这些语句来防止对象被复制或赋值。单例模式是指一个类只能有一个实例,如果允许对象被拷贝或赋值,就有可能会创建多个实例,违反单例模式的原则。
//如果不写 Object(const Object&) = delete;和Object& operator=(const Object&) = delete; 就会在编译时产生缺省的拷贝构造函数和赋值
类的缺省函数:
1.缺省构造函数
2.缺省拷贝构造函数
3.缺省析构函数
4.缺省赋值运算符
5.缺省取址运算符
6.缺省取地址运算符
类中不能出现类类型自身构建的对象,否则在构建对象时会陷入无限递归,需要改为静态对象:
class Object{
private:
int value;
Object objx;//error 类中不能出现类类型自身构建的对象,否则在构建对象时会陷入无限递归
};
class Object{
private:
int value;
static Object objx; //ok
}
懒汉模式
普通懒汉模式(未考虑线程安全)
class Object {
private:
int value;
static Object* pobj;
private:
Object(int x = 0) :value(x) { cout << "create object" << endl; }
Object(const Object&) = delete;//表示禁止使用拷贝构造函数来复制对象。
Object& operator=(const Object&) = delete;//禁止使用赋值运算符来赋值对象
public:
~Object() { cout << "destroy object" << endl; }
void SetValue(int x = 0) { value = x; }
int GetValue()const { return value; }//常方法
//GetInstance()方法是一个静态方法,用于获取Object类的唯一实例的指针。在该方法内部,首先判断指针pobj是否为空,如果为空,则通过调用Object类的私有构造函数创建一个新的实例,并将该实例的指针赋值给pobj。如果pobj不为空,则直接返回pobj的值。由于pobj是一个静态指针,它的值在程序运行期间只会被赋值一次,因此Object类只会有一个实例。同时,由于Object类的构造函数是私有的,因此无法在类的外部直接创建新的实例,只能通过GetInstance()方法获取唯一实例的指针。这种设计模式被称为单例模式,用于确保某个类只有一个实例。
static Object* GetInstance()
{
if (nullptr == pobj)
{
pobj = new Object();
}
return pobj;
}
//定义一个嵌套类,在该类的析构函数中,自动释放外层类的资源
class CRelease
{
public:
~CRelease() { delete pobj; }
};
//通过该静态对象在程序结束时自动析构的特点,来释放外层类的对象资源
static CRelease release;
};
Object* Object::pobj = nullptr;
Object::CRelease Object::release;
int main()
{
Object* p1 = Object::GetInstance();
Object* p2 = Object::GetInstance();
Object* p3 = Object::GetInstance();
cout << p1 << " " << p2 << " " << p3 << endl;
}
符合单例模式的要求,三次获取的都是同一个对象(地址相同),而且程序启动时,只对pobj
指针初始化了空值,等第一次调用GetInstance
函数时,由于pobj
指针为nullptr
,才进行对象的实例化,所以是一个懒汉式单例模式。
线程安全的单例模式
1. 饿汉单例模式的线程安全特性
饿汉单例模式中,单例对象定义成了一个static静态对象,它是在程序启动时,main函数运行之前就初始化好的,因此不存在线程安全问题,可以放心的在多线程环境中使用。
2. 懒汉单例模式的线程安全特性
懒汉单例模式,获取单例对象的方法如下:
static Object* GetInstance()
{
if (nullptr == single)
{
single = new Object();c
}
return pobj;
}
GetInstance()
是一个不可重入函数,在多线程环境中执行,会出现竞态条件问题。
pobj = new Object()
它会做三件事情,开辟内存,调用构造函数,给single指针赋值,那么在多线程环境下,就有可能出现如下问题:
- 线程A先调用
GetInstance()
函数,由于pobj
为nullptr
,进入if语句。 new()
操作先开辟内存,此时A线程的CPU时间片到了,切换到B线程。- B线程由于
pobj
为nullptr
,也进入if语句了,开始new()
操作。
new()
操作并非原子操作,所以需要在if()
之外对整个判断语句加锁
//直接利用thread库与mutex库进行加锁
class Object {
private:
int value;
static Object* pobj;
private:
Object(int x = 0) :value(x) { cout << "create object" << endl; }
Object(const Object&) = delete;//表示禁止使用拷贝构造函数来复制对象。
Object& operator=(const Object&) = delete;//禁止使用赋值运算符来赋值对象
public:
~Object() { cout << "destroy object" << endl; }
void SetValue(int x = 0) { value = x; }
int GetValue()const { return value; }//常方法
static Object* GetInstance()
{
//new()操作并非原子操作,所以需要在if()之外对整个判断语句加锁
mtx.lock();//加锁,创建完成后需要解锁
if (nullptr == pobj)
{
pobj = new Object();
/*new在线程进行中可能被打断
1 sizeof(Object); 2 malloc(); 3 调用拷贝构造函数构建对象; 4 返回对象地址给pobj。
*/
}
mtx.unlock();//解锁
return pobj;
}
//定义一个嵌套类,在该类的析构函数中,自动释放外层类的资源
class CRelease
{
public:
~CRelease() { delete pobj; }
};
//通过该静态对象在程序结束时自动析构的特点,来释放外层类的对象资源
static CRelease release;
};
Object* Object::pobj = nullptr;
Object::CRelease Object::release;
int main()
{
Object* p1 = Object::GetInstance();
Object* p2 = Object::GetInstance();
Object* p3 = Object::GetInstance();
cout << p1 << " " << p2 << " " << p3 << endl;
}
Linux系统下,利用pthread库
#include <iostream>
#include <pthread.h>
using namespace std;
class CSingleton
{
public:
static CSingleton* getInstance()
{
//获取互斥锁
pthread_mutex_lock(&mutex);
if (nullptr == single)
{
single = new CSingleton();
}
//释放互斥锁
pthread_mutex_unlock(&mutex);
return single;
}
private:
static CSingleton* single;
CSingleton() { cout << "CSingleton()" << endl; }
~CSingleton()
{
pthread_mutex_destroy(&mutex); // 释放锁
cout << "~CSingleton()" << endl;
}
CSingleton(const CSingleton&);
class CRelease
{
public:
~CRelease() { delete single; }
};
static CRelease release;
//定义线程间的互斥锁
static pthread_mutex_t mutex;
};
CSingleton* CSingleton::single = nullptr;
CSingleton::CRelease CSingleton::release;
//互斥锁的初始化
pthread_mutex_t CSingleton::mutex = PTHREAD_MUTEX_INITIALIZER;
int main()
{
CSingleton* p1 = CSingleton::getInstance();
CSingleton* p2 = CSingleton::getInstance();
CSingleton* p3 = CSingleton::getInstance();
return 0;
}
优化,把互斥锁封装成一个类:
#include <iostream>
#include <pthread.h>
using namespace std;
//对互斥锁操作的封装
class CMutex
{
public:
CMutex() { pthread_mutex_init(&mutex, NULL); } // 初始化锁
~CMutex() { pthread_mutex_destroy(&mutex); } // 销毁锁
void lock() { pthread_mutex_lock(&mutex); } // 获取锁
void unlock() { pthread_mutex_unlock(&mutex); } // 释放锁
private:
pthread_mutex_t mutex;
};
class CSingleton
{
public:
static CSingleton* getInstance()
{
if (nullptr == single)
{
//获取互斥锁
mutex.lock();
/*
这里需要再添加一个if判断,否则当两个
线程都进入这里,又会多次new对象,不符合
单例模式的涉及
*/
if (nullptr == single)
{
single = new CSingleton();
}
//释放互斥锁
mutex.unlock();
}
return single;
}
private:
static CSingleton* single;
CSingleton() { cout << "CSingleton()" << endl; }
~CSingleton() { cout << "~CSingleton()" << endl; }
CSingleton(const CSingleton&);
class CRelease
{
public:
~CRelease() { delete single; }
};
static CRelease release;
//线程间的静态互斥锁
static CMutex mutex;
};
CSingleton* CSingleton::single = nullptr;
CSingleton::CRelease CSingleton::release;
//定义互斥锁静态对象
CMutex CSingleton::mutex;
int main()
{
CSingleton* p1 = CSingleton::getInstance();
CSingleton* p2 = CSingleton::getInstance();
CSingleton* p3 = CSingleton::getInstance();
return 0;
}
另外的例子
//懒汉模式,需要考虑线程安全
std::mutex mtx; //全局互斥锁
class Object{
private:
int value;
static Object* pobj;//定义一个静态指针
private:
Object(int x=0):value(x) {cout<<"create object"<<endl;}
Object(const Object&) = delete;//表示禁止使用拷贝构造函数来复制对象。
Object& operator=(const Object&) = delete;//禁止使用赋值运算符来赋值对象
public:
~Object(){cout<<"destroy object"<<endl;}
void SetValue(int x=0) {value=x;}
int GetValue()const {return value;}//常方法
static Object& GetRefObject()//获得对象的引用。类内的成员函数用static修饰,可以直接拿类型名调用
{
//new()操作并非原子操作,所以需要在if()之外对整个判断语句加锁
mtx.lock();//加锁,创建完成后需要解锁
if(nullptr == pobj)
{
pobj=new Object();
/*new在线程进行中可能被打断
1 sizeof(Object); 2 malloc(); 3 调用拷贝构造函数构建对象; 4 返回对象地址给pobj。
*/
}
mtx.unlock();//解锁
return *pobj;
}
};
Object* Object::pobj = nullptr;
void fun1()
{
Object& obja=Object::GetRefObject();//第一次构建时pobj==nullptr,则调动构造函数构建对象
obja.SetValue(100);
cout<< &obja <<endl;
}
void fun2()
{
Object& objb=Object::GetRefObject();//第二次在调用时pobj!=nullptr,则不再调动构造函数
cout<<objb.GetValue()<<endl;
cout<< &objb <<endl;
}
int main()
{
std::thread tha(fun1);
std::thread thb(fun2);
//当static Object& GetRefObject()中未添加互斥锁mutex时,fun1和fun2最终所打印出的obja和objb的地址不同,说明并非一个单例模式
//当加锁操作在if()语句之中,因为new()操作并非一个原子操作(需要四步来创建对象),所以仍然有可能打印出两个不同的地址,说明构建了两个对象
tha.join();//join()等待线程结束
thb.join();
}
x86
x64
补充知识
C++中不能把静态函数定义为常性(用const修饰)
看函数有无this指针
在C++中,静态函数是属于类的函数,它们不依赖于任何类的实例,因此它们不会改变任何类的成员变量。所以,静态函数可以被视为类的全局函数,但是它们只能访问类的静态成员变量和其他静态函数。
由于静态函数不依赖于类的实例,它们没有this指针。因此,静态函数不能被声明为const
。const
成员函数的作用是保证该函数不会修改对象的成员变量,但是静态函数没有this指针,也就无法访问对象的成员变量,因此const
修饰符在静态函数中没有意义。
总之,静态函数不依赖于类的实例,它们只能访问类的静态成员变量和其他静态函数,并且它们没有this指针,因此不能被声明为const
。
全局函数、友元函数也没有this指针,故不能用const
修饰。类的成员函数才能用const
修饰。
静态成员只有一份,在数据区中,被所有创建的实例对象所共享。
C++中的四个域
局部变量域
块域:在函数局部变量域之中,块结束时块内创建的变量被释放。块域是对函数进行局部加锁,例如进入块域就开始锁,块结束之后则进行解锁。
类域:在类外调用类内的成员,前面必须加上类名::
,eg:Object Object::objx(10);
全局域
void fun()
{
int x;//局部变量域
if(x==0)
{
mutex;
int y=x;//块域
}
}
不可重入函数
不可重入函数是指在多线程环境下,如果该函数被多个线程同时调用,可能会导致数据竞争和不确定的行为。这是因为不可重入函数在执行过程中使用了全局变量或静态变量等共享资源,而这些资源在多线程环境下可能会被多个线程同时访问和修改,导致数据不一致或者程序崩溃等问题。
例如,如果一个不可重入函数使用了一个全局变量来存储状态,那么当多个线程同时调用该函数时,它们可能会同时修改该全局变量的值,导致状态不一致。另外,如果该函数使用了非线程安全的库函数,也可能会导致类似的问题。
为了避免不可重入函数的问题,可以使用线程安全的函数或者使用互斥锁等机制来保护共享资源的访问。另外,也可以使用可重入函数来代替不可重入函数,可重入函数在执行过程中不使用全局变量或静态变量等共享资源,因此可以安全地在多线程环境下并发调用。
下面是一个使用不可重入的strtok
函数的示例代码:
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello,World";
char *token;
token = strtok(str, ",");
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, ",");
}
return 0;
}
在上面的代码中,我们使用了strtok
函数将字符串"Hello,World"按照逗号分割成两个子串"Hello"和"World"。strtok
函数使用了一个静态指针来记录当前分割位置,因此在多线程环境下,如果多个线程同时调用该函数,可能会导致数据竞争和不确定的行为。
下面是一个使用可重入的strtok_r
函数的示例代码:
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello,World";
char *token, *saveptr;
token = strtok_r(str, ",", &saveptr);
while (token != NULL) {
printf("%s\n", token);
token = strtok_r(NULL, ",", &saveptr);
}
return 0;
}
在上面的代码中,我们使用了可重入的strtok_r
函数将字符串"Hello,World"按照逗号分割成两个子串"Hello"和"World"。strtok_r
函数使用了一个指向指针的指针来记录当前分割位置,因此可以安全地在多线程环境下并发调用。