目录
前言
一.如何设计一个类只能在堆上创建对象
二.如何设计一个类只能在栈上创建对象
三.如何设计一个类不能拷贝
四.如何设计一个类不能被继承
五. 单例模式从理论到实现
六. 总结前文
在面试的时候经常会遇到一些特殊类设计的题目. 这些题目其实也暗含了一些设计模式, 我们要想限制对象的构造, 最容易想到的方式当然就是先限制构造函数, 然后我们来提供特殊的构造对象的接口. 就像是单例模式一样。。。。。
然鹅, 在构造出来对象之前我们是没有对象的, 如何调用我们自己设计的限制性的创建一个对象的函数呢??? 将函数静态化, 这样这个函数是属于整个类的, 使用类名 + ::就可以实现访问了
class HeapOnly {
public:
static HeapOnly* CreateObj() {
return new HeapOnly;
}
private:
HeapOnly() {};
//方式1: 防止拷贝构造
HeapOnly(const HeapOnly& h);
public:
//方式2 :防止拷贝构造
HeapOnly(const HeapOnly& h) = delete;
};
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
回忆杀. 我们在 new 一个对象的时候的本质:
- 调用 全局的 operator new 分配内存
- 调用构造函数初始化
delete一个对象的本质:
- 调用析构函数做结束处理, 可能是写磁盘呀, 关闭文件等等操作
- 调用 全局的 operator delete 释放资源
class StackOnly {
public:
static StackOnly CreateObj() {
return StackOnly(); //构造一个临时对象拷贝返回
}
//因为存在临时对象的返回, 存在拷贝构造, 所以没有必要也不可以禁止掉拷贝构造。。
//思考 ? 和上述堆区创建对象不一样之处
private:
StackOnly() {};
//方式2: 禁止堆区构造必须调用的函数
//void* operator new(size_t size) = delete;
//void operator delete(void* p) = delete;
};
class CopyBan {
public:
CopyBan(int a = 0): _a(a) {
//C++11的方式
//CopyBan(const CopyBan& obj) = delete;
//CopyBan& operator= (const CopyBan& obj) = delete;
private:
//C++98方式
CopyBan(const CopyBan& obj);
CopyBan& operator= (const CopyBan& obj);
int _a;
};
//方式1,
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
class B : public NonInherit {
};
//方式2:使用final 关键字修饰
class A final {
};
什么叫做单例模式: 单例, 单个对象(实例), 也就是一个类只能创建一个对象. 这个就是单例模式.
单例模式简单应用场景如下:
- 比如在某个服务器程序中,该服务器的配置信息存放在一个文件 中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置 信息,这种方式简化了在复杂环境下的配置管理。
- 还有就是游戏开发的时候的地图, 这种一般也是设计为单例的, 因为地图肯定也是仅仅加载一份的, 所有玩家都是使用的同一份地图
单例模式实现思路:
- 首先因为我们对于对象创建的限制, 所以肯定还是私有化构造函数和禁止拷贝构造, 然后我们需要提供一个获取这个全局单例对象的一个对外接口 GetInstance
- 然后因为全局仅仅一个单例对象, 一个类只能实例化出来一个对象, 这个对象来源: 堆区 + 全局数据段 (必然不可以是栈区, 因为栈区 生命周期有限)
- 这个对象要与类同在, 要属于整个类, 而不是某一个对象, 所以需要 static
饿汉式: 一来就要吃, 所以是在进入主函数之前就已经创建好了这个单例对象的....
使用堆区 new 一个对象 + static成员指针指向这个new出来的堆区对象
class Singleton {
public:
static Singleton* GetInstance() {
return _ins;
}
void Print() {
cout << "饿汉子" << endl;
}
private:
Singleton() {}
Singleton(const Singleton& ins) = delete; //禁止拷贝构造
Singleton& operator=(const Singleton& ins) = delete;
static Singleton* _ins;
};
Singleton* Singleton::_ins = new Singleton;
另外一种及其简单的方式, 直接创建一个静态的对象 return &ins; 很骚气的方式, 利用static 的优势, 仅仅在第一次的时候创建出来这个对象, 且这个对象不会因为使用过一次之后释放掉, 后面的每一次都是复用的同一个这个对象, 也巧妙简单的实现了单例。。。
class Singleton {
public:
static Singleton* GetInstance() {
static Singleton ins;
return &ins;
}
private:
Singleton() {}
Singleton(const Singleton& ins) = delete;
const Singleton& operator =(const Singleton& ins) = delete;
};
懒汉式实现单例:
相较饿汉式, 什么时候适合使用懒汉式
假设单例类构造函数中,要做很多配置初始化工作,那么饿汉就不合适了。。(会导致进入主函数延迟, 程序启动会非常慢, 因为饿汉是在进行主函数之前创建的对象)
懒汉式含义:
懒汉式故名思意就是比较懒, 不会在进入主函数之前就创建出来单例对象, 仅仅只是在第一次需要使用的时候创建单例对象。。。 意味着 需要在 GetInstance 中实现创建对象配置等等操作。。。。。
这个时候思考一个问题??????? 对于懒汉式, 调用GetInstance 函数的时候创建对象, 其实相当于是写入操作, 会不会出现线程不安全的问题?????
当然一个线程区调用这个GetInstance 函数是没有问题的, 但是如果是多个线程同时调用,可能就会出现冲突了.... 重入, 两个线程或者多个线程同时进入判断 _ins == nullptr 都去创建对象, 就不安全了..... 所以这个时候 针对 _ins的判断和创建操作我们需要将其绑定为一个原子操作
多个线程同时进行 读取操作, 是不会产生线程危险的问题的, 就是重入了, 只要不写入数据也是没有问题的, 所以前面的懒汉 不需要加锁保护, 就是因为他仅仅只是 return 创建好的对象, 仅仅只是读取操作, 不存在需要创建对象的写操作, 所以哪怕是多线程来读取都是没问题的..... 此处多线程写需要保护.......
但但是上述是否存在一定的效率问题?????? 要是 存在很多的线程都需要调用这个 GetInstance函数, 但是其实仅仅只是第一次写入的时候才会存在线程安全问题.,......因为一旦第一次写的时候是线程安全的, 写入进去之后其实只需要做的事情就是 读取操作了, 就不再需要写了, 这个时候, 只要经历第一次写的时候是线程安全的, 后面全部都是读取操作了, 其实就没必要再不停的加锁解锁了.... 所以这个时候我们一般会用双检查来提高效率
class Singleton {
public:
static Singleton* GetInstance() {
if (_ins == nullptr) { //双检查提高效率
_lock.lock();
if (_ins == nullptr) {
_ins = new Singleton;
}
_lock.unlock();
}
return _ins;
}
static void DelInstance() { //销毁单例对象的接口
_lock.lock();
if (_ins) {
delete _ins;
_ins = nullptr;
}
_lock.unlock();
}
void Print() {
cout << "懒汉子" << endl;
}
class Garbage { //垃圾回收类, 最后前面没有调用Del接口, 此处自动回收
public:
~Garbage() {//此处不进行加锁, 因为此处的锁很可能已经释放
if (_ins) {
delete _ins;
_ins = nullptr;
}
}
};
private:
Singleton() {
//配置初始化
}
~Singleton() {
//程序结束之后需要做一些持久化保存工作, 比如写入磁盘操作
}
Singleton(const Singleton& ins) = delete; //禁止拷贝构造
Singleton& operator=(const Singleton& ins) = delete;
static Singleton* _ins;
static mutex _lock;
static Garbage gar;
};
Singleton* Singleton::_ins = nullptr;
mutex Singleton::_lock;
Singleton::Garbage Singleton::gar;
- 首先是从特殊类的设计做引入,: 普世方法: 私有化构造函数, 然后提供公有的限制性的创建对象的静态接口, 因为开始没有对象, static 之后可以使用类名调用创建
- 然后引入单例模式: 一个类只能实例化一个对象的一种设计模式..... 为了实现实例化对象的限制, 还是私有化构造函数, 提供限制性创建或者是获取对象的公有静态的接口函数
- 饿汉式单例, 一来就创建单例对象了, 进入主函数之前就创建好了, 缺陷, 如果配置文件等等过大, 可能导致程序启动起来特别慢.. 它的 GetInstance函数仅仅是读取_ins单例对象的操作, 所以是线程安全的, 不需要加锁保护
- 懒汉式单例, 第一次需要的时候再 GetInstance 中创建单例对象, 因为第一次实例化单例对象的时候可能存在多个线程写操作, 也就是多个线程都要 调用 GetInstance,这个时候为了避免函数重入这样的冲突, 所以加锁 原子操作进行保护,为了提高效率仅仅第一次写加锁保护, 于是采用双检查模式提高效率