• 单例模式(饿汉模式 & 懒汉模式)与一些特殊类设计


    文章目录

    一、不能被拷贝的类

    二、只能在堆上创建类对象

    三、只能在栈上创建类对象

    四、不能被继承的类

    五、单例模式

    5、1 什么是单例模式

    5、2 什么是设计模式

    5、3 单例模式的实现

    5、3、1 饿汉模式

    5、3、1 懒汉模式


    🙋‍♂️ 作者:@Ggggggtm 🙋‍♂️

    👀 专栏:C++ 👀

    💥 标题:特殊类的设计💥

     ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️ 

    一、不能被拷贝的类

      一个类拷贝都是由拷贝构造来完成的。拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。代码如下:

    1. class NonCopyableClass {
    2. public:
    3. NonCopyableClass() {}
    4. private:
    5. // 禁用拷贝构造函数
    6. NonCopyableClass(const NonCopyableClass&) = delete;
    7. // 禁用拷贝赋值运算符
    8. NonCopyableClass& operator=(const NonCopyableClass&) = delete;
    9. };
    10. int main() {
    11. NonCopyableClass obj1;
    12. // 编译错误,不能拷贝对象
    13. // NonCopyableClass obj2(obj1);
    14. //NonCopyableClass obj3 = obj1;
    15. return 0;

      在C++98中,将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有也是可以的。

    二、只能在堆上创建类对象

      我们在栈和静态区创建对象时,都需要调用构造函数来初始化。同时,为了防止拷贝和赋值在栈空间上,所以将默认构造、拷贝构造、赋值重载都禁用了。

      那我们可以在类内部单独提供一个动态申请对象的静态成员函数。具体代码如下:

    1. class HeapOnly
    2. {
    3. public:
    4. // 提供一个公有的,获取对象的方式,对象控制是new出来的
    5. static HeapOnly* CreateObj()
    6. {
    7. return new HeapOnly;
    8. }
    9. // 防拷贝
    10. HeapOnly(const HeapOnly& hp) = delete;
    11. HeapOnly& operator=(const HeapOnly& hp) = delete;
    12. void Destroy() {
    13. delete this;
    14. }
    15. private:
    16. // 构造函数私有
    17. HeapOnly()
    18. :_a(0)
    19. {}
    20. private:
    21. int _a;
    22. };
    23. int main()
    24. {
    25. HeapOnly* hp = HeapOnly::CreateObj();
    26. //HeapOnly copy(*hp);
    27. hp->Destroy();
    28. return 0;
    29. }

    三、只能在栈上创建类对象

      为了防止在堆上或者静态区申请对象,构造函数应该私有。可以在类内部提供一个返回栈对象的方法。代码如下:

    1. class StackOnly
    2. {
    3. public:
    4. static StackOnly CreateObj()
    5. {
    6. StackOnly st;
    7. return st;
    8. }
    9. private:
    10. // 构造函数私有
    11. StackOnly()
    12. :_a(0)
    13. {}
    14. private:
    15. int _a;
    16. };

       但是,此时还能拷贝构造和赋值呢!!!那么能够禁用掉拷贝构造吗?答案是不能的。原因是我们通过传值返回的栈对象,此时必须需要拷贝。不能够传引用返回,因为是局部变量。出了函数就会被销毁。我们最多的就是禁用掉new的使用,具体代码如下:

    1. class StackOnly
    2. {
    3. public:
    4. static StackOnly CreateObj()
    5. {
    6. StackOnly st;
    7. return st;
    8. }
    9. // 不能防拷贝
    10. //StackOnly(const StackOnly& st) = delete;
    11. StackOnly& operator=(const StackOnly& st) = delete;
    12. void* operator new(size_t n) = delete;
    13. private:
    14. // 构造函数私有
    15. StackOnly()
    16. :_a(0)
    17. {}
    18. private:
    19. int _a;
    20. };
    21. int main()
    22. {
    23. StackOnly st1 = StackOnly::CreateObj();
    24. // 拷贝构造
    25. static StackOnly copy2(st1);
    26. //StackOnly* copy3 = new StackOnly(st1);
    27. return 0;
    28. }

    四、不能被继承的类

      一个类不能被继承有两种方法:

    • C++98方式:构造函数私有化,派生类中调不到基类的构造函数。则无法继承。
      1. class NonInherit
      2. {
      3. public:
      4. static NonInherit GetInstance()
      5. {
      6. return NonInherit();
      7. }
      8. private:
      9. NonInherit()
      10. {}
      11. };
    • C++11方法:final关键字,final修饰类,表示该类不能被继承。
      1. class A final
      2. {
      3. // ....
      4. };

    五、单例模式

    5、1 什么是单例模式

      单例模式是一种设计模式,用于确保在整个应用程序中只存在一个特定类的实例对象,该实例对象被所有程序模块共享。其主要目的是限制类的实例化操作,以确保在任何情况下都只能获得同一个实例。

      比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

    5、2 什么是设计模式

      设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。

      使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

    5、3 单例模式的实现

      单例模式实现的方法有两种:饿汉模式、懒汉模式。我们先来看一下饿汉模式的实现。

    5、3、1 饿汉模式

      所谓饿汉模式,就是在main函数之前,就会创建出对象。并不会考虑到你是否要用这个对象。那怎么实现呢?

      首先,为了保证只能实例出一份对象,就必须把构造函数私有化。其次,需要在自己的类内部定义一个静态的类对象或者类指针。然后在类内部定义一些静态成员函数来初始化、获取对象/指针等操作。具体代码如下:

    1. //class MemoryPool
    2. //{
    3. //public:
    4. //
    5. //private:
    6. // // 构造函数私有化
    7. // MemoryPool()
    8. // {}
    9. //
    10. // char* _ptr = nullptr;
    11. // // ...
    12. //
    13. // static MemoryPool _inst; // 声明
    14. //};
    15. //
    16. 定义
    17. //MemoryPool MemoryPool::_inst;
    18. class MemoryPool
    19. {
    20. public:
    21. static MemoryPool* GetInstance()
    22. {
    23. return _pinst;
    24. }
    25. void* Alloc(size_t n)
    26. {
    27. void* ptr = nullptr;
    28. // ....
    29. return ptr;
    30. }
    31. void Dealloc(void* ptr)
    32. {
    33. // ...
    34. }
    35. MemoryPool(MemoryPool& my) = delete;
    36. MemoryPool& operator= (MemoryPool& my) = delete;
    37. private:
    38. // 构造函数私有化
    39. MemoryPool()
    40. {}
    41. char* _ptr = nullptr;
    42. // ...
    43. static MemoryPool* _pinst; // 声明
    44. };
    45. // 定义
    46. MemoryPool* MemoryPool::_pinst = new MemoryPool;
    47. int main()
    48. {
    49. //MemoryPool pool1;
    50. //MemoryPool pool2;
    51. void* ptr1 = MemoryPool::GetInstance()->Alloc(10);
    52. MemoryPool::GetInstance()->Dealloc(ptr1);
    53. }

      我们来分析一下:当类内定义的是静态成员时,需要在类外进行初始化。那么在类外能够调用私有的构造函数进行初始化吗?答案是可以的!因为该成员是属于类内部的私有成员,只不过是在类外进行的初始化。

      那么能在类内定义非静态类对象吗?答案是不可以的,这本身就是语法错误。其次静态成员变量并不属于某个对象,而是属于整个类

      总结饿汉模式(Eager Initialization) 的优点

    • 实现简单,线程安全。在类加载时就创建了实例,没有线程安全问题。
    • 可以保证在任何时候获取到同一个实例。

      缺点

    • 类加载时即创建实例,尤其是在实例初始化比较耗时的情况下,会影响到程序的启动速度。
    • 如果该实例一直没有被使用,则会造成内存的浪费。
    • 当有多个单例对象时,饿汉模式无法很好的控制其初始化先后顺序。当然,在一个文件內部还好,在多个文件中就无法确定静态成员的初始化顺序。

    5、3、1 懒汉模式

      如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
       懒汉模式在第一次使用时才会创建单例对象,延迟实例化以节省资源。代码如下:

    1. class MemoryPool
    2. {
    3. public:
    4. static MemoryPool* GetInstance()
    5. {
    6. if (_pinst == nullptr)
    7. {
    8. _pinst = new MemoryPool;
    9. }
    10. return _pinst;
    11. }
    12. void* Alloc(size_t n)
    13. {
    14. void* ptr = nullptr;
    15. // ....
    16. return ptr;
    17. }
    18. void Dealloc(void* ptr)
    19. {
    20. // ...
    21. }
    22. // 实现一个内嵌垃圾回收类
    23. class CGarbo {
    24. public:
    25. ~CGarbo()
    26. {
    27. if (_pinst)
    28. delete _pinst;
    29. }
    30. };
    31. MemoryPool(MemoryPool& my) = delete;
    32. MemoryPool& operator= (MemoryPool& my) = delete;
    33. private:
    34. // 构造函数私有化
    35. MemoryPool()
    36. {
    37. // ....
    38. }
    39. char* _ptr = nullptr;
    40. // ...
    41. static MemoryPool* _pinst; // 声明
    42. };
    43. // 定义
    44. MemoryPool* MemoryPool::_pinst = nullptr;
    45. // 回收对象,main函数结束后,他会调用析构函数,就会释放单例对象
    46. static MemoryPool::CGarbo gc;

      总结懒汉模式(Lazy Initialization) 优点

    • 延迟实例化,只有第一次调用获取实例的方法时才会创建对象,避免了资源浪费。
    • 可以控制对象的初始化顺序。
    • 不影响程序的启动速度。

      缺点

    • 不是线程安全的,如果多个线程并发地调用获取实例的方法,可能会创建多个实例。
    • 在多线程环境下,需要额外的同步措施来保证线程安全,增加了复杂性开销。
  • 相关阅读:
    HDLbits: Lfsr5
    [python 刷题] 242 Valid Anagram
    警告:未配置spring boot 配置注解处理器
    CSP模拟52联测14 A.长春花
    Cookie、Session、Token、JWT详解
    蔚来杯2022牛客暑期多校训练营1
    【openwrt学习笔记】miniupnpd学习笔记
    Webpack浅记
    XOR Construction
    【LeetCode-中等题】34. 在排序数组中查找元素的第一个和最后一个位置
  • 原文地址:https://blog.csdn.net/weixin_67596609/article/details/133090680