掌握常见特殊类的设计方式
一.设计一个类,不能被拷贝
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
- C 98 将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
class CopyBan
{
// ...
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
//...
};
原因:
- 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝了
- 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
- C 11 C 11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。
class CopyBan
{
// ...
CopyBan(const CopyBan&)=delete;
CopyBan& operator=(const CopyBan&)=delete;
//...
};
二.设计一个类,只能在堆上创建对象
1. 普通类的创建对象
普通的类,可以在三种位置上创建对象:
- 栈
- 堆
- 静态区
#include<iostream>
using namespace std;
class HeapOnly
{};
int main()
{
HeapOnly hp1;//栈
HeapOnly* php2 = new HeapOnly;//堆
static HeapOnly hp3;//静态区
return 0;
}
2.只能在堆上创建对象的类
要想只能在堆上创建对象,那一定需要在构造函数上动手脚,因为构造函数默认在栈上创建对象。
实现方式:
- 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
- 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建。
#include<iostream>
using namespace std;
class HeapOnly
{
public:
static HeapOnly* CreateObject()
{
return new HeapOnly;
}
private:
HeapOnly()
{}
};
int main()
{
HeapOnly* php = HeapOnly::CreateObject();
return 0;
}
为什么要加上static?
如果CreateObject不加上static,那么在调用该方法就需要在存在对象的基础上才能使用该方法,而该对象默认一定会用构造函数,但是构造函数已经私有化,这就是一个先有鸡还是先有蛋的问题,因此一定要加上static。
但是就目前的情况,仍然可能在栈上开辟对象,首先友元一定是可以的。其次,拷贝构造函数没有显示化调用会默认生成,因此,如下方式仍可以在栈上创建对象:
代码语言:javascript复制int main()
{
HeapOnly* php2 = HeapOnly::CreateObject();
HeapOnly php3(*php2);//栈上创建对象
return 0;
}
所以,拷贝构造函数同样需要禁掉,才是只能在堆上创建的类:
代码语言:javascript复制#include<iostream>
using namespace std;
class HeapOnly
{
public:
static HeapOnly* CreateObject()
{
return new HeapOnly;
}
private:
HeapOnly() {}
HeapOnly(const HeapOnly&) = delete;
};
int main()
{
HeapOnly* php2 = HeapOnly::CreateObject();
return 0;
}
只在堆上创建类的第二种方式:析构私有化
如果析构私有化,那么直接创建对象会显示没有合适的构造函数,从而无法在栈上创建对象。
代码语言:javascript复制class HeapOnly
{
public:
HeapOnly()
{}
private:
~HeapOnly()
{}
HeapOnly(const HeapOnly&) = delete;
};
int main()
{
HeapOnly hp1;
return 0;
}
但此时可以在堆上创建,那么此时分为如下步骤:
- 析构函数私有化
- 构造函数public显示调用
- 新增Destory方法,用来释放堆空间
class HeapOnly
{
public:
HeapOnly()
{}
void Destory()
{
this->~HeapOnly();
}
private:
~HeapOnly()
{}
HeapOnly(const HeapOnly&) = delete;
};
int main()
{
HeapOnly* php1 = new HeapOnly;
php1->Destory();
return 0;
}
Destory对于static没有要求,用不用static修饰完全是我们自己所决定的。
注:在vs2019中,上面的this必须显示调用才没有错误。
三.设计一个类,只能在栈上创建对象
方法一:(同上)
- 将构造函数私有化。
- 然后设计静态方法创建对象返回即可。
//请设计一个类,只能在栈上创建对象
class StackOnly
{
public:
static StackOnly CreateObj()
{
return StackOnly();
}
private:
StackOnly()
{}
};
int main()
{
StackOnly so1 = StackOnly::CreateObj();
// 下面两种静态区和堆的位置都不能创建
//static StackOnly so2;
//StackOnly* pso3 = new StackOnly;
return 0;
}
实际上,这种方法也没有彻底的封死,下面这种方式仍然可以在静态区创建:
代码语言:javascript复制int main()
{
static StackOnly so2 = StackOnly::CreateObj();
return 0;
}
解决这个问题的方式:
这里设计到的强制类型转换,强制类型转换中间会生成一个临时对象,将这个临时对象拷贝给需要定义的对象,若把拷贝构造封住,那么不仅这个会报错,前面的也会报错,因为前者的赋值也是将返回的对象临时拷贝:
因此,没有什么很好的办法去完全的封死。但硬要封死,即把拷贝构造封住,那就不要用 = 获取,而是直接调用,如下:
代码语言:javascript复制//请设计一个类,只能在栈上创建对象
class StackOnly
{
public:
static StackOnly&& CreateObj()
{
return StackOnly();
}
void Print() const
{
cout << "StackOnly::Print()" << endl;
}
private:
StackOnly()
{}
StackOnly(const StackOnly&) = delete;
};
int main()
{
/*StackOnly so1 = StackOnly::CreateObj();
static StackOnly so2 = StackOnly::CreateObj();*/
StackOnly::CreateObj().Print();
const StackOnly& so4 = StackOnly::CreateObj();
so4.Print();
return 0;
}
目的就是防止拷贝。
ps,由于StackOnly()是局部对象,出了作用域被销毁,因此采用右值引用才可以传出。
方法二:封注operator new
和operator delete
class StackOnly
{
public:
StackOnly()
{}
static StackOnly CreateObj()
{
return StackOnly();
}
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
};
这种方式同样可以,因为在new对象的过程中,一定会存在operator new的步骤。但是这种方法只能封住堆上的,却无法封住静态的。
所以最好的方式就是用方式一。
四.设计一个类,不能被继承
- C 98方式
// C 98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
- C 11方法
final关键字,final修饰类,表示该类不能被继承。
代码语言:javascript复制class A final
{
// ....
};
五.单例模式
- 单例模式:只能创建一个对象。
1.什么是设计模式?
设计模式是在软件工程中经过反复实践证明的一套解决问题的经验总结,用于解决常见的设计问题。以下是一些常见的设计模式:
- 创建型模式(Creational Patterns):
- 工厂方法模式(Factory Method Pattern)
- 抽象工厂模式(Abstract Factory Pattern)
- 单例模式(Singleton Pattern)
- 建造者模式(Builder Pattern)
- 原型模式(Prototype Pattern)
- 结构型模式(Structural Patterns):
- 适配器模式(Adapter Pattern)
- 桥接模式(Bridge Pattern)
- 组合模式(Composite Pattern)
- 装饰者模式(Decorator Pattern)
- 外观模式(Facade Pattern)
- 享元模式(Flyweight Pattern)
- 代理模式(Proxy Pattern)
- 行为型模式(Behavioral Patterns):
- 观察者模式(Observer Pattern)
- 状态模式(State Pattern)
- 策略模式(Strategy Pattern)
- 命令模式(Command Pattern)
- 职责链模式(Chain of Responsibility Pattern)
- 迭代器模式(Iterator Pattern)
- 中介者模式(Mediator Pattern)
- 备忘录模式(Memento Pattern)
- 访问者模式(Visitor Pattern)
- 模板方法模式(Template Method Pattern)
- 并发型模式(Concurrent Patterns):
- 信号量模式(Semaphore Pattern)
- 线程池模式(Thread Pool Pattern)
- 读写锁模式(Read-Write Lock Pattern)
- 生产者消费者模式(Producer-Consumer Pattern)
以上仅是一些常见的设计模式,实际上还有其他的设计模式。每个设计模式都有特定的应用场景和解决问题的方式。请注意,在使用设计模式时,应根据具体的需求和情况来选择适当的设计模式。
比如迭代器模式,把复杂的东西给封装好,使用时就可以避免接触复杂的底层结构。 比如配接器模式等等,也是这个意思。
使用设计模式的目的: 为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
2.单例模式
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
由于全局对象只能有一个,换句话说是获取这个对象,那就需要对构造函数进行操作。
单例模式有两种实现模式:饿汉模式、懒汉模式
饿汉模式:不管你将来用不用,程序启动时就创建一个唯一的实例对象。
饿汉模式的条件:main函数之前就初始化
设计饿汉模式的步骤:
- 将构造函数设成private,以及封死拷贝构造和重载赋值
- 定义成员变量,变量类型为
static 类型名
- 在类外初始化这个单例的对象
- 添加其它成员方法
//单例模式的类:全局只有一个唯一对象
// 饿汉模式(main函数之前初始化)
// 缺点:1、单例对象初始化时对象太多,导致启动慢
// 2、多个单例类有初始化依赖关系,饿汉模式无法控制
class InfoSingleton
{
public:
static InfoSingleton& GetInstance()
{
return _sins;
}
void Insert(string name, int salary)
{
_info[name] = salary;
}
void Print()
{
for (auto kv : _info)
{
cout << kv.first << ":" << kv.second << endl;
}
}
private:
InfoSingleton()
{}
InfoSingleton(const InfoSingleton&) = delete;
InfoSingleton& operator=(const InfoSingleton& info) = delete;
map<string, int> _info;
// ...
private:
static InfoSingleton _sins;
};
InfoSingleton InfoSingleton::_sins;
int main()
{
InfoSingleton::GetInstance().Insert("张三", 10000);
InfoSingleton& infosl = InfoSingleton::GetInstance();
infosl.Insert("李四", 12000);
infosl.Insert("王五", 15000);
infosl.Insert("赵六", 11000);
infosl.Print();
cout << endl;
InfoSingleton::GetInstance().Insert("张三", 18000);
infosl.Insert("李四", 12000);
infosl.Insert("王五", 15000);
infosl.Insert("赵六", 11000);
infosl.Print();
return 0;
}
可见在调用时可以通过引用来简化代码量。
饿汉模式的缺点:
- 单例对象初始化数据太多,导致启动慢
- 多个单例类有初始化依赖关系,饿汉模式无法控制
假设有两个单例类A和B,分别代表数据库和文件系统,要求先初始化A,再初始化B,并且B会依赖A,那么此时饿汉模式就无法控制顺序。
如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
懒汉模式的条件:
- 对象在main函数之后才会创建,不会影响启动顺序
- 可以主动控制创建顺序
设计懒汉模式的步骤:(与饿汉模式基本相同)
- 将构造函数设成private,以及封死拷贝构造和重载赋值
- 定义成员变量,变量类型为
static 类型名
- 在类外初始化这个单例的对象
- 添加其它成员方法
与饿汉模式的区别:
- 对象在main函数之后才会创建,不会影响启动顺序
- 可以主动控制创建顺序
- 将对象的创建改为在堆上创建
- 懒汉模式存在多个对象一起调用GetInstance的情况,存在线程安全的风险,可能new出来多个对象,因此需要加锁,需要新增一个锁的成员对象,并定义为static类型;饿汉模式一开始就一个对象,不用创建,所以不用锁。
注意:锁不能被拷贝,因此定义锁的成员变量时可以用指针(地址)或者引用的方式定义,C 采用地址的行为不常见,用引用更好。
加锁也是有讲究的,如果像这样的代码:
代码语言:javascript复制//多个对象一起调用GetInstance,存在线程安全的风险,可能new出来多个对象,因此需要加锁
static InfoSingleton& GetInstance()
{
_pmtx.lock();
if (_psins == nullptr)//避免对象new出来以后每次都加锁,提高性能
{
_psins = new InfoSingleton;
}
_pmtx.unlock();
return *_psins;
}
由于这种方式每次都需要加锁,但实际上只有第一次创建对象才需要加锁,所以为了避免锁影响效率,使用双层if检查;此外,对于new,一旦抛异常,就需要捕获,此时可以使用try-catch,但这种写法不可行,通过之前智能指针的RAII思想,我们可以自己设定一个类:基于RAII思想的管理类,来防止锁的问题。
代码语言:javascript复制为什么try-catch不可行,因为还在加锁阶段,一旦进行捕获跳转,那么这把锁会一直锁住,为了避免出现这种情况,才使用RAII的思想。C 线程库中也有对应的库函数方法,但是这里仍然可以手撕一个。
//RAII的锁管理类
template<class Lock>
class LockGuard
{
public:
LockGuard(Lock& lk)
:_lk(lk)
{
lk.lock();
}
~LockGuard()
{
_lk.unlock();
}
private:
Lock& _lk;//成员变量用引用-->避免拷贝
};
//懒汉模式
//1、对象在main函数之后才会创建,不会影响启动顺序
//2、可以主动控制创建顺序
//问题:
class InfoSingleton
{
public:
//多个对象一起调用GetInstance,存在线程安全的风险,可能new出来多个对象,因此需要加锁
static InfoSingleton& GetInstance()
{
//第一次获取单例对象的时候创建对象
//双检查加锁
if (_psins == nullptr)//避免对象new出来以后每次都加锁,提高性能
{
// t1 t2
LockGuard<mutex> mtx(_smtx);
if (_psins == nullptr) //保证线程安全且只new一次
{
_psins = new InfoSingleton;
}
}
return *_psins;
}
void Insert(string name, int salary)
{
_info[name] = salary;
}
void Print()
{
for (auto kv : _info)
{
cout << kv.first << ":" << kv.second << endl;
}
}
private:
InfoSingleton()
{}
InfoSingleton(const InfoSingleton&) = delete;
InfoSingleton& operator=(const InfoSingleton& info) = delete;
map<string, int> _info;
// ...
private:
static InfoSingleton* _psins;
static mutex _smtx;
};
InfoSingleton* InfoSingleton::_psins = nullptr;
mutex InfoSingleton::_smtx;
int main()
{
InfoSingleton::GetInstance().Insert("张三", 10000);
InfoSingleton& infosl = InfoSingleton::GetInstance();
infosl.Insert("李四", 12000);
infosl.Insert("王五", 15000);
infosl.Insert("赵六", 11000);
infosl.Print();
cout << endl;
InfoSingleton::GetInstance().Insert("张三", 18000);
infosl.Insert("李四", 12000);
infosl.Insert("王五", 15000);
infosl.Insert("赵六", 11000);
infosl.Print();
return 0;
}
库中也有对应的加锁方法:
代码语言:javascript复制static InfoSingleton& GetInstance()
{
//第一次获取单例对象的时候创建对象
//双检查加锁
if (_psins == nullptr)//避免对象new出来以后每次都加锁,提高性能
{
// t1 t2
//LockGuard<mutex> mtx(_smtx);
std::lock_guard<mutex> lock(_smtx);//库中的方法
if (_psins == nullptr) //保证线程安全且只new一次
{
_psins = new InfoSingleton;
}
}
return *_psins;
}
new出来之后是否需要释放?
一般单例模式对象不需要考虑释放。单例模式的类的一个对象通常在整个程序运行期间都会使用,因此最后不delete也不会有问题,只要进程最终正常结束,对象的资源就会由OS自动释放。
什么时候单例模式的对象需要释放?
单例对象不用时,必须手动处理,一些资源需要保存。假设工资名单需要保存到文件里,要求系统结束之前将信息保存进去,此时就需要手动处理。所以,可以新增一个方法DelInstance(),是否需要调用取决于自己:
代码语言:javascript复制//懒汉模式
//1、对象在main函数之后才会创建,不会影响启动顺序
//2、可以主动控制创建顺序
class InfoSingleton
{
public:
//多个对象一起调用GetInstance,存在线程安全的风险,可能new出来多个对象,因此需要加锁
static InfoSingleton& GetInstance()
{
//第一次获取单例对象的时候创建对象
//双检查加锁
if (_psins == nullptr)//避免对象new出来以后每次都加锁,提高性能
{
// t1 t2
//LockGuard<mutex> mtx(_smtx);
std::lock_guard<mutex> lock(_smtx);
if (_psins == nullptr) //保证线程安全且只new一次
{
_psins = new InfoSingleton;
}
}
return *_psins;
}
//一般单例对象不需要考虑释放
//单例对象不用时,必须手动处理,一些资源需要保存
static void DelInstance()
{
//保存数据到文件
// ...
std::lock_guard<mutex> lock(_smtx);
if (_psins)
{
delete _psins;
_psins = nullptr;
}
}
void Insert(string name, int salary)
{
_info[name] = salary;
}
void Print()
{
for (auto kv : _info)
{
cout << kv.first << ":" << kv.second << endl;
}
}
private:
InfoSingleton()
{}
InfoSingleton(const InfoSingleton&) = delete;
InfoSingleton& operator=(const InfoSingleton& info) = delete;
map<string, int> _info;
// ...
private:
static InfoSingleton* _psins;
static mutex _smtx;
};
InfoSingleton* InfoSingleton::_psins = nullptr;
mutex InfoSingleton::_smtx;
int main()
{
InfoSingleton::GetInstance().Insert("张三", 10000);
InfoSingleton& infosl = InfoSingleton::GetInstance();
infosl.Insert("李四", 12000);
infosl.Insert("王五", 15000);
infosl.Insert("赵六", 11000);
infosl.Print();
cout << endl;
InfoSingleton::GetInstance().Insert("张三", 18000);
infosl.Insert("李四", 12000);
infosl.Insert("王五", 15000);
infosl.Insert("赵六", 11000);
infosl.Print();
InfoSingleton::DelInstance();//主动调用
return 0;
}
如果忘记主动调用,同样会产生错误,因此仍需要设计一个能够自动回收的方式,这里采用新增一个内部类GC,利用RAII的思想,一旦忘主动回收,其在main函数结束时就会自动回收,此时就需要新增一个成员变量以及内部类:
代码语言:javascript复制注:内部类是外部类的友元
//懒汉模式
//1、对象在main函数之后才会创建,不会影响启动顺序
//2、可以主动控制创建顺序
class InfoSingleton
{
public:
//多个对象一起调用GetInstance,存在线程安全的风险,可能new出来多个对象,因此需要加锁
static InfoSingleton& GetInstance()
{
//第一次获取单例对象的时候创建对象
//双检查加锁
if (_psins == nullptr)//避免对象new出来以后每次都加锁,提高性能
{
// t1 t2
//LockGuard<mutex> mtx(_smtx);
std::lock_guard<mutex> lock(_smtx);
if (_psins == nullptr) //保证线程安全且只new一次
{
_psins = new InfoSingleton;
}
}
return *_psins;
}
//一般单例对象不需要考虑释放
//单例对象不用时,必须手动处理,一些资源需要保存
static void DelInstance()
{
//保存数据到文件
// ...
std::lock_guard<mutex> lock(_smtx);
if (_psins)
{
delete _psins;
_psins = nullptr;
}
}
//忘记调用DelInstance(),自动回收
class GC
{
public:
~GC()
{
if (_psins)
{
cout << " ~GC()" << endl;
DelInstance();
}
}
};
void Insert(string name, int salary)
{
_info[name] = salary;
}
void Print()
{
for (auto kv : _info)
{
cout << kv.first << ":" << kv.second << endl;
}
}
private:
InfoSingleton()
{}
InfoSingleton(const InfoSingleton&) = delete;
InfoSingleton& operator=(const InfoSingleton& info) = delete;
map<string, int> _info;
// ...
private:
static InfoSingleton* _psins;
static mutex _smtx;
static GC _gc;
};
InfoSingleton* InfoSingleton::_psins = nullptr;
mutex InfoSingleton::_smtx;
InfoSingleton::GC InfoSingleton::_gc;
int main()
{
InfoSingleton::GetInstance().Insert("张三", 10000);
InfoSingleton& infosl = InfoSingleton::GetInstance();
infosl.Insert("李四", 12000);
infosl.Insert("王五", 15000);
infosl.Insert("赵六", 11000);
infosl.Print();
cout << endl;
InfoSingleton::GetInstance().Insert("张三", 18000);
infosl.Insert("李四", 12000);
infosl.Insert("王五", 15000);
infosl.Insert("赵六", 11000);
infosl.Print();
return 0;
}
因此,可以主动回收,也可以在程序结束时自动回收,但单例对象一般不需要回收。
实现懒汉的另一种方式:
懒汉模式的实现还有另一种方式,直接static,也是只创建一次对象,所以下面的方式也可以,但不是一种通用的方式。
代码语言:javascript复制//是懒汉:因为静态的局部变量是在main函数之后才创建初始化的:局部静态变量的初始化只初始化一次。
//C 11之前,不能保证sinst的初始化是线程安全的。
//C 11之后,可以。
class InfoSingleton
{
public:
//多个对象一起调用GetInstance,存在线程安全的风险,可能new出来多个对象,因此需要加锁
static InfoSingleton& GetInstance()
{
static InfoSingleton sinst;
return sinst;
}
void Insert(string name, int salary)
{
_info[name] = salary;
}
void Print()
{
for (auto kv : _info)
{
cout << kv.first << ":" << kv.second << endl;
}
}
private:
InfoSingleton()
{
cout << "InfoSingleton()" << endl;
}
InfoSingleton(const InfoSingleton&) = delete;
InfoSingleton& operator=(const InfoSingleton& info) = delete;
map<string, int> _info;
// ...
private:
};
int main()
{
InfoSingleton::GetInstance().Insert("张三", 10000);
InfoSingleton& infosl = InfoSingleton::GetInstance();
infosl.Insert("李四", 12000);
infosl.Insert("王五", 15000);
infosl.Insert("赵六", 11000);
infosl.Print();
cout << endl;
InfoSingleton::GetInstance().Insert("张三", 18000);
infosl.Insert("李四", 12000);
infosl.Insert("王五", 15000);
infosl.Insert("赵六", 11000);
infosl.Print();
return 0;
}