• C++之特殊类设计


    设计一个不能被拷贝的类

    拷贝只会发生在两个场景中: 拷贝构造函数以及赋值运算符重载, 因此想要让一个类禁止拷贝, 只需让该类不能调用拷贝构造函数以及赋值运算符重载即可.

    C++98
    将拷贝构造函数与赋值运算符重载只声明不定义且将其访问权限设置为私有即可:

    1. class banCopy
    2. {
    3. public:
    4. banCopy()
    5. {}
    6. private:
    7. banCopy(const banCopy& a);
    8. banCopy& operator=(const banCopy& a);
    9. };

    1. 只声明不定义: 只声明是因为拷贝构造和赋值重载都是默认成员函数, 用户不写编译器会自动生成, 不定义是因为该函数根本不会被调用, 定义了其实也没有什么意义, 而且如果定义了就不能防止成员函数内部的拷贝.

    2. 设置成私有是为了防止, 如果只声明不定义但没有设置私有, 但用户自己在类外定义了这两个函数, 依然可以调用这两个函数, 而设置为私有即使自己类外定义了也无法调用.

    1. banCopy::banCopy(const banCopy& a)
    2. {
    3. //假如类外实现拷贝构造
    4. //..
    5. }

    仍然无法被拷贝 

    C++11

    C++11扩展delete的用法, 如果在默认成员函数后加上=delete, 表示让编译器删除掉该默认成员函数, 从根本上杜绝了拷贝的发生:

    1. class banCopy
    2. {
    3. private:
    4. banCopy(const banCopy& a) = delete;
    5. banCopy& operator=(const banCopy& a) = delete;
    6. };

    设计一个只能在堆上创建对象的类

    方法一: 析构函数私有化

    C++是一个静态绑定的语言。在编译过程中,所有的非虚函数调用都必须分析完成。即使是虚函数, 也需检查可访问性。因此,  当在栈上生成对象时, 对象会自动析构, 也就说析构函数必须可以访问, 否则编译出错。而在堆上生成对象, 由于析构函数由程序员调用(通过使用delete), 所以不一定需要析构函数。

    1. class heapOnly
    2. {
    3. private:
    4. ~heapOnly()
    5. {
    6. cout << "~heapOnly" << endl;
    7. }
    8. };
    9. int main()
    10. {
    11. heapOnly h1;
    12. return 0;
    13. }

    在堆上申请空间就可以创建对象: 

    1. int main()
    2. {
    3. heapOnly* h3 = new heapOnly;
    4. delete h3;
    5. return 0;
    6. }

      那如何delete呢? 可以在类内实现一个Destroy函数:

    1. class heapOnly
    2. {
    3. public:
    4. static void Destroy(heapOnly* ptr)
    5. {
    6. delete ptr;
    7. }
    8. private:
    9. ~heapOnly()
    10. {
    11. cout << "~heapOnly" << endl;
    12. }
    13. };
    1. int main()
    2. {
    3. heapOnly* h3 = new heapOnly;
    4. heapOnly::Destroy(h3);
    5. return 0;
    6. }

    这里将Destroy设置为了静态成员函数, 不然需要h3->Destroy(h3)有点怪, 但是静态成员函数可以访问非静态成员(~heapOnly)吗?  不可以, 因为static成员函数没有this指针, 但是这里也没有直接用this调用析构, 而是通过delete调用析构, 也就是先调用的是ptr->~heapOnly(), 再调用operator delete函数释放对象的空间, 和this没什么关系.

    但更简便的写法是:

    1. class heapOnly
    2. {
    3. public:
    4. void Destroy()
    5. {
    6. delete this;
    7. }
    8. private:
    9. ~heapOnly()
    10. {
    11. cout << "~heapOnly" << endl;
    12. }
    13. };
    14. int main()
    15. {
    16. heapOnly* h3 = new heapOnly;
    17. h3->Destroy();
    18. return 0;
    19. }

    这里的h2其实就是传给Destroy的this指针, 直接delete this即可.

    方法二: 构造函数+拷贝构造函数私有化

    1. class heapOnly2
    2. {
    3. public:
    4. static heapOnly2* Create()
    5. {
    6. return new heapOnly2;
    7. }
    8. private:
    9. heapOnly2()
    10. {
    11. cout << "heapOnly2()" << endl;
    12. }
    13. heapOnly2(const heapOnly2& hp)
    14. {}
    15. };
    16. int main()
    17. {
    18. heapOnly2* h1= heapOnly2::Create();
    19. //heapOnly2 h2(*h1);//无法拷贝构造从而在栈上建立对象
    20. return 0;
    21. }

    1. 将构造函数私有, 就无法直接在通过heapOnly2 h;的形式在栈上创建一个对象, 那如何在堆上创建呢? 再写一个Create成员函数在内部new一个对象并返回这块空间的地址, 但是又出现一个问题: 没有对象如何调用对象的成员函数呢? 所以要将这个函数修饰为静态函数, 同理这里也不会用到this指针, 因为new是先operator new开辟一段空间, 然后在申请的空间上执行构造函数, 和this没有关系, 可以设置为静态成员函数.

    2. 此外, 这里还要把拷贝构造函数私有, 否则的话还是可以通过拷贝构造函数构造一个栈上的对象:


     设计一个只能在栈上创建对象的类

    法一: 构造私有,提供一个在栈上创建对象的静态成员函数 (有缺陷)

    先延续设计一个只能在堆上创建对象的类的思路, 但是这里通过析构函数私有化就无法限制new在堆上开辟空间, 那我们可以将构造函数私有化, 并提供一个静态成员函数用来在栈上创建对象: 

    1. class stackOnly
    2. {
    3. public:
    4. static stackOnly Create()
    5. {
    6. return stackOnly();
    7. }
    8. private:
    9. stackOnly()
    10. {}
    11. };

    限制了在堆上创建对象: 

     通过Create可以在栈上创建对象:  

    但是这样有一个缺陷, 我们可以通过拷贝构造的方式在堆上创建对象: 

    那可以把拷贝构造函数设置为私有吗?

    看上去好像是可以的, 但是这样也无法在栈上创建对象了:

    我们是通过Create函数在类作用域内部调用构造创建匿名对象, 并将匿名对象通过值返回的方式隐式使用拷贝构造去构造一个栈上的对象, 所以禁用拷贝构造虽然限制了在堆上创建对象, 但也限制了我们在栈上创建对象, 伤敌一千, 自损一千二.

    所以这种方法我们设计出的只能在栈上创建对象的类是有缺陷的。

    在类中禁用 operator new 和 operator delete 函数 

    再从new的角度出发, new是 operator new + 构造函数, 既然不能禁用构造函数, 那么我们可以去禁用operator new:

    如果类中没有重载 operator new 和 operator delete 函数,那么 new 和 delete 默认会去调用全局的 operator new 和 operator delete 函数, 注意, 这两个函数是普通的全局函数, 而不是运算符重载, 只是它们的函数名是这样. 但是如果我们在自己的类中重载一个operator new, new就会去调用自己实现的那个operator new, 实现了类专属的operator new.

    重载 operator new 和 operator delete 函数, 然后将它们私有化或者删除(=delete),  这样就不能通过 new 和 delete 在堆上创建与销毁对象了.

    1. class stackOnly
    2. {
    3. public:
    4. stackOnly() {}
    5. ~stackOnly() {}
    6. //实现类专属的operator new
    7. //new这个对象时, operator new就会调用这个, 而不会调用全局的
    8. void* operator new(size_t size) = delete;
    9. void operator delete(void* p) = delete;
    10. };


     设计一个不能被继承的类

    C++98:

    构造函数私有化, 派生类中调不到基类的构造函数, 则无法继承 

    1. class NonInherit
    2. {
    3. public:
    4. static NonInherit Create()
    5. {
    6. return NonInherit();
    7. }
    8. private:
    9. NonInherit()
    10. {}
    11. };

    C++11:
    final关键字, final修饰类, 表示该类不能被继承

    1. class A final
    2. {
    3. // ....
    4. };

    设计一个只能创建一个对象的类(单例模式)

    什么是设计模式?

    设计模式(Design Patter)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结.

    为什么会产生设计模式呢?

    就像人类历史发展会产生兵法. 最开始部落之间打仗时都是人拼人的对砍, 后来春秋战国时期, 七国之间经常打仗, 就发现打仗也是有套路的,, 来孙子就总结出了《孙子兵法》.设计模式也是类似.

    使用设计模式的目的:

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

    单例模式

    我们之前其实已经接触过一些设计模式了, 比如迭代器模式适配器/配接器模式,单例模式就是一种设计模式. 一个类只能创建一个对象, 即单例模式. 该模式可以保证系统中该类只有一个实例, 并提供一个访问它的全局访问点, 该实例被所有程序模块共享.

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

    单例模式有两种实现模式: 饿汉模式和懒汉模式

    饿汉模式

    程序启动时就创建一个唯一的实例对象.

    现在我们用饿汉模式写一个单例模式的类:

    1. 首先要删除构造函数拷贝构造函数或者设为私有, 禁构造是为了防止在栈上直接创建和new一个对象, 禁拷贝构造是为了防止用已有的那个单例去靠北构造出新的"单例", 那这也就不叫单例模式了. 禁了拷贝构造函数最好也把赋值重载也禁了, 虽然影响不大. 

    1. #include
    2. //饿汉模式: 提前创建好对象(main函数启动时)
    3. class Singleton_Hunger
    4. {
    5. private:
    6. Singleton_Hunger()
    7. {}
    8. Singleton_Hunger(const Singleton_Hunger& sh)
    9. {}
    10. Singleton_Hunger operator=(const Singleton_Hunger& sh)
    11. {}
    12. private:
    13. map _dic;//类内部的一个成员,以map为例
    14. };

    2. 既然我们禁用了构造函数, 那我们怎么去创建这个单例? 像之前一样在类内提供一个Create接口吗? 肯定不行, 那样就可以创建很多个对象了, 那怎么办? 可以考虑创建一个全局变量, 但是全局变量有个缺点, 如果是在头文件定义这个全局变量而被多个源文件包含, 就会发生重定义的问题, 那就可以考虑设置为一个静态变量, 那如何去创建这个静态变量? 肯定不能在类外创建, 也只能在类内创建了:

    在类内部定义一个Singleton_Hunger类型的静态变量. 

    首先这个Singleton_Hunger类型的变量一定是静态的, 非静态会报错. 那为什么类可以创建一个自己类型的静态变量呢?

    因为静态变量是存放在静态区的, 它并不属于某个对象, 静态成员变量和普通静态变量的区别在于静态成员变量具有类的权限, 可以去访问类的私有属性, 所以在类外定义静态变量的时候是可以调用私有的构造函数的; 另外C++讲究封装, 封装其实就是一种管控, 它受到类域的限制, 所以静态成员变量相当于把静态变量和类进行了融合: 1. 受类访问限定符的限制 2. 可以访问类的私有 3.存放在静态区

    1. #include
    2. //饿汉模式: 提前创建好对象(main函数启动时)
    3. class Singleton_Hunger
    4. {
    5. private:
    6. Singleton_Hunger()
    7. {}
    8. Singleton_Hunger(const Singleton_Hunger& sh)
    9. {}
    10. Singleton_Hunger operator=(const Singleton_Hunger& sh)
    11. {}
    12. private:
    13. map _dic;
    14. static Singleton_Hunger SH;
    15. };
    16. Singleton_Hunger Singleton_Hunger::SH;

     我们还需要提供一个全局访问点, 去得到这个单例, 可以实现一个getInstance接口, 然后再添加几个成员函数便于测试: 

    1. #include
    2. //饿汉模式: 提前创建好对象(main函数启动时)
    3. class Singleton_Hunger
    4. {
    5. public:
    6. //全局访问点
    7. static Singleton_Hunger* getInstantce()
    8. {
    9. return &SH;
    10. }
    11. void Add(const string& key, const string& value)
    12. {
    13. _dic[key] = value;
    14. }
    15. void Print()
    16. {
    17. for (auto& e: _dic)
    18. {
    19. cout << e.first << ":" << e.second << endl;
    20. }
    21. }
    22. private:
    23. Singleton_Hunger()
    24. {}
    25. Singleton_Hunger(const Singleton_Hunger& sh)
    26. {}
    27. Singleton_Hunger operator=(const Singleton_Hunger& sh)
    28. {}
    29. private:
    30. map _dic;
    31. static Singleton_Hunger SH;
    32. };
    33. Singleton_Hunger Singleton_Hunger::SH;

    测试一下: 

    1. int main()
    2. {
    3. Singleton_Hunger::getInstantce()->Add("left", "左边");
    4. Singleton_Hunger::getInstantce()->Add("right", "右边");
    5. Singleton_Hunger::getInstantce()->Print();
    6. return 0;
    7. }

    饿汉模式

    优点:实现简单
    缺点:1. 可能会导致进程启动慢, 2. 如果有多个单例且启动有先后顺序, 启动顺序不确定.

    懒汉模式

    针对饿汗模式存在的这些缺陷, 有人又提出了懒汉模式, 懒汉模式的特点是在第一次使用实例对象时才创建对象. 懒汉模式的简单实现如下:

    1. class Singleton_Lazy
    2. {
    3. public:
    4. //全局访问点
    5. static Singleton_Lazy* getInstantce()
    6. {
    7. if (SH == nullptr)
    8. {
    9. SH = new Singleton_Lazy;
    10. }
    11. return SH;
    12. }
    13. void Add(const string& key, const string& value)
    14. {
    15. _dic[key] = value;
    16. }
    17. void Print()
    18. {
    19. for (auto& e : _dic)
    20. {
    21. cout << e.first << ":" << e.second << endl;
    22. }
    23. }
    24. private:
    25. Singleton_Lazy()
    26. {}
    27. Singleton_Lazy(const Singleton_Lazy& sh)
    28. {}
    29. Singleton_Lazy operator=(const Singleton_Lazy& sh)
    30. {}
    31. private:
    32. map _dic;
    33. static Singleton_Lazy* SH;
    34. };
    35. Singleton_Lazy* Singleton_Lazy::SH = nullptr;

     内部设置一个Singleton_Lazy*的对象并初始化为空指针, 这样就不会提前创建单例, 然后再getInstance函数内new一个对象并返回, 实现第一次需要使用单例的时候再创建.

    测试一下: 

    1. int main()
    2. {
    3. Singleton_Lazy::getInstantce()->Add("left", "左边");
    4. Singleton_Lazy::getInstantce()->Add("right", "右边");
    5. Singleton_Lazy::getInstantce()->Print();
    6. return 0;
    7. }

    由于懒汉模式是在第一次使用单例对象时才去创建单例对象, 所以就不存在程序启动加载慢以及不使用对象浪费系统资源的问题了, 同时, 我们也可以通过在程序中先使用A对象再使用B对象的方式来控制有初始化依赖关系的单例对象的实例化顺序.

    懒汉模式的析构问题: 

    正常来说懒汉模式其实不需要手动去delete释放空间, 因为单例一般是在程序运行期间一直存在的, 进程结束后操作系统会去释放空间, 但是如果我们希望在进程结束前完成一些持久化操作, 比如把数据写入文件之类, 这时就需要通过在析构函数中实现持久化的操作实现, 并用delete去调用析构函数, 也顺便把资源给释放了

    这样进行手动释放顾然可以, 但是如果在不知道这里的单例被手动释放的前提下, 后面又用单例去做了一些事情, 就出现问题了, 所以我们还期望能在main函数结束后自动调用析构. 怎么办? 我们可以用一个创建一个内部类, 并创建一个内部类的对象, 在内部类的析构函数中实现delete操作, 既然这样那先把构造函数设为私有, 然后封装一个destroyInstance接口, 然后在内部类的析构中调用destroyInstance即可, 这个destroy接口可以设置为共有, 保留手动释放的功能:

    1. class Singleton_Lazy
    2. {
    3. public:
    4. //全局访问点
    5. static Singleton_Lazy* getInstance()
    6. {
    7. if (SH == nullptr)
    8. {
    9. SH = new Singleton_Lazy;
    10. }
    11. return SH;
    12. }
    13. static void destroyInstance()
    14. {
    15. if (SH)
    16. {
    17. delete SH;
    18. SH = nullptr;
    19. }
    20. }
    21. void Add(const string& key, const string& value)
    22. {
    23. _dic[key] = value;
    24. }
    25. void Print()
    26. {
    27. for (auto& e : _dic)
    28. {
    29. cout << e.first << ":" << e.second << endl;
    30. }
    31. }
    32. private:
    33. ~Singleton_Lazy()
    34. {
    35. cout << "数据写入文件" << endl;
    36. }
    37. Singleton_Lazy()
    38. {}
    39. Singleton_Lazy(const Singleton_Lazy& sh)
    40. {}
    41. Singleton_Lazy operator=(const Singleton_Lazy& sh)
    42. {}
    43. class gc
    44. {
    45. public:
    46. ~gc()
    47. {
    48. destroyInstance();
    49. }
    50. };
    51. static gc _gc;
    52. private:
    53. map _dic;
    54. static Singleton_Lazy* SH;
    55. };
    56. Singleton_Lazy* Singleton_Lazy::SH = nullptr;
    57. Singleton_Lazy::gc Singleton_Lazy::_gc;

    再来测试: 

    1. int main()
    2. {
    3. Singleton_Lazy::getInstance()->Add("left", "左边");
    4. Singleton_Lazy::getInstance()->Add("right", "右边");
    5. Singleton_Lazy::getInstance()->Print();
    6. return 0;
    7. }

    再来测试就可以自动调用析构了. 

    注意: 饿汉模式不需要考虑这个问题, 因为它是在栈上开辟的, main函数结束后自动可以自动调用析构.


    未完: 

  • 相关阅读:
    Oracle归档日志暴增排查优化
    【esp32】 PWM控制LED亮度原理及代码详解
    《C Primer Plus》第12章复习题与编程练习
    Postman入门教程
    【数据聚类】第三章第三节3:类K-Means算法之模糊K-均值算法(FCM算法)
    c++概述-语言特征
    频次最高的38道selenium面试题及答案
    Pytorch之ConvNeXt图像分类
    JUC并发编程——读写锁(基于狂神说的学习笔记)
    CH单库数据迁移到读写分离模式
  • 原文地址:https://blog.csdn.net/ZZY5707/article/details/136472266