C++进阶 特殊类的设计

简介: C++进阶 特殊类的设计

设计一个类不能被拷贝

我们的拷贝只会发生在两个场景当中

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

所以说我们只需要让类失去 或者说不能使用这两个函数即可

这里有两个解决方案

在C++98中

我们将拷贝构造函数只声明不定义 并且将其访问权限设置为私有即可

class CopyBan
{
  // ...
private:
  CopyBan(const CopyBan&);
  CopyBan& operator=(const CopyBan&);
  //...
};

原因如下

  • 我们设置为私有化之后 就能够禁止用户在外面调用 如果不设置私有 用户可以在外面定义并且调用
  • 因为我们不需要这个函数 所以定义没有意义 并且如果定义的话就可以在类内部调用从而以另一个函数完成拷贝

在C++11中

在C++11中 如果我们需要禁用一个函数直接使用delete关键字即可

代码标识如下

class CopyBan
{
  // ...
  CopyBan(const CopyBan&)=delete;
  CopyBan& operator=(const CopyBan&)=delete;
  //...
};

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

这个题目很有意思 让我们只能在堆上创建对象 也就是说我们不能在栈区还有静态区创建对象

再通俗一点说 我们不能创建局部变量和全局变量

用代码标识就是 下面的定义方式不允许

HeapOnly ho;
static HeapOnlu ho;

我们都知道 创建对象是需要构造函数的 所以说我们只需要将构造函数私有化之后 上面的创建方式就不允许了

但是实际上构造函数私有化之后我们也不能定义堆上的对象了

此时我们就需要创建一个新的函数 让这个函数帮助我们创建一个对象来

设计代码如下

static HeapOnly* CreateObject() 
 {   
    return new HeapOnly;  
 }

我们使用一个静态函数new出来一个新的对象 然后返回这个对象的指针 当然我们也只有这种方式可以获取新的对象 也就是说我们只能在堆上创建对象了

与此同时我们要禁用拷贝构造和赋值运算符重载

因为可能会有人写出这样子的代码导致我们的对象创建在栈上

HeapOnly* HO2 = HeapOnly(HO1);

禁用拷贝构造和赋值运算符重载的思路同第一

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

设计思路和只能在堆上创建对象类似 我们都是私有化构造函数之后暴露出一个静态函数来让外部调用

这个静态函数的唯一功能就是在栈上创建一个对象并且返回这个对象

class StackOnly
{
public:
  static StackOnly Createobj()
  {
    return StackOnly();
  }
private:
    //将构造函数设置为私有
  StackOnly()
    {}
};

这里有一点需要特别注意 因为我们是在函数的内部创建了一个对象 所以说这个对象是一个局部变量 所以说我们必须要使用传值返回而不能使用传引用返回

而传值返回不可避免的一点就是 我们需要使用拷贝构造函数

但是呢 有了拷贝构造函数之后我们就没办法限制在堆和静态区创建对象了 因为用户可以通过拷贝构造的方式来实现这一点

所以说我们一定没办法禁止全局对象的创建 即在静态区中创建对象

但是我们还是有办法禁止对象在堆上创建

因为new和delete的底层使用的是operator new和operator delete所以说我们只需要在类中禁用这两个成员函数即可

void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;

设计一个类不能被继承

在C++98中

在C++98中 因为子类必须要调用父类的构造函数 所以说我们只需要将父类的构造函数私有化

此时父类就成为了事实上不能被继承的类了 代码标识如下

class NonInherit
{
public:
  static NonInherit GetInstance()
  {
    return NonInherit();
  }
private:
    //构造函数私有
  NonInherit()
  {}
};

在C++11中

在C++11中 提供了一个关键字给我们使用 final

  • final关键字 如果我们在一个类的后面加上final 那么该类就是最终类 不能被继承
class A final
{
  //...  
};

单例模式

设计模式概念:

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

使用设计模式的目的:

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

单例模式:

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式有两种实现方式 分别是 饿汉模式懒汉模式

饿汉模式

饿汉模式我们可以理解为这个人十分的饥饿 所以说食物必须要提前准备好

反应到代码中 就是这个单例要在main函数启动前就要准备好

