【C++11】特殊类设计

简介: 【C++11】特殊类设计

> 作者:დ旧言~

> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:理解特殊类,能自己模拟实现特殊类。

> 毒鸡汤:白日莫闲过,青春不再来。

> 专栏选自:C嘎嘎进阶

> 望小伙伴们点赞👍收藏✨加关注哟💕💕



🌟前言

在C++的学习的过程中,我们已经知道如何创建一个类,之后我们学习了类的三大特性封装,继承,多态,在C++11中我们有一些特殊的类,我们称这些类为--->特殊类,那特殊类真的有那么特殊么???


⭐主体

学习【C++11】特殊类设计咱们按照下面的图解:



🌙 设计一个类,不能被拷贝

拷贝只会发生在两个场景中:

  • 拷贝构造函数
  • 赋值运算符重载

因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可


💫 C++98方法:

解决方案:

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

举个栗子:

class CopyBan
{
public:
  CopyBan()
  {}
private:
  CopyBan(const CopyBan& cb);            // 拷贝构造函数声明
  CopyBan& operator=(const CopyBan& cb); // 赋值运算符重载声明
};


说明原因:

  1. 设置成私有:如果只声明而没有设置成 private,用户自己如果在类外定义了,就可以不能禁止拷贝了。
  2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没什么意义,不写反而更简单,而且如果定义了就不能防止成员函数内部拷贝了。


总结:

如上图代码,在对这个特殊类进行拷贝和赋值的时候,因为这两个成员函数私有而无法调用

拷贝构造以及赋值运算符重载等成员函数,在调用时都是编译器在域外调用,所以必须是公有的


💫 C++11方法:

C++11 扩展了 delete 的用法,delete 除了释放 new 申请的资源以外,如果在默认成员函数后跟上 = delete,表示让编译器删除掉该默认成员函数,此时编译器也不会自动生成默认的拷贝构造和赋值运算符重载函数。

举个栗子:

class CopyBan
{
public:
    CopyBan()
    {}
private:
    CopyBan(const CopyBan& cb) = delete;            // 拷贝构造函数声明
    CopyBan& operator=(const CopyBan& cb) = delete; // 赋值运算符重载声明
};


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

正常创建类对象时,会在栈上创建,并且自动调用构造函数来初始化。下面设计一个类,只能在堆上创建对象。只能在堆上创建对象的含义就是:必须使用new来创建对象.

实现方式:

只能在创建在堆上时,就需要让该对象只能通过 new 来创建,并且调用构造函数来初始化。

  1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
  2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建。

举个栗子:

class HeapOnly
{
public:
  static HeapOnly* CreateObject()
  {
    return new HeapOnly;
  }
private:
  //构造函数
  HeapOnly() {}
  HeapOnly(const HeapOnly& hp) = delete; // 禁止拷贝
};


总结分析:

定义一个静态成员函数,在该函数内部 new 一个 HeapOnly 对象。将构造函数和拷贝构造函数私有,并且禁止生成拷贝构造函数。

分析原因:

  • 使用静态成员函数 new 一个 HeapOnly 对象。


  1. 那么非静态成员函数在调用时,必须使用点 (.) 操作符来调用,这一步是为了传 this 指针。这样的前提是先有一个 HeapOnly 对象,但是构造函数设置成了私有,就无法创建这样一个对象。
  2. 而静态成员函数的调用不用传 this 指针,也就不需要必须有 HeapOnly 对象,只需要类域 :: 静态成员函数即可。静态成员函数属于 HeapOnly 域内,所以在 new 一个对象的时候,可以调用私有的构造函数。


  • 禁止调用拷贝构造函数并且私有化。

这样做的目的是为了禁止拷贝,防止使用堆区上的 HeapOnly 对象在栈区上拷贝,如下面代码:


而禁止了拷贝构造就杜绝了这一行为,从而保证了 HeapOnly 对象只能在堆上创建。


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

步骤如下:

  • 主要是需要做到不能在堆上创建类对象。
  • new 一个对象的时候,会调用该类的 operator new(size_t size) 函数,在释放资源的时候又会调用该类的 operator delete(void* p) 函数。


💫 方法一:

解决方案:

防止在堆上创建类对象就是要禁止调用 operator new(size_t size) 和 operator delete(void* p) 这两个函数。


举个栗子:

