C++从面试常考实现特殊类到单例模式的实现

简介: C++从面试常考实现特殊类到单例模式的实现

前言

在面试的时候经常会遇到一些特殊类设计的题目. 这些题目其实也暗含了一些设计模式, 我们要想限制对象的构造, 最容易想到的方式当然就是先限制构造函数, 然后我们来提供特殊的构造对象的接口. 就像是单例模式一样。。。。。  


然鹅, 在构造出来对象之前我们是没有对象的, 如何调用我们自己设计的限制性的创建一个对象的函数呢???   将函数静态化, 这样这个函数是属于整个类的, 使用类名 + ::就可以实现访问了


私有化构造函数    +   禁止掉拷贝构造, 不让其进行拷贝构造


禁掉拷贝构造的方式方法.  1. 私有化拷贝构造( C++98 )   2. delete关键字修饰( C++11)


构造函数私有化了, 但是还是需要可以构造出对象, 提供GetInstance 公有接口限制性的创建对象


公有接口静态化, 使得可以通过  类名访问  (解决 如何调用问题)

一.如何设计一个类只能在堆上创建对象

  • 私有化构造函数
  • 禁掉拷贝构造
  • 提供类公有静态接口函数 限制性的只能在堆上创建
class HeapOnly {
public:
  static HeapOnly* CreateObj() {
    return new HeapOnly;
  }
private:
  HeapOnly() {};  
  //方式1: 防止拷贝构造
  HeapOnly(const HeapOnly& h);
public:
  //方式2 :防止拷贝构造
  HeapOnly(const HeapOnly& h) = delete;
};

二.如何设计一个类只能在栈上创建对象

  • 只能在栈上创建, 其实就是禁止掉在堆上和全局数据段创建的对象
  • 此处 除了使用上述的方式, 私有化然后提供一个  GetInstance 接口的方式以外,还存在如下的方式方法
  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;
};
  • 使用禁止掉 oprator new 和 operator delete的缺陷所在, 仅仅限制了在堆区创建对象, 但是在全局数据段, 也就是静态 static 这个是没有禁止掉的..

三.如何设计一个类不能拷贝

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 : 将父类的构造函数进行一个私有化,这样继承不会报错, 但是子类无法调用父类构造函数, 也就没法复用父类的代码。。。 实例化的时候报错....  因为仅仅在实例化的时候才会真正调用父类构造函数, 才能发现调用不了    


方式2: C++11方式, 来了一个 final 关键字 修饰类, 表示这个类是最终类了, 自然也就不会允许被继承, 一继承就会报错呢

//方式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,这个时候为了避免函数重入这样的冲突, 所以加锁 原子操作进行保护,为了提高效率仅仅第一次写加锁保护, 于是采用双检查模式提高效率


相关文章
|
2天前
|
安全 编译器 程序员
【C++篇】C++类与对象深度解析(六):全面剖析拷贝省略、RVO、NRVO优化策略
【C++篇】C++类与对象深度解析(六):全面剖析拷贝省略、RVO、NRVO优化策略
13 2
|
2天前
|
存储 C++
【C++篇】C++类和对象实践篇——从零带你实现日期类的超详细指南
【C++篇】C++类和对象实践篇——从零带你实现日期类的超详细指南
12 2
【C++篇】C++类和对象实践篇——从零带你实现日期类的超详细指南
|
2天前
|
安全 C语言 C++
【C++篇】探寻C++ STL之美:从string类的基础到高级操作的全面解析
【C++篇】探寻C++ STL之美:从string类的基础到高级操作的全面解析
22 4
|
2天前
|
存储 编译器 数据安全/隐私保护
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解2
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解
13 3
|
2天前
|
编译器 C++
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解1
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解
18 3
|
2天前
|
存储 编译器 程序员
【C++篇】手撕 C++ string 类:从零实现到深入剖析的模拟之路
【C++篇】手撕 C++ string 类:从零实现到深入剖析的模拟之路
22 2
|
2天前
|
存储 设计模式 编译器
【C++篇】C++类与对象深度解析(五):友元机制、内部类与匿名对象的高级应用
【C++篇】C++类与对象深度解析(五):友元机制、内部类与匿名对象的高级应用
11 2
|
2月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
2天前
|
JSON 安全 前端开发
第二次面试总结 - 宏汉科技 - Java后端开发
本文是作者对宏汉科技Java后端开发岗位的第二次面试总结,面试结果不理想,主要原因是Java基础知识掌握不牢固,文章详细列出了面试中被问到的技术问题及答案,包括字符串相关函数、抽象类与接口的区别、Java创建线程池的方式、回调函数、函数式接口、反射以及Java中的集合等。
9 0
|
2月前
|
XML 存储 JSON
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。