前言
在有些开发场景下需要我们设计出一些特殊的类来满足特殊的需求,本期我们将来介绍一下常见的特殊类的设计!
目录
一、设计一个类,不能被拷贝
拷贝只会在两个场景中:拷贝构造 和 赋值运算符重载,因此想要让一类禁止拷贝,只需要让该类不能调用 拷贝构造 和 运算符重载 即可!
C++98:
将一个类的拷贝构造和赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
class CopyBan
{
public:
CopyBan()
{}
private:
// C++98 将 拷贝构造 和 赋值运算符重载 声明 为私有
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
};
1、为什么要将声明设置为私有?
如果将 拷贝构造 和 赋值拷贝 的声明被设置成私有,用户有可能在类外面自己实现,这样就达不到实现禁止拷贝的目的了!
2、为什么只是声明而不实现?
不实现是因为他两根本是不会被调用的,及时定义了也没啥意义。所以,不实现更简单!
C++11 :
C++11 扩展了 delete 的用法,delete 除了释放 new 申请的资源外,如果在默认成员函数后跟上 =delete 表示让编译器删除该默认成员函数!
class CopyBan
{
public:
CopyBan()
{}
// C++11 将 拷贝构造 和 赋值运算符重载 让编译器删除掉
CopyBan(const CopyBan&) =delete;
CopyBan& operator=(const CopyBan&) = delete;
};
二、设计一个类,只能在堆上创建对象
方式一:
1、将类的构造函数私有化,拷贝够声明为私有/禁用。防止别人调用拷贝构造在栈上生成对象!
2、提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建!
class HeapOnly
{
public:
// 提供一个静态的 创建堆对象的成员函数
static HeapOnly* CreateObject()
{
return new HeapOnly;
}
private:
HeapOnly()
{}
HeapOnly(const HeapOnly&); // C++98 将拷贝构造的声明私有化,防止利用拷贝构造在栈上创建对象
// C++11 禁用掉拷贝构造
// HeapOnly(const HeapOnly&) = delete;
};
方式二:
将析构函数私有化。将析构函数私有化之后,不能直接创建对象了。只能使用 new 来创建对象即在堆上。现在的问题是:new 完之后如何释放呢?因为析构被设置成了私有,所以不能直接调,此时需要提供一个成员函数,在这成员函数内部直接 delete this 即可,谁调用 this 就是谁,更好解决了析构私有化不能调用的问题!
class HeapOnly
{
public:
void Destroy()
{
delete this;
}
private:
~HeapOnly()
{
cout << "~HeapOnly()" << endl;
}
};
首先看一下直接创建对象不成功:
只能使用 new 创建
三、设计一个类,只能在栈上创建对象
和上面的思路类似。将构造函数私有化,然后设计一个静态的方法创建对象返回即可!
class StackOnly
{
public:
static StackOnly CreateObject()
{
return StackOnly();
}
private:
StackOnly(){}
};
由于没有将 new 处理所以有时候下面这种情况还是可以通过的:
int main()
{
StackOnly st1 = StackOnly::CreateObject();
StackOnly* st2 = new StackOnly(st1);// st2 就是堆上的对象
return 0;
}
这里你可能会想:直接把拷贝构造直接禁用掉不就完全解决了嘛?表面上看好像没问题,但实际上是不行的!因为 CreateObject() 函数返回的时临时对象,所以必须是传值返回,所以不能将拷贝构造删除。
这里的解决方法是:在我们自己的类中重载 operator new 然后将他禁用掉就OK了!为什么将我们重载的 禁用掉用好了呢?原因是,我们平时的 new 是全局的,当我们自己类中实现之后默认就是用的是当前类的,这里禁用掉之后就无法 new 了
class StackOnly
{
public:
static StackOnly CreateObject()
{
return StackOnly();
}
// 实现当前类的 专属 new
void* operator new(size_t) = delete;
private:
StackOnly(){}
};
四、设计一个类,不能被继承
C++98:将构造函数私有化,派生类中调用不到基类的构造函数,即无法继承
class NonTherit
{
public:
static NonTherit GetInstance()
{
return NonTherit();
}
private:
NonTherit()
{}
};
C++11 :使用 final 关键字,final 修饰的类,表示该类不能被继承
class NonInherit final
{
public:
NonInherit()
{}
private:
// ...
};
五、设计一个类,只能创建一个对象(单例模式)
设计模式:设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结/套路。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式:一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式很常用,例如我们前面实现的 线程池 就是使用的 单例模式!
单例模式的两种实现:饿汉模式 和 懒汉模式
1、饿汉模式
不管你未来用不用,在程序启动时就创建一个唯一的实例对象。
首先既然是唯一的一个实例,那必然不能拷贝,所以我么必须得把 拷贝构造 和 赋值拷贝 给删除掉。然后将 构造函数 私有化!在向外提供一个 static 的 获取该类对象的方法!
如何保障在进程启动即执行 main 时 就已将有一个创建好的对象呢?我们可以声明一个的静态成员变量(不属于类,本质是全局的),然后在类外面定义;这样就可以保证在进程启动时就已有唯一的对象了
class Singleton
{
public:
static Singleton* GetInstance()
{
return &m_instance;
}
private:
// 私有化构造
Singleton() {}
// 禁用掉拷贝
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
// 创建一个 static Singleton 的成员属性
static Singleton m_instance;
};
Singleton Singleton::m_instance;// 再类外定义
我们如何验证此时的对象只有一个呢?我们可以打印获取到单例对象的地址:
• 饿汉模式的优点:实现简单
• 饿汉模式的缺点:可能会导致进程启动慢;如果两个单例有启动先后顺序,那么饿汉无法控制
如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。因为在程序启动前需要进行初始化,如果需要初始化的资源很多,就会降低程序的启动速度。
2、懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件, 初始化网络连接,读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。懒汉模式就是在我们需要使用时,第一次才给我们创建对象
class Singleton
{
public:
static Singleton* GetInstance()
{
// 存在线程安全的问题--》加锁
unique_lock<mutex> lock(_mtx);
if (m_instance == nullptr)
{
m_instance = new Singleton();
}
return m_instance;
}
private:
// 私有化构造
Singleton() {}
// 禁用掉拷贝
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
// 声明一个 static Singleton 的成员属性
static Singleton* m_instance;
// 保证线程安全,声明一把互斥锁
static mutex _mtx;
};
Singleton* Singleton::m_instance = nullptr;// 再类外定义
mutex Singleton::_mtx;// 再类外定义
这种情况下,还可以稍微优化一下:因为枷锁还是要消耗时间的,如果是第一次还好,不是第一次的话后面的每一次都要“傻傻的”加锁,在白白的消耗资源,所以我们可以在这里进行提前判断一下:
static Singleton* GetInstance()
{
// 双重判断
if (m_instance == nullptr)
{
// 存在线程安全的问题--》加锁
unique_lock<mutex> lock(_mtx);
if (m_instance == nullptr)
{
m_instance = new Singleton();
}
}
return m_instance;
}
• 懒汉模式的优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制;
• 懒汉模式的缺点:复杂。
上述的懒汉式实现的单例,有一点点小问题就是 new 的那个单例对象没有释放没可能会造成内存泄漏的问题!这里可以等进程结束的时候释放,也可以自己写一个回收机制,这里我们也实现一个简单的gc:
class Singleton
{
public:
static Singleton* GetInstance()
{
// 双重判断
if (m_instance == nullptr)
{
// 存在线程安全的问题--》加锁
unique_lock<mutex> lock(_mtx);
if (m_instance == nullptr)
{
m_instance = new Singleton();
}
}
return m_instance;
}
// 回收资源的 GC
class GC
{
public:
~GC()
{
if (Singleton::m_instance)
delete Singleton::m_instance;
}
};
static GC gc;// 定义一个静态成员变量,程序结束时,系统自动调用它的析构释放资源
private:
// 私有化构造
Singleton() {}
// 禁用掉拷贝
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
// 声明一个 static Singleton 的成员属性
static Singleton* m_instance;
// 保证线程安全,声明一把互斥锁
static mutex _mtx;
};
Singleton* Singleton::m_instance = nullptr;// 再类外定义
mutex Singleton::_mtx;// 再类外定义
我们这次使用多线程测试一下:
int main()
{
thread t1([] {cout << Singleton::GetInstance() << endl; });
thread t2([] {cout << Singleton::GetInstance() << endl; });
t1.join();
t2.join();
cout << Singleton::GetInstance() << endl;
cout << Singleton::GetInstance() << endl;
return 0;
}
OK,本期分享就到这里,我是 CP 我们下期再见~!