1、构造函数私有,防止直接调用构造函数在栈上创建对象
2、提供一个获取对象的static接口,用于获取对象
3、将拷贝函数设置成私有,并且只声明不是先,防止调用拷贝函数在栈上创建对象
/* 只能在堆上创建对象 */
class HeapOnly {
static HeapOnly* CreateObj() {
return new HeapOnly();
}
private:
HeapOnly() {};
// C++98
HeapOnly(const HeapOnly&);
// C++11
// HeapOnly(const HeapOnly&) = delete;
};
方法1
1、将构造函数设置成私有,防止外部直接调用构造函数在堆上创建对象
2、向外部提供一个获取对象的static接口,该接口在栈上创建一个对象并且返回
/* 只能在栈上创建对象 */
class StackOnly {
public:
static StackOnly CreateObj() {
return StackOnly();
}
private:
StackOnly() {};
};
该方法无法防止外部调用拷贝构造函数创建对象
StackOnly obj1 = StackOnly::CreateObj();
static StackOnly obj2(obj1); // 在静态区拷贝构造对象
StackOnly* ptr = new StackOnly(obj1); // 在堆上拷贝构造对象
但是我们不能把拷贝构造函数删除,因为CreateObj
函数中创建的对象是局部对象,返回局部对象的过程中势必要调用拷贝构造函数
方法2
屏蔽operator new 和 operator delete函数
class StackOnly2 {
public:
StackOnly2() {};
private:
// C++ 98
void* operator new(size_t size);
void operator delete(void* p);
// C++ 11
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
};
new和delete默认调用的是全局的operator new函数和operator delete 函数,如果一个类重载了专属的operator new函数和operator delete 函数,那么new 和delete就会调用这个专属的函数。所以只要把operator new函数和operator delete函数屏蔽掉,那就无法再使用new在堆上创建对象了
但该方法也有缺陷,其无法防止外部在静态区创建对象
static StackOnly obj;
将方法1和方法二结合一下就可以防止在静态区创建对象,但是无法防止在静态区拷贝构造对象
class CopyBan {
public:
CopyBan(){};
private:
// C++98
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
// C++11
CopyBan(const CopyBan&); = delete;
CopyBan & operator=(const CopyBan&); = delete;
};
方法1: C++98
该类的构造函数设置成私有即可,因为子类的构造函数被调用时,必须调用父类的构造函数初始化父类的那一部分成员,父类的私有成员子类是不可见的,所以子类创建时无法调用父类构造函数初始化父类成员,因此子类无法创建对象
class NonInherit {
public:
static NonInherit CreateObj() {
return NonInherit();
}
private:
NonInherit() {};
};
方法2: C++11
C++98的方法并不够彻底,因为这个类任然可以被继承(编译器不会报错)只是不能实例化对象而已,于是C++11提供了final关键字,final修饰的类叫做最终类,最终类无法被继承,就算继承后没有创建对象也会报错
class NonInherit final
{
//...
}
单例模式是一种设计模式(Design Pattern), 设计模式就是一套被反复使用、多数人知晓代码设计经验的总结,使用设计模式的目的就是为了可重用代码,让代码能被他人理解,保证代码的可靠性和程序的重要性
单例某事指一个类只能创建一个对象,该某事可以保证一个进程该类只有一个实例,并提供一个可以全局访问点,该实例被所有程序模块共享
在一个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其它对象再通过这个单例对象获取这些配置信息,简化了复杂环境下的配置管理
class Singleton {
public:
static Singleton* GetInstance() {
return _inst;
}
private:
Singleton() {};
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* _inst;
};
Singleton* Singleton::_inst = new Singleton;
饿汉模式的单例非常简单,由于类的成员变量_inst
是静态成员变量,再程序运行主函数之前就完成了创建,由于main函数之前是不存在多线程的,并且main函数创建后调用GetInstance
函数是一个只读操作,不需要加锁。
当然如果线程获取到单例对象后,要用这个单例对象进行一些线程不安全操作,那就需要加锁了
class Singleton {
public:
static Singleton* GetInstance();
private:
Singleton() {};
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* _inst;
static std::mutex _mtx;
};
Singleton* Singleton::_inst = nullptr;
Singleton* Singleton::GetInstance() {
if (_inst == nullptr) {
std::unique_lock < std::mutex > u1(_mtx);
if (_inst == nullptr) {
_inst = new Singleton;
}
}
return _inst;
}
懒汉模式在程序运行之前没有单例对象创建,而是在等第一个线程需要使用这个单例才进行创建。所以获取单例对象时需要确认单例是否为空**,这里使用了双重判定的设计,可以防止每次都要加锁解锁判断单例是否为空,规避大量无意义的加锁解锁操作**
饿汉模式
饿汉模式的优势就是非常简单,但是缺点也非常明显,饿汉模式在程序主程序运行前就将单例对象创建好,若单例对象非常大,那么单例类的构造函数就需要进行大量工作,导致程序启动非常慢。
此外,如果有多个单例类需要创建单例对象,并且它们之间的初始化存在某种依赖关系,比如单例A对象必须创建在单例B对象之后,此时饿汉模式可能就会存在问题,因为我们无法保证多个单例对象哪个对象先创建
懒汉模式
而懒汉模式可以很好解决上述问题,因为懒汉模式的对象是由用户第一次调用GetInstance
来创建的,其顺序可控,并且我们可以在程序开始运行后单独拿出一个线程来跑单例模式的创建,让程序快速启动。
懒汉模式的缺点就是,在编码上比懒汉模式复杂,在创建单例对象时需要考虑线程安全的问题
class Singleton {
public:
static Singleton* GetInstance();
~Singleton() {
std::unique_lock<std::mutex> ul(_mtx);
if (_inst) delete _inst;
}
private:
Singleton() {};
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* _inst;
static std::mutex _mtx;
};
Singleton* Singleton::_inst = nullptr;
~Singleton() {
std::unique_lock ul(_mtx);
if (_inst) delete _inst;
}
int main() {
Singleton* sl = Singleton::GetInstance();
sl->~Singleton();
}
如果我们在析构函数中显示调用delete析构对象,就会出现以上这种情况,我们显示析构对象,对象的析构函数中调用了delete,delete又会去调用Singleton的析构函数,析构函数中又有delete就陷入了无限递归调用析构函数。
解决方案
单例对象在创建后一般在整个程序运行期间都需要使用,所以可以不考虑单例对象的释放,程序正常结束后会自动将资源还给操作系统,如果要考虑单例对象的释放可以考虑一下两种方法
在单例类中编写一个
DelInstance
函数,可以在该函数中进行单例对象的释放操作,在不需要单例对象时主动调用
void Singleton::DelInstance() {
std::unique_lock<std::mutex> ul(_mtx);
if (_inst != nullptr) {
delete _inst;
_inst = nullptr;
}
}
在单例类中内嵌一个垃圾回收类,在垃圾回收类的析构函数中完成单例对象的释放,在单例类中定义一个静态的垃圾回收对象,当独享被消耗时就会调用析构函数完成对单例对象的释放
class Singleton {
public:
static Singleton* GetInstance();
static void DelInstance();
private:
Singleton() {};
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
class CGarbo {
public:
~CGarbo() {
if (_inst != nullptr) {
delete _inst;
_inst = nullptr;
}
}
};
static Singleton* _inst;
static CGarbo cg;
static std::mutex _mtx;
};
Singleton* Singleton::_inst = nullptr;
Singleton::CGarbo Singleton::cg;
Singleton* Singleton::GetInstance() {
if (_inst == nullptr) {
std::unique_lock < std::mutex > u1(_mtx);
if (_inst == nullptr) {
_inst = new Singleton;
}
}
return _inst;
}
懒汉模式还有一种比较经典的实现方式
1、将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置成私有或者删除,防止外部创建或者拷贝i对象
2、提供一个全局访问点获取单例对象
class Singleton {
public:
static Singleton* GetInstance() {
static Singleton inst;
return &inst;
}
Singleton() {};
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
由于实际上只有第一次调用GetInstance
函数时才会定义这个静态的单例对象,也就保证了全局只有这唯一实例。并且这里的单例对象的定义过程是线程安全的,因为C++11标准保证多线程初始化static变量不会发生数据竞争,可以视为原子操作
但是这种版本的懒汉模式有两种缺点:
单例对象定义在静态区,如果单例对象太大的话不推荐这种方式
单例对象在静态区创建后就不能主动释放