本篇博客介绍:介绍几种特殊的类
特殊类的设计
- 设计一个类不能被拷贝
- 设计一个类 只能在堆上创建对象
- 设计一个类 只能在栈上创造对象
- 设计一个类不能被继承
- 单例模式
- 饿汉模式
- 懒汉模式
- 单例模式对象的释放问题
- 总结
设计一个类不能被拷贝
我们的拷贝只会发生在两个场景当中
- 拷贝构造函数
- 赋值运算符重载
所以说我们只需要让类失去 或者说不能使用这两个函数即可
这里有两个解决方案
在C++98中
我们将拷贝构造函数只声明不定义 并且将其访问权限设置为私有即可
class CopyBan
{
// ...
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
//...
};
原因如下
- 我们设置为私有化之后 就能够禁止用户在外面调用 如果不设置私有 用户可以在外面定义并且调用
- 因为我们不需要这个函数 所以定义没有意义 并且如果定义的话就可以在类内部调用从而以另一个函数完成拷贝
在C++11中
在C++11中 如果我们需要禁用一个函数直接使用delete关键字即可
代码标识如下
class CopyBan
{
// ...
CopyBan(const CopyBan&)=delete;
CopyBan& operator=(const CopyBan&)=delete;
//...
};
设计一个类 只能在堆上创建对象
这个题目很有意思 让我们只能在堆上创建对象 也就是说我们不能在栈区还有静态区创建对象
再通俗一点说 我们不能创建局部变量和全局变量
用代码标识就是 下面的定义方式不允许
HeapOnly ho;
static HeapOnlu ho;
我们都知道 创建对象是需要构造函数的 所以说我们只需要将构造函数私有化之后 上面的创建方式就不允许了
但是实际上构造函数私有化之后我们也不能定义堆上的对象了
此时我们就需要创建一个新的函数 让这个函数帮助我们创建一个对象来
设计代码如下
static HeapOnly* CreateObject()
{
return new HeapOnly;
}
我们使用一个静态函数new出来一个新的对象 然后返回这个对象的指针 当然我们也只有这种方式可以获取新的对象 也就是说我们只能在堆上创建对象了
与此同时我们要禁用拷贝构造和赋值运算符重载
因为可能会有人写出这样子的代码导致我们的对象创建在栈上
HeapOnly* HO2 = HeapOnly(HO1);
禁用拷贝构造和赋值运算符重载的思路同第一
设计一个类 只能在栈上创造对象
设计思路和只能在堆上创建对象类似 我们都是私有化构造函数之后暴露出一个静态函数来让外部调用
这个静态函数的唯一功能就是在栈上创建一个对象并且返回这个对象
class StackOnly
{
public:
static StackOnly Createobj()
{
return StackOnly();
}
private:
//将构造函数设置为私有
StackOnly()
{}
};
这里有一点需要特别注意 因为我们是在函数的内部创建了一个对象 所以说这个对象是一个局部变量 所以说我们必须要使用传值返回而不能使用传引用返回
而传值返回不可避免的一点就是 我们需要使用拷贝构造函数
但是呢 有了拷贝构造函数之后我们就没办法限制在堆和静态区创建对象了 因为用户可以通过拷贝构造的方式来实现这一点
所以说我们一定没办法禁止全局对象的创建 即在静态区中创建对象
但是我们还是有办法禁止对象在堆上创建
因为new和delete的底层使用的是operator new和operator delete所以说我们只需要在类中禁用这两个成员函数即可
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
设计一个类不能被继承
在C++98中
在C++98中 因为子类必须要调用父类的构造函数 所以说我们只需要将父类的构造函数私有化
此时父类就成为了事实上不能被继承的类了 代码标识如下
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
//构造函数私有
NonInherit()
{}
};
在C++11中
在C++11中 提供了一个关键字给我们使用 final
- final关键字 如果我们在一个类的后面加上final 那么该类就是最终类 不能被继承
class A final
{
//...
};
单例模式
设计模式概念:
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:
为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式:
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现方式 分别是 饿汉模式
和 懒汉模式
饿汉模式
饿汉模式我们可以理解为这个人十分的饥饿 所以说食物必须要提前准备好
反应到代码中 就是这个单例要在main函数启动前就要准备好
设计思路如下
- 我们让这个类中有一个静态的类对象 只声明不定义
- 将构造函数私有化 私有化之后外部就不能通过构造函数来创建对象
- 删除拷贝构造和赋值运算符重载函数
- 在类外面定义这个静态对象
- 暴露出一个静态函数 返回这个静态对象的指针
至此外面的饿汉模式就设计完毕了
//单例模式:全局只有唯一对象
//饿汉模式:一开始就创建对象(main函数之前)
class InfoSingleton
{
public:
static InfoSingleton& GetInstance()
{
return _sins;
}
private:
InfoSingleton()
{}
InfoSingleton(InfoSingleton& info) = delete;
InfoSingleton& operator=(const InfoSingleton& info) = delete;
private:
static InfoSingleton _sins;//声明
};
InfoSingleton InfoSingleton::_sins;//定义,属于类域::,可以调用构造函数
饿汉模式的优缺点
优点:
- 实现方式简单
- 没有线程安全相关问题
缺点:
- 因为在main函数之前就要定义 可能会导致启动慢
- 无法控制单例初始化顺序
懒汉模式
懒汉模式我们可以理解为这个人十分的懒 所以说不到最后一刻要交任务的时候绝对不会做事的
反应到代码中 就是我们需要使用这个单例的时候这个单例才会创建 也就是说在main函数里面创建
设计思路如下
- 我们让这个类中有一个静态的类对象 只声明不定义
- 将构造函数私有化 私有化之后外部就不能通过构造函数来创建对象
- 删除拷贝构造和赋值运算符重载函数
- 在类外面定义这个静态对象指针设置为空
- 暴露出一个静态函数 如果说指针为空则我们构造并返回一个新的对象
至此我们的懒汉模式就设计完毕了
//懒汉模式
class InfoSingleton
{
public:
static InfoSingleton& GetInstance()
{
//第一次获取单例对象的时候创建对象
if (_psins == nullptr)
{
//第一次获取单例对象的时候创建对象
_psins = new InfoSingleton;
}
return *_psins;
}
private:
InfoSingleton()
{}
InfoSingleton(const InfoSingleton& info) = delete;
InfoSingleton& operator=(const InfoSingleton& info) = delete;
private:
static InfoSingleton* _psins;
};
InfoSingleton* InfoSingleton::_psins=nullptr;
懒汉模式的优缺点
优点:
- 可以主动控制定义顺序
- 在main函数后面启动 不影响启动时间
缺点:
- 有很严重的线程安全问题
如果有两个线程同时进入了GetInstance()函数内部就有可能发生线程安全问题 而导致产生两个或多个对象出来
我们这里推荐一种双检查加锁模式
代码标识如下
static InfoSingleton& GetInstance()
{
//第一次获取单例对象的时候创建对象
if (_psins == nullptr)//对象new出来以后,避免每次都加锁的检查,提高性能
{
_smtx.lock();
if (_psins == nullptr)//保证线程安全的检查且只new一次
{
_psins = new InfoSingleton;
}
_smtx.unlock();
}
return *_psins;
}
首先我们检查下 对象指针是否为空 如果为空我们加锁 (主要是为了避免无脑先加锁带来的效率损失)
其次我们加锁 并且再次检查对象指针是否为空 (主要是为了线程安全问题)
如果为空我们创建对象之后解锁 如果不为空我们直接解锁
单例模式对象的释放问题
我们的单例模式对象创建之后一般会运行到程序结束 所以说一般不存在释放问题
如果说就非要释放的话我们可以创造一个函数来释放我们的对象
static void DelInstance()
{
_mtx.lock();
if (_inst != nullptr)
{
delete _inst;
_inst = nullptr;
}
_mtx.unlock();
}
此外如果我们担心内存泄漏问题的话也可以使用智能指针来管理该对象