这一节主要是对前面学习的类和对象的一个应用,考查对于类和对象基本概念的理解,学会掌握常见特殊类的设计方式,可以加深我们对于类和对象的理解。
目录
一、请设计一个类,不能被拷贝
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝, 只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
C++98的做法:将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
class CopyBan
{
// ...
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
//...
};
原因: 1. 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,它还是可以在类外进行拷贝构造,这样就会失去效果! 2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
C++98的做法:C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上 =delete,表示让编译器删除掉该默认成员函数。
class CopyBan
{
// ...
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
//...
};
二、请设计一个类,只能在堆上创建对象
实现方式:
- 将类的构造函数私有化,拷贝构造声明成私有不实现。防止别人调用拷贝在栈上生成对象。
- 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建。
#include <iostream>
using namespace std;
/*1、*****************请设计一个类,只能在堆上创建对象***************************/
/*思路分析:正常创建出来的对象一定要调用构造函数或者拷贝构造函数,那么我们是不是就可以让构造函数私有化?
但是这样存在缺点:不论是栈上创建出的对象
还是堆上创建出的对象都无法调用构造函数(私有函数无法被外界调用),如何解决?
那么我们是不是可以实现一个成员函数来是实现堆上创建出对象,答案是可以的,
并且需要是静态的成员函数,因为普通成员函数是需要通过对象调用的,
而我们还没有对象无法调用,
静态的成员函数属于这个类,我们可以直接使用类来调用!
但是存在另一个问题:它可以调用拷贝构造创建出对象!
将拷贝构造设置成私有只声明不实现!
************************************************************************************/
/*总结*/
//1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
//2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
class HeapOnly
{
public:
static HeapOnly* GetObj()
{
return new HeapOnly;
}
private:
HeapOnly()
{
}
//C++98做法
// 声明成私有,只声明,不实现。因为我不让你拷贝构造通过!
HeapOnly(const HeapOnly&);
// C++11做法
HeapOnly(const HeapOnly&) = delete;
};
int main()
{
//HeapOnly hp; 无法通过!
//HeapOnly* p=new HeapOnly; 无法通过!
HeapOnly* p = HeapOnly::GetObj();
std::shared_ptr<HeapOnly> sp1(HeapOnly::GetObj()); //防止内存泄漏
//HeapOnly hp1(*sp1); //这里会调用拷贝构造!创建出栈上的对象==>将拷贝构造设置成私有只声明不实现
return 0;
}
/******************************************************************************************/
三、请设计一个类,只能在栈上创建对象
同上将构造函数私有化,然后设计静态方法创建对象返回即可。
/******************请设计一个类,只能在栈上创建对象 ************************/
class StackOnly
{
public:
static StackOnly GetObj()
{
return StackOnly(); //传值返回这里会调用拷贝构造
}
private:
StackOnly()
{
}
};
int main()
{
StackOnly so = StackOnly::GetObj();
//StackOnly * p = new StackOnly; 无法通过!new会调用构造函数,但是无法调用,此时构造函数为私有的!
return 0;
}
/*******************************************************************************/
四、请设计一个类,不能被继承
C++98方式:构造函数私有化,派生类中调不到基类的构造函数。则无法继承
/**************************请设计一个类,不能被继承 *******************************/
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
/*******************************************************************************/
C++11方法 :final关键字,final修饰类,表示该类不能被继承。
/**************************请设计一个类,不能被继承 *******************************/
class A final
{
// ....
};
/*******************************************************************************/
五、请设计一个类,只能创建一个对象(单例模式)
5.1 设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的 总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打 仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后 来孙子就总结出了《孙子兵法》。孙子兵法也是类似。 使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。之前已经学过一些设计模式,迭代器模式--基于面向对象三大特性之一的封装设计出来的,用一个迭代器类封装以后,不暴露容器的结构的情况下,统一的方式访问修改容器中的数据。适配器模式--体现的是一种复用,还有一些常见的设计模式如:工厂模式、装饰器模式、观察者模式、单例模式...
5.2 单例模式
一个类只能在全局(进程)实例化出一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。再比如:一个进程中有一个内存池,进程中的多线程需要内存都要到这个内存池中取,那么这个内存池的类就可以设计成单例模式。单例模式有两种实现模式:饿汉模式和懒汉模式。
5.2.1 饿汉模式
就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。
#include <iostream>
using namespace std;
// 饿汉模式
// 优点:简单
// 缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。
class Singleton
{
public:
static Singleton* GetInstance()
{
if (_pinst == nullptr)
{
_pinst = new Singleton;
}
return _pinst;
}
private:
// 构造函数私有
Singleton()
{
};
// C++98 防拷贝
Singleton(Singleton const&);
Singleton& operator=(Singleton const&);
// C++11 防拷贝
/*Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;*/
static Singleton* _pinst; //静态的所有对象共享一个,而且属于这个类,类内声明,类外定义
};
Singleton* Singleton::_pinst=nullptr; //定义并初始化
int main()
{
cout << Singleton::GetInstance() << endl;
cout << Singleton::GetInstance() << endl;
cout << Singleton::GetInstance() << endl;
return 0;
}
如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避 免资源竞争,提高响应速度更好。
5.2.2 懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取 文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化, 就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。下面这段代码实现了一个线程安全的懒汉式单例模式。懒汉式单例模式在第一次使用实例对象时才创建对象,从而避免了程序启动时的负载。为了保证线程安全和效率,使用了双重检查锁(Double-Check Locking)技术。
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
class Singleton {
public:
// 获取单例实例的静态方法
static Singleton* GetInstance() {
// 第一次检查是否需要实例化单例对象
if (nullptr == m_pInstance) {
// 加锁保护,防止多个线程同时进入
m_mtx.lock();
// 第二次检查,确保单例对象只被实例化一次
if (nullptr == m_pInstance) {
m_pInstance = new Singleton();
}
// 解锁
m_mtx.unlock();
}
return m_pInstance;
}
// 嵌套的垃圾回收类,用于在程序结束时释放单例对象
class CGarbo {
public:
~CGarbo() {
// 删除单例对象
if (Singleton::m_pInstance)
delete Singleton::m_pInstance;
}
};
// 静态成员变量,程序结束时会自动调用其析构函数从而释放单例对象
static CGarbo Garbo;
private:
// 私有构造函数,防止外部实例化
Singleton() {}
// 防止拷贝构造和赋值操作
Singleton(Singleton const&);
Singleton& operator=(Singleton const&);
// 单例对象指针
static Singleton* m_pInstance;
// 互斥锁,保证线程安全
static mutex m_mtx;
};
// 静态成员变量初始化
Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Garbo;
mutex Singleton::m_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;
}
5.2.3 饿汉和懒汉模式的区别
- 懒汉模式需要考虑线程安全和释放的问题,实现相对更复杂,饿汉模式不存在以上问题实现简单。
- 懒汉是一种懒加载模式需要时在初始化创建对象,不会影响程序的启动。饿汉模式则相反,程序启动阶段就创建初始化实例化对象,会导致程序启动慢,影响体验。
至此,这一讲内容介绍完毕,内容简单,星光不问赶路人,加油吧,感谢阅读,如果对此专栏感兴趣,点赞加关注!