目录标题
- 设计一个不能被拷贝的类
- 设计一个只能从堆上创建对象的类
- 设计一个只能在栈上创建对象的类
- 设计一个无法被继承的类
- 什么是单例模式
- 饿汉模式
- 饿汉模式的缺点
- 懒汉模式
- 懒汉模式的优点
- 懒汉模式的缺点
- 特殊的懒汉
设计一个不能被拷贝的类
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。那么c++98采用的方式就是将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可,比如说下面的代码:
class CopyBan
{
// ...
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
//...
};
设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就不能禁止拷贝了,只声明不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数,那么这里的代码就如下:
class CopyBan
{
// ...
CopyBan(const CopyBan&)=delete;
CopyBan& operator=(const CopyBan&)=delete;
//...
};
设计一个只能从堆上创建对象的类
我们可以在三个位置上创建类的对象,分别为堆上栈上和静态区上,创建的方式如下:
int main()
{
HeapOnly tmp1;//栈上
HeapOnly* tmp2 = new HeapOnly;//堆上
static HeapOnly tmp3;//静态区上
}
创建对象的时候必须得调用构造函数,那么这里就可以将构造函数放到私有里面,然后创建一个函数通过new来着堆上创建对象,但是这里会有一个先有鸡还是先有蛋的问题,所以我们把这个创建对象的函数变为静态的,比如说下面的代码:
class HeapOnly
{
public:
static HeapOnly* CreateObject()
{
return new HeapOnly;
}
private:
HeapOnly() {}
};
这样写就可以让该容器智能在堆上创建空间,那么上面的代码运行的结果就如下:
可以看到上面的代码就出问题原因时无法调用构造函数,那么这里要想创建对象就智能调用里面的函数,比如说下面的代码
int main()
{
HeapOnly* tmp = HeapOnly::CreateObject();
}
虽然上面的写法可以有效的避免在栈上和静态区上创建对象,但是它依然可以通过拷贝构造在栈上创建对象,所以还得把拷贝构造禁止掉,比如说下面的代码:
class HeapOnly
{
public:
static HeapOnly* CreateObject()
{
return new HeapOnly;
}
private:
HeapOnly() {}
HeapOnly(const HeapOnly&) = delete;
};
这里还有个方法就是将析构函数私有化,因为栈上的变量会自动销毁并调用析构函数,所以当对象的生命周期结束之后就会自动地调用析构函数来释放空间,但是析构函数私有化了所以掉不动,也就进一步阻止了对象在栈上船舰,但是堆上创建的对象也是掉不了析构函数,所以这里可以创建一个函数给堆上的变量能够调用析构函数来释放空间,那么这里地代码就如下:
#include<iostream>
using namespace std;
class HeapOnly
{
public:
HeapOnly()
{}
void Destory()
{
this->~HeapOnly();
}
private:
~HeapOnly()
{}
HeapOnly(const HeapOnly& tmp) = delete;
};
int main()
{
HeapOnly* tmp1 = new HeapOnly;
tmp1->Destory();
return 0;
}
运行地结果也是没有错误的
设计一个只能在栈上创建对象的类
这里也是跟上面的思路差不多,创建一个函数,函数中栈上创建对象并返回该对象,比如说下面的代码:
class StackOnly
{
public:
static StackOnly creatobj()
{
return StackOnly();
}
private:
StackOnly()
{}
};
那么我们就可以用下面的代码在栈上创建对象:
{
StackOnly tmp1 = StackOnly::creatobj();
return 0;
}
如果在其他位置上创建空间的话就会直接报错,比如说下面的代码:
int main()
{
StackOnly* tmp1 = new StackOnly;
static StackOnly tmp2;
return 0;
}
报错的内容如下:
但是这种方法没有完全的封死,我们依然可以通过拷贝构造在静态区上开辟空间,比如说下面的代码:
int main()
{
static StackOnly tmp1 = StackOnly::creatobj();
return 0;
}
那能不能把拷贝构造函数也封掉的呢?答案使不行的,因为拷贝构造函数被封掉之后不仅静态区上无法创建空间,而且栈上也没有办法创建对象,这里还有种方法就是封掉operator new和operator delete,比如说下面的代码:
class StackOnly
{
public:
StackOnly()
{}
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
private:
};
那么这个时候再使用new创建对象就会直接报错,比如说下面的运行结果:
但是这种方法只能保证不能在堆上开辟空间,无法保证在静态区上也不能开辟空间,比如说下面的代码还是可以正常运行的:
int main()
{
static StackOnly tmp2;
return 0;
}
所以痛过常规方法这里没有办法完全封死的,要想完全封死的话就只有一个办法就是封掉拷贝构造函数并且不接受对象,通过引用或者创建临时匿名对象来直接调用函数,比如说下面的代码:
int main()
{
StackOnly::creatobj().Print();
const StackOnly& so4 = StackOnly::creatobj();
so4.Print();
return 0;
}
代码的运行结果如下:
但是这样的实现存在一个缺陷就是无法修改对象里面的内容。
设计一个无法被继承的类
方法一就是构造函数私有, C++98中构造函数私有化,派生类中调不到基类的构造函数,所以无法继承
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
方法二就是添加final,final修饰类表示该类不能被继承,那么这里的代码就如下:
class NonInherit final
{
// ....
};
什么是单例模式
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理,单例模式有两种实现方式第一种就是饿汉模式,第二种就是懒汉模式,我们首先来看看饿汉模式的原理。
饿汉模式
单例模式的特点就是全局只有一个唯一对象,如何保证全局只有一个唯一对象呢?首先构造函数封死,如果构造函数不封起来的话使用者可以用这个类创建多个对象,那么我们就可以创建一个类,类中含有一个map容器并装着一些数据,然后类里面就提供了一些函数用于访问map的数据,修改map的数据等等,然后把这个类的构造函数放到私有里面,比如说下面的代码:
class InfoSingleton
{
public:
private:
InfoSingleton()
{}
map<string, int> _info;
};
然后这里就存在一个问题如何来创建对象,并且保证对象的个数就只有一个呢?答案是在类里面创建一个静态的变量然后提供一个静态成员函数来获取对象的引用,比如说下面的代码:
class InfoSingleton
{
public:
static InfoSingleton& GetInstance()
{
return _sins;
}
private:
InfoSingleton()
{}
map<string, int> _info;
static InfoSingleton _sins;
};
InfoSingleton InfoSingleton::_sins;
外面的静态成员变量是定义,内部的静态成员变量是声明,然后为了方便往对象里面的插入数据和修改数据,我们还要创建一个insert函数,在里面通过方括号来修改内部的数据,比如说下面的代码:
void insert(string name, int salary)
{
_info[name] = salary;
}
然后我们就可以在main函数里面通过引用或者匿名调用的方式来插入或者修改数据,比如说下面的代码:
int main()
{
InfoSingleton::GetInstance().insert("张三", 10000);
InfoSingleton::GetInstance().insert("李四", 12000);
InfoSingleton& tmp = InfoSingleton::GetInstance();
tmp.insert("王五", 13000);
tmp.insert("老六", 14000);
return 0;
}
然后我们还可以创建一个print函数用来打印内部的数据,比如说下面的代码:
void print()
{
for (auto& ch : _info)
{
cout << ch.first << ":" << ch.second << endl;
}
}
然后我们就可以查看容器里面的数据,那么这里的运行结果如下:
这种方式就是饿汉模式一开始就创建对象,但是这种方法存在一个问题就是拷贝构造和赋值重载会多创建出来一个对象不符合特征,所以这里得把拷贝构造和赋值重载也去掉,那么这里的代码就如下:
class InfoSingleton
{
public:
static InfoSingleton& GetInstance()
{
return _sins;
}
void insert(string name, int salary)
{
_info[name] = salary;
}
void print()
{
for (auto& ch : _info)
{
cout << ch.first << ":" << ch.second << endl;
}
}
private:
InfoSingleton()
{}
InfoSingleton(InfoSingleton const&) = delete;
InfoSingleton& operator=(InfoSingleton const&) = delete;
map<string, int> _info;
static InfoSingleton _sins;
};
InfoSingleton InfoSingleton::_sins;
我们就把这样的实现方式成为饿汉模式,因为它在程序的一开始就创建了一个对象。
饿汉模式的缺点
1.饿汉模式初始化时如果数据太多,会导致启动的速度较慢,因为饿汉模式在main函数之前就得进行初始化,而数据的含量可能会非常的多并且数据可能还要链接数据库等等,所以可能会导致启动的速度很慢。
2.多个单例类有初始化依赖关系,饿汉模式无法控制。比如说A和B都是单利类,要求先初始化A,再初始化B,因为B会依赖A,但是饿汉模式中的变量都是全局变量无法保证初始化顺序,所以这里就可能会出错,那么未来解决这个问题有人就提出了懒汉模式。
懒汉模式
饿汉模式是程序一开始就创建对象而懒汉模式则是先不着急创建对象,等需要的时候再创建对象,那么我们就把类里面的静态对象修改成为一个静态的指针对象,在类外面将其初始化为空,在GetInstance函数里面就判断当前的指针是否为空,如果为空的话就创建对象最后返回当前的指针指向的对象,那么这里的代码就如下:
class InfoSingleton
{
public:
static InfoSingleton& GetInstance()
{
//第一次调用的时候创建对象
if (_psins == nullptr)
{
_psins = new InfoSingleton;
}
return *_psins;
}
void insert(string name, int salary)
{
_info[name] = salary;
}
void print()
{
for (auto& ch : _info)
{
cout << ch.first << ":" << ch.second << endl;
}
}
private:
InfoSingleton()
{}
InfoSingleton(InfoSingleton const&) = delete;
InfoSingleton& operator=(InfoSingleton const&) = delete;
map<string, int> _info;
static InfoSingleton* _psins;
};
InfoSingleton* InfoSingleton::_psins=nullptr;
那么这里就不是一开始就创建对象,而是等你调用的时候创建对象。
懒汉模式的优点
1.对象在main函数之后才会创建,不会影响启动顺序。
2.可以主动控制初始化顺序。比如说A依赖于B,那么我们就可以先调用A再调用B来解决这个问题。
懒汉模式的缺点
多个线程一起调用单例对象的时候创建对象时也可能会创建多个对象,第一个线程看到的当前类没有对象会new一个,第一个线程还没有创建完成第二个线程跑过来发现依然没有所以这个时候就又会创建一个对象出来,所以这个时候就得在类里面添加一个枷锁变量,因为静态的成员函数没有this指针不能访问非静态的成员变量,所以这里就得创建一个静态的枷锁,那么这里的代码如下:
class InfoSingleton
{
public:
static InfoSingleton& GetInstance()
{
mtx.lock();
if (_psins == nullptr)
{
_psins = new InfoSingleton;
}
mtx.unlock();
return *_psins;
}
void insert(string name, int salary)
{}
void print()
{}
private:
InfoSingleton()
{}
InfoSingleton(InfoSingleton const&) = delete;
InfoSingleton& operator=(InfoSingleton const&) = delete;
map<string, int> _info;
static InfoSingleton* _psins;
static mutex mtx;
};
mutex InfoSingleton::mtx;
InfoSingleton* InfoSingleton::_psins=nullptr;
可是这里就存在一个问题,我们只会在第一次创建对象的时候出现问题,而我们每次使用这个函数的时候都得进行枷锁解锁,这里是不是就会产生时间消耗啊,所以能不能把枷锁放到if的里面呢?答案是不行的这里会出现线程安全,因为可能会两个线程都进入到创建对象里面,并且这种方法会加剧线程安全的风险,所以这里就可以添加双层检查来进行枷锁保护,那么这里的代码就如下:
static InfoSingleton& GetInstance()
{
if (_psins == nullptr)//对象new出来了,避免每次都枷锁的检查,提高性能。
{
mtx.lock();
if (_psins == nullptr)//保证线程安全且执行一次
{
_psins = new InfoSingleton;
}
mtx.unlock();
}
return *_psins;
}
但是这里new可能会抛出异常所以这里得捕捉异常,如果没捕捉的话这里就不会解锁了,那么改进后的代码就如下:
static InfoSingleton& GetInstance()
{
if (_psins == nullptr)
{
mtx.lock();
try
{
if (_psins == nullptr)
{
_psins = new InfoSingleton;
}
}
catch (...)
{
mtx.unlock();
throw;
}
}
return *_psins;
}
上面的释放方式有点不好看,所以这里我们可以使用智能指针的思想来解决这里的问题,创建一个类类中含有一个锁的引用对象,那么这个类的构造函数就是对这个锁进行上锁,类的析构函数就是对这个类进行解锁,那么这里的代码就是这样:
template<class Lock>
class LockGuard
{
public:
LockGuard(Lock& lk)
:_lk(lk)
{
_lk.lock();
}
~LockGuard()
{
_lk.unlock();
}
private:
Lock& _lk;
};
那么上面的函数我们就可以写成下面这样:
static InfoSingleton& GetInstance()
{
if (_psins == nullptr)
{
LockGuard<mutex> lock(mtx);
if (_psins == nullptr)
{
_psins = new InfoSingleton;
}
}
return *_psins;
}
一般单例对象不需要考虑内存释放,因为单例对象一般都是在整个程序里面进行使用,但是单例对象在不用时必须得手动处理,让一些资源进行报错,所以这个时候就得提供一个delete函数来手动释放,那么这里的函数代码如下:
static void DelInstance()
{
/*保存数据到文件
...*/
std::lock_guard<mutex> lock(mtx);
if (_psins)
{
delete _psins;
_psins = nullptr;
}
}
那这里能不能做到自动释放该类呢?答案时可以的,我们可以定义一个内部类并且该类的析构函数里面调用DelInstance,然后在外部类里面添加一个该类的静态对象,这样当单例的那个类被销毁时内部类的对象就会被销毁,内部类对象被销毁时就会调用它的析构函数,然后析构函数就调用GetInstance函数来释放,那么完整的代码就如下:
template<class Lock>
class LockGuard
{
public:
LockGuard(Lock& lk)
:_lk(lk)
{
_lk.lock();
}
~LockGuard()
{
_lk.unlock();
}
private:
Lock& _lk;
};
class InfoSingleton
{
public:
static InfoSingleton& GetInstance()
{
if (_psins == nullptr)
{
LockGuard<mutex> lock(mtx);
if (_psins == nullptr)
{
_psins = new InfoSingleton;
}
}
return *_psins;
}
void insert(string name, int salary)
{
_info[name] = salary;
}
void print()
{
for (auto& ch : _info)
{
cout << ch.first << ":" << ch.second << endl;
}
}
static void DelInstance()
{
/*保存数据到文件
...*/
std::lock_guard<mutex> lock(mtx);
if (_psins)
{
delete _psins;
_psins = nullptr;
}
}
class GC
{
public:
~GC()
{
if (_psins)
{
cout << "~GC()" << endl;
DelInstance();
}
}
};
private:
InfoSingleton()
{}
InfoSingleton(InfoSingleton const&) = delete;
InfoSingleton& operator=(InfoSingleton const&) = delete;
map<string, int> _info;
static InfoSingleton* _psins;
static mutex mtx;
static GC _gc;
};
mutex InfoSingleton::mtx;
InfoSingleton* InfoSingleton::_psins=nullptr;
InfoSingleton::GC InfoSingleton::_gc;
特殊的懒汉
class InfoSingleton
{
public:
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;
}
cout << endl;
}
private:
InfoSingleton()
{
cout << "InfoSingleton()" << endl;
}
InfoSingleton(const InfoSingleton& info) = delete;
InfoSingleton& operator=(const InfoSingleton& info) = delete;
map<string, int> _info;
// ...
};
我们之前说过函数中的静态成员变量是在第一次调用该函数的时候进行创建并且初始化,第一次调用完之后就不会再执行该代码,所以上面的代码能够保证只创建一个对象,并且该对象的创建也发生了main函数之后,可是面对多线程的时候上面的代码可能会出现线程安全问题吗?答案是C++11之前,这里是不能保证sinst的初始化是线程安全的,C++11之后可以保证安全。所以这种写法不一定安全并不是通用的方法,所以对于这种写法如果编译器支持c++11则可以写,如果不支持则不能写。