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


相关文章
|
3月前
|
缓存 安全 编译器
C++面试周刊(3):面试不慌,这样回答指针与引用,青铜秒变王者
《C++面试冲刺周刊》第三期聚焦指针与引用的区别,从青铜到王者级别面试回答解析,助你21天系统备战,直击高频考点,提升实战能力,轻松应对大厂C++面试。
404 131
C++面试周刊(3):面试不慌,这样回答指针与引用,青铜秒变王者
|
5月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
142 0
|
5月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
222 0
|
7月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
273 12
|
8月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
161 16
|
8月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
8月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
8月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
9月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
9月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)