一、不能被拷贝的类
拷贝一般只有两个场景,一个是拷贝构造,另一个是赋值运算符重载,所以我们需要想办法将这两个函数禁止掉
C++98
- 将拷贝构造和赋值运算符重载仅申明不定义,并定义为私有
- 私有的原因是防止别人在类外重新定义使用
- 不定义的原因是这两个函数根本就不应该被调用,定义了也没什么作用,并且如果定义了就不能防止在类内存在拷贝
class CopyBan
{
public:
CopyBan(int a)
:_a(a)
{}
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
private:
int _a;
};
C++11
- C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上 =delete,表示让编译器删除掉该默认成员函数
class CopyBan
{
CopyBan(const CopyBan&)=delete;
CopyBan& operator=(const CopyBan&)=delete;
};
二、只能在堆上创建对象的类
将构造函数私有化,防止别人在栈上创建对象,并提供一个静态成员函数用于创建对象(用静态是因为可以直接用类域调用函数)
class HeapOnly
{
public:
static HeapOnly* CreateObject()
{
return new HeapOnly;
}
private:
HeapOnly() {}
};
但是上述代码还存在一定的问题,由于我们没有管控拷贝构造函数,如果我们先在堆上创建了一个对象,我们依旧可以调用拷贝构造在栈上创建对象。
int main()
{
//堆上创建
HeapOnly* pho1 = HeapOnly::CreateObject();
//栈上创建
HeapOnly ho2(*pho1);
}
所以我们需要将拷贝构造delete掉或者仅声明不定义
class HeapOnly
{
public:
static HeapOnly* CreateObject()
{
return new HeapOnly;
}
private:
HeapOnly() {}
// C++98
HeapOnly(const HeapOnly&);
// 或者
// C++11
HeapOnly(const HeapOnly&) = delete;
};
三、只能创建在栈上创建对象的类
与只能在堆上创建对象相似,将构造函数私有化,并提供一个静态成员函数用于在栈上创建对象
class StackOnly
{
public:
static StackOnly CreateObj()
{
return StackOnly();
}
private:
StackOnly()
:_a(0)
{}
private:
int _a;
};
这个代码依旧是存在一点小问题的,我们可以利用拷贝构造在堆上创建对象,因为new的底层是调用operator new和拷贝构造函数,虽然我们将构造函数私有化了,但是拷贝构造还是放出来了
int main()
{
StackOnly obj = StackOnly::CreateObj();
StackOnly* ptr3 = new StackOnly(obj);
}
但是我们不能将拷贝构造也delete掉或者仅声明不定义,因为我们写的静态成员函数是传值返回的需要用到拷贝构造,所以我们可以重写operator new,将它设置为仅声明不定义或者delete
class StackOnly
{
public:
static StackOnly CreateObj()
{
return StackOnly();
}
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
private:
StackOnly()
:_a(0)
{}
private:
int _a;
};
四、不能被继承的类
C++98
可以将基类的构造函数私有化,这样派生类就无法调用基类的构造函数
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
C++11
添加final关键字
class A final
{
// ....
};
五、单例模式
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个 访问它的全局访问点,该实例被所有程序模块共享。
饿汉模式
饿汉模式就是尽早的先创建出唯一的这个对象,使用时就可以直接使用
//饿汉模式
class Singleton
{
public:
static Singleton* Getinstance()
{
return &_sgl;
}
private:
//拷贝构造私有化
Singleton(int a,int b,const string& str)
:_a(a),_b(b),_str(str)
{}
//防止拷贝
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
private:
int _a;
int _b;
string _str;
//声明
static Singleton _sgl;
};
//定义:全局对象并不是保存在对象中的而是保存在静态区的,只是受类域限制,在程序启动时就创建好了
Singleton Singleton::_sgl(1, 2, "hello");
饿汉模式存在一些问题:
- 饿汉模式的单例资源在进入main函数之前就创建好了,如果一个程序要加载很多的单例资源并且如果这些单例资源比较大的话,就会影响程序的启动时间
- 如果单例资源存在依赖关系,饿汉模式无法控制。比如A类和B类是单例,A单例要连接数据库,B单例要用A单例访问数据库,类一旦加载,就会立即初始化其实例,这导致程序员无法直接控制这些实例的创建顺序和依赖关系。
- 如果一个单例对象在多线程高并发环境下频繁使用,性能要求较高,使用饿汉模式来避免资源竞争,提高响应速度更好,因为饿汉模式在程序加载时单例对象就创建好了,此时不存在多线程,避免了加锁等竞争资源
懒汉模式
懒汉模式就是要使用对象时在创建
//懒汉模式
class Singleton
{
public:
static Singleton* Getinstance()
{
if (_sgl == nullptr)
{
return new Singleton(1, 2,"hello");
}
return _sgl;
}
private:
//拷贝构造私有化
Singleton(int a, int b, const string& str)
:_a(a), _b(b), _str(str)
{}
//防止拷贝
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
private:
int _a;
int _b;
string _str;
//声明
static Singleton* _sgl;
};
Singleton* Singleton::_sgl = nullptr;
懒汉模式完美解决了饿汉模式存在的两个问题,但是上述写法还存在一点问题,在多线程下是不安全的,比如一个线程已经判断_sgl为空,准备return了,此时轮转到另一个线程了,它依旧可以进入if语句中,创建好一个对象并返回,假如后续又轮转到第一个线程了,他又会创建一个对象,那么此时一共创建了两个不同的对象,并且上一个对象的资源造成了内存泄漏,是很严重的问题,我们可以加锁来解决
//懒汉模式
class Singleton
{
public:
static Singleton* Getinstance()
{
unique_lock<mutex> lock(_mtx);
if (_sgl == nullptr)
{
return new Singleton(1, 2,"hello");
}
return _sgl;
}
private:
//拷贝构造私有化
Singleton(int a, int b, const string& str)
:_a(a), _b(b), _str(str)
{}
//防止拷贝
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
private:
int _a;
int _b;
string _str;
//声明
static Singleton* _sgl;
static mutex _mtx;
};
Singleton* Singleton::_sgl = nullptr;
mutex Singleton::_mtx;
上述代码解决了线程安全的问题,但是我们还可以优化一下,观察一下代码,我们加锁是希望创建对象的时候保证线程安全,如果对象创建出来了直接返回就可以了,但是上述的写法不论对象是否创建出来都需要去竞争锁,所以我们可以采用二次判断的方法
六、饿汉模式与懒汉模式对比
- 饿汉模式相对于懒汉模式来说更加简单,不需要考虑线程安全的问题
- 饿汉模式存在两个问题,单例资源比较多或者比较大的时候不建议使用饿汉模式
- 懒汉模式完美解决了饿汉模式的这两个问题,但是需要考虑线程安全和效率的问题