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


相关文章
|
11天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
2月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
63 2
|
2月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
113 5
|
2月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
115 4
|
2月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
152 4
|
2月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
137 4
|
3月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
35 4
|
3月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
33 1
|
5月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
2月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!