设计思路如下

  • 我们让这个类中有一个静态的类对象 只声明不定义
  • 将构造函数私有化 私有化之后外部就不能通过构造函数来创建对象
  • 删除拷贝构造和赋值运算符重载函数
  • 在类外面定义这个静态对象
  • 暴露出一个静态函数 返回这个静态对象的指针

至此外面的饿汉模式就设计完毕了

//单例模式:全局只有唯一对象
//饿汉模式:一开始就创建对象(main函数之前)
class InfoSingleton
{
public:
  static InfoSingleton& GetInstance()
  {
    return _sins;
  }
private:
  InfoSingleton()
  {}
    InfoSingleton(InfoSingleton& info) = delete;
  InfoSingleton& operator=(const InfoSingleton& info) = delete;
private:
  static InfoSingleton _sins;//声明
};
InfoSingleton InfoSingleton::_sins;//定义,属于类域::,可以调用构造函数

饿汉模式的优缺点

优点:

  • 实现方式简单
  • 没有线程安全相关问题

缺点:

  • 因为在main函数之前就要定义 可能会导致启动慢
  • 无法控制单例初始化顺序

懒汉模式

懒汉模式我们可以理解为这个人十分的懒 所以说不到最后一刻要交任务的时候绝对不会做事的

反应到代码中 就是我们需要使用这个单例的时候这个单例才会创建 也就是说在main函数里面创建

设计思路如下

  • 我们让这个类中有一个静态的类对象 只声明不定义
  • 将构造函数私有化 私有化之后外部就不能通过构造函数来创建对象
  • 删除拷贝构造和赋值运算符重载函数
  • 在类外面定义这个静态对象指针设置为空
  • 暴露出一个静态函数 如果说指针为空则我们构造并返回一个新的对象

至此我们的懒汉模式就设计完毕了

//懒汉模式
class InfoSingleton
{
public:
  static InfoSingleton& GetInstance()
  {
    //第一次获取单例对象的时候创建对象
    if (_psins == nullptr)
    {
            //第一次获取单例对象的时候创建对象
      _psins = new InfoSingleton;
    }
    return *_psins;
  }
private:
  InfoSingleton()
  {}
  InfoSingleton(const InfoSingleton& info) = delete;
  InfoSingleton& operator=(const InfoSingleton& info) = delete;
private:
  static InfoSingleton* _psins;
};
InfoSingleton* InfoSingleton::_psins=nullptr;

懒汉模式的优缺点

优点:

  • 可以主动控制定义顺序
  • 在main函数后面启动 不影响启动时间

缺点:

  • 有很严重的线程安全问题

如果有两个线程同时进入了GetInstance()函数内部就有可能发生线程安全问题 而导致产生两个或多个对象出来

我们这里推荐一种双检查加锁模式

代码标识如下

static InfoSingleton& GetInstance()
  {
    //第一次获取单例对象的时候创建对象
    if (_psins == nullptr)//对象new出来以后,避免每次都加锁的检查,提高性能
    {
      _smtx.lock();
      if (_psins == nullptr)//保证线程安全的检查且只new一次
      {
        _psins = new InfoSingleton;
      }
      _smtx.unlock();
    }
    return *_psins;
  }

首先我们检查下 对象指针是否为空 如果为空我们加锁 (主要是为了避免无脑先加锁带来的效率损失)

其次我们加锁 并且再次检查对象指针是否为空 (主要是为了线程安全问题)

如果为空我们创建对象之后解锁 如果不为空我们直接解锁

单例模式对象的释放问题

我们的单例模式对象创建之后一般会运行到程序结束 所以说一般不存在释放问题

如果说就非要释放的话我们可以创造一个函数来释放我们的对象

static void DelInstance()
{
  _mtx.lock();
  if (_inst != nullptr)
  {
    delete _inst;
    _inst = nullptr;
  }
  _mtx.unlock();
}

此外如果我们担心内存泄漏问题的话也可以使用智能指针来管理该对象

总结

相关文章
|
1月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
51 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
105 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
94 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
112 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
32 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
32 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
29 1
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
2月前
|
存储 编译器 C语言
【C++类和对象(上)】—— 我与C++的不解之缘(三)
【C++类和对象(上)】—— 我与C++的不解之缘(三)