class StackOnly
{
public:
  // 构造函数
  StackOnly()
  {}
 
  void* operator new(size_t size) = delete; // 禁止调用new
  void operator delete(void* p) = delete;   // 禁止调用delete
};


说明:

使用 delete 来禁止这两个函数的调用,那么在 new 一个对象的时候,就会产生编译错误,从而无法在堆区上创建类对象。



此时在堆上创建对象时就会报错,尝试引用已删除的函数。


💫 方法二:

解决方案:

另一种方式就是和之前一样,通过一个静态成员函数在栈区上创建一个类对象,并且将默认构造函数私有化,然后设计静态方法创建对象返回即可。

举个栗子:

class StackOnly
{
public:
  static StackOnly CreateObject()
  {
    return StackOnly();
  }
private:
  StackOnly()
  {}
};


此时 new 一个对象的时候,由于默认构造函数私有而无法调用,所以报错。



总结分析:

但以上两种设计方法都有一个漏洞,类对象可以在静态区(数据段)上创建:

static StackOnly so = StackOnly::CreateObject();


设计特殊类的核心:

  • 只能通过静态成员函数来创建类,封掉其他所有创建方式


🌙 设计一个类,不能被继承


💫 C++98 方法:

解决方法:

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

举个栗子:

// 基类
class NonInherit
{
private:
  // 基类构造函数私有
  NonInherit()
  {}
};
 
// 派生类
class B : public NonInherit
{};


💫 C++11 方法:

解决方法:

使用 final 关键字来修饰类,表示该类不能被继承。

举个栗子:

class A final
{};


🌙 设计一个类,只能创建一个对象


💫 设计模式

概念阐述:

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

为什么会产生设计模式这样的东西呢:

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


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


💫 单例模式:

概念阐述:

  • 一个类只能创建一个对象。
  • 该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。


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


分析:

  • 全局只有一个实例对象,所以将单例对象放在静态区 / 堆区(保证只创建一次)。
  • 为了防止其他位置创建该对象,将构造函数私有。
  • 为了防止拷贝,使用 delete 修饰拷贝构造和赋值运算符重载函数。

1. 饿汉模式

概念阐述:

就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。

举个栗子:

// 饿汉模式
// 优点:简单
// 缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。
class Singleton
{
public:
  static Singleton* GetInstance()
  {
    return _ins;
  }
private:
  //限制类外随意创建对象
  Singleton(const Singleton& s) = delete;
  Singleton& operator=(const Singleton& s) = delete;
  Singleton()
  {}
private:
  static Singleton* _ins;
};
Singleton* Singleton::_ins = new Singleton;


分析原因:

静态成员变量只能在类域外进行定义初始化。所以在 main 函数之前就将单例对象定义初始化,此时该单例对象创建在静态区上,而且仅有一个,后面就无法再创建。

为什么称之为饿汉模式呢?

不管将来会不会使用到这个单例对象,但是在程序一启动还没有进入 main 函数之前就创建一个唯一的实例对象。就像一个饿汉一样,一上来就先吃(创建单例对象)。

总结:

 如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。


饿汉单例模式优点:


  1. 保证全局(整个进程)只有唯一实例对象。
  1. 饿汉模式:一开始就创建对象,特别简单。

饿汉单例模式缺点:


  • 多个单例对象 A、B、C 假设要求他们之间有依赖关系:依次创建就无法达到,无法保证顺序可能会导致进程启动很慢    


如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避

免资源竞争,提高响应速度更好。

1. 饿汉模式

代码实现:

//懒汉模式
class Singleton
{
public:
  static Singleton* GetInstance()
  {
    if (_ins == nullptr)//双检查加锁,只有第一次进来时需要加锁,其他情况不用加锁
    {
      imtx.lock();
      if (_ins == nullptr)//第一次调用才创建实例!
      {
        _ins = new Singleton;
      }
      imtx.unlock();
    }
 
    return _ins;
  }
  void DelInstance()
  {
    imtx.lock();
    if (_ins != nullptr)
    {
      cout << "over!!!" << endl;
      delete _ins;
      _ins = nullptr;
    }
    imtx.unlock();
  }
private:
  //限制类外随意创建对象
  Singleton(const Singleton& s) = delete;
  Singleton& operator=(const Singleton& s) = delete;
  Singleton()
  {}
private:
  static Singleton* _ins;
  static mutex imtx;
};
Singleton* Singleton::_ins = nullptr;
mutex Singleton::imtx;


