前言
本文将会向你介绍如何设计不能被拷贝、不能被继承、只能在栈上、只能在堆上、只能创建一个对象的类
特殊类设计
设计一个不能被拷贝的类
//禁掉拷贝构造函数和赋值运算符重载
class CpBan
{
CpBan(const CpBan&) = delete;
CpBan& operator=(const CpBan&) = delete;
};
设计一个不能被继承的类
法一
//C++11引入的final,防止类进一步被继承或虚函数被重写
class A final
{
//...
};
法二
//子类的构造会调用基类的构造,私有化基类的构造
class NonInherit
{
private:
NonInherit()
{}
};
设计一个只能在堆上创建对象的类
法一
//1、将析构函数私有化
class HeapOnly
{
public:
void destory()
{
delete this;
}
private:
~HeapOnly()
{
cout << "~HeapOnly()" << endl;
}
};
法二
//2、将构造函数私有化
class HeapOnly
{
public:
//提供一个获取对象的接口,必须设置为静态成员函数
static HeapOnly* CreateObject()
{
return new HeapOnly();
}
private:
//构造函数私有化,防止外部直接调用构造函数在栈上创建对象
HeapOnly()
{}
//防止外部调用拷贝构造函数在栈上创建对象
HeapOnly(const HeapOnly& hp) = delete;
HeapOnly& operator=(const HeapOnly& hp) = delete;
};
注意:CreateObject函数必须设置为静态成员函数,因为外部调用该接口就是为了获取对象,但是非静态成员函数必须使用对象调用,这样就是一个鸡生蛋还是蛋生鸡的问题了,因此需要加上static,这样就能通过类域调用
设计一个只能在栈上创建对象的类
class StackOnly
{
public:
//在栈上创建一个对象并返回
static StackOnly CreateObj()
{
StackOnly st;
return st;
}
private:
//将构造函数设置为私有,防止外部直接调用构造函数在堆上创建对象
StackOnly()
{}
void* operator new(size_t size) = delete;
};
这里把operator new禁掉的原因是虽然把构造函数私有化了,但是保不准外部使用拷贝构造来创建一个在栈上的对象,这里是不能禁拷贝构造的,因为CreateObj函数当中创建的是局部对象,返回局部对象的过程(传值返回)中势必需要调用拷贝构造函数的
new在堆上申请空间实际分为两步:第一步先调用operator new函数申请空间,第二步再在申请的空间上执行构造函数,完成对象的初始化工作 delete释放空间也分为两步,第一步在该空间上执行析构函数,完成对对象中资源的清晰工作,第二部调用operator delete函数释放对象的空间 但是还是有一个缺陷,就是不能防止外部在静态区创建对象
Static StackOnly obj;
单例模式
单例模式是一种设计模式(Design Pattern),设计模式就是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式的目的就是为了可重用代码、让代码更容易被他人理解、保证代码可靠性程序的重用性。
单例模式指的就是一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象同一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
饿汉模式
namespace hungry
{
class Singleton
{
public:
//提供一个获取单例对象的全局唯一接口
static Singleton* GetInstance()
{
return _sin;
}
private:
//禁拷贝与赋值
Singleton(const Singleton& sin) = delete;
Singleton& operator=(const Singleton& sin) = delete;
//构造函数私有化
Singleton()
{}
static Singleton* _sin; //声名
};
//在进入程序入口前完成单例对象的初始化
Singleton* Singleton::_sin = new Singleton; //定义
}
优点:设计简单
缺点:可能会导致进程启动慢,如果两个单例有先后顺序,那么饿汉模式无法控制,受类域限制的静态成员对象在main函数之前到底谁先初始化,跟包含关系、编译器编译顺序都有关系(可能存在的场景:有A单例,B单例,有可能B单例中调用A单例,现实生活中有这样的情景业务,这个单例需要去到另一个单例中读取地址IP )
如果程序比较大,用了100个单例,如果是一个客户端软件,会迟迟看不到主界面。
不需要加锁,饿汉模式在程序运行主函数之前就完成了单例对象的创建,此时多线程还没有运行起来,不存在线程安全问题,创建好单例对象后,所有线程要通过GetInstance函数来访问这个单例对象,这个获取的过程也是不需要枷锁的,因为这是一个读操作,如果线程通过GetInstance获取到单例对象后,要用该单例对象做一些线程不安全的操作,此时就需要加锁了线程安全相关问题:
懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件,初始化网络连接,读取文件,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常地缓慢,所以这种情况使用懒汉模式(延时加载)更好
namespace lazy
{
class Singleton
{
public:
//提供一个获取单例对象的全局唯一接口
static Singleton& GetInstance()
{
if (_psin == nullptr)//双检查加锁,只有第一次进来时需要加锁,其余情况不需要
{
imtx.lock();
if (_psin == nullptr)
{
//第一次调用GetInstance时才创建单例对象
_psin = new Singleton;
}
imtx.unlock();
}
return *_psin;
}
~Singleton()
{
//...可做持久化(如写入...到文件中保存)等操作
cout << "~Singleton()" << endl;
}
static void DelInstance()
{
imtx.lock();
if (_psin)
{
delete _psin;
_psin = nullptr;
}
imtx.unlock();
}
class GC
{
public:
~GC()
{
lazy::Singleton::DelInstance();
}
};
private:
//构造函数私有化
Singleton()
{}
//禁拷贝与赋值
Singleton(const Singleton& sin) = delete;
Singleton& operator=(const Singleton& sin) = delete;
static Singleton* _psin; //声名
static GC _gc;
};
Singleton* Singleton::_psin = nullptr; //定义
Singleton::GC Singleton::_gc;
}
一般情况下单例模式创建的对象是不用释放的,因为程序正常退出就会释放,但仍存在一些特殊场景需要我们去释放 特殊场景: 1、中途需要显示释放 2、程序结束时,需要做一些特殊动作(如持久化---写入文件中) 如果需要释放的单例对象比较多,那么手动释放会比较麻烦 这里加了一个内部类gc,并且创建对象_gc,当_gc对象出了作用域就会调用gc类的析构函数从而调用Singleton的析构函数,这样就可以达到自动释放的效果懒汉模式下的单例对象的释放
由于懒汉模式是线程调用GetInstance才会去创建单例对象,这个操作是对static指针写操作,这个过程不是线程安全的,如果不加锁,多个线程可能同时调用GetInstance函数,多个线程各自创建出一个对象 这里采用双检查加锁,因为当第一个线程调用GetInstance函数创建好单例对象后,后续的线程直接使用即可,我们需要保证第一次创建单例对象的过程只有一个线程,因此在if语句上下加互斥锁,如果仅是这样的话还不够,对于这种只有第一次加锁的场景可以用双检查加锁,在当前加锁解锁的外面进行一次if判断,这样就不用大量的无意义的加锁解锁操作,导致线程不断切入切出,影响程序运行效率线程安全相关问题
小结
今日的分享就到这里了,如果本文存在疏漏或错误的地方还请您能够指出