为什么称之为懒汉模式呢?


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


优缺点:

优点:

  • 第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序(通过代码顺序)自由控制。

缺点:

  • 复杂。
  • 如果不加锁是会出现线程安全的问题。但是加锁是会十分影响性能的,所以引入了双检查。那么既要保证线程安全+又要保证效率的问题。


🌟结束语

      今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。


目录
相关文章
|
3天前
|
编译器 C++
C++的基类和派生类构造函数
基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。 在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。 这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。 下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:
12 1
|
15天前
|
C++ 运维
开发与运维函数问题之析构函数在C++类中起什么作用如何解决
开发与运维函数问题之析构函数在C++类中起什么作用如何解决
27 11
|
15天前
|
C++ 运维
开发与运维函数问题之C++类的简单示例如何解决
开发与运维函数问题之C++类的简单示例如何解决
42 10
|
14天前
|
C++ 运维
开发与运维C++问题之在重新设计后,LogFileReader类的角色发生了什么变化
开发与运维C++问题之在重新设计后,LogFileReader类的角色发生了什么变化
28 1
|
1月前
|
C++
C++友元函数和友元类的使用
C++中的友元(friend)是一种机制,允许类或函数访问其他类的私有成员,以实现数据共享或特殊功能。友元分为两类:类友元和函数友元。类友元允许一个类访问另一个类的私有数据,而函数友元是非成员函数,可以直接访问类的私有成员。虽然提供了便利,但友元破坏了封装性,应谨慎使用。
51 9
|
27天前
|
存储 编译器 C语言
【C++基础 】类和对象(上)
【C++基础 】类和对象(上)
|
1月前
|
编译器 C++
【C++】string类的使用④(字符串操作String operations )
这篇博客探讨了C++ STL中`std::string`的几个关键操作,如`c_str()`和`data()`,它们分别返回指向字符串的const char*指针,前者保证以&#39;\0&#39;结尾,后者不保证。`get_allocator()`返回内存分配器,通常不直接使用。`copy()`函数用于将字符串部分复制到字符数组,不添加&#39;\0&#39;。`find()`和`rfind()`用于向前和向后搜索子串或字符。`npos`是string类中的一个常量,表示找不到匹配项时的返回值。博客通过实例展示了这些函数的用法。
|
1月前
|
存储 C++
【C++】string类的使用③(非成员函数重载Non-member function overloads)
这篇文章探讨了C++中`std::string`的`replace`和`swap`函数以及非成员函数重载。`replace`提供了多种方式替换字符串中的部分内容,包括使用字符串、子串、字符、字符数组和填充字符。`swap`函数用于交换两个`string`对象的内容,成员函数版本效率更高。非成员函数重载包括`operator+`实现字符串连接,关系运算符(如`==`, `&lt;`等)用于比较字符串,以及`swap`非成员函数。此外,还介绍了`getline`函数,用于按指定分隔符从输入流中读取字符串。文章强调了非成员函数在特定情况下的作用,并给出了多个示例代码。
|
1月前
|
C++
【C++】string类的使用④(常量成员Member constants)
C++ `std::string` 的 `find_first_of`, `find_last_of`, `find_first_not_of`, `find_last_not_of` 函数分别用于从不同方向查找目标字符或子串。它们都返回匹配位置,未找到则返回 `npos`。`substr` 用于提取子字符串,`compare` 则提供更灵活的字符串比较。`npos` 是一个表示最大值的常量,用于标记未找到匹配的情况。示例代码展示了这些函数的实际应用,如替换元音、分割路径、查找非字母字符等。
|
1月前
|
C++
C++】string类的使用③(修改器Modifiers)
这篇博客探讨了C++ STL中`string`类的修改器和非成员函数重载。文章介绍了`operator+=`用于在字符串末尾追加内容,并展示了不同重载形式。`append`函数提供了更多追加选项,包括子串、字符数组、单个字符等。`push_back`和`pop_back`分别用于在末尾添加和移除一个字符。`assign`用于替换字符串内容,而`insert`允许在任意位置插入字符串或字符。最后,`erase`函数用于删除字符串中的部分内容。每个函数都配以代码示例和说明。