当前位置: 首页 > news >正文

【重走C++学习之路】25、特殊类设计

目录

一、不能被拷贝的类

二、堆上创建对象的类

三、栈上创建对象的类

四、不能被继承的类

五、单例模式

结语


一、不能被拷贝的类

如何实现一个不能被拷贝的类?在看到这个要求的第一反应就是禁掉类的拷贝构造函数和赋值运算符重载函数,再往深了探究,“禁掉”这个词怎么实现?有两种方法:

方法一:私有化

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

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

  1. 设置成私有:设置成private,函数只能在类内部调用,且防止在类外定义从而导致又能使用。
  2. 只声明不定义:不定义是因为这两个函数不会被调用,且还能防止内类成员函数自己调用。

方法二:delete

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

在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。 在这里这两个函数的访问权限没有规定,毕竟显示删除后,不论在哪都不能使用。

二、堆上创建对象的类

方法一:构造和拷贝构造函数私有化,提供对外创建对象的接口

一般我们实例化对象的时候都是在栈上,想要在堆上创建只能通过new实现,那么我们就需要将构造函数和拷贝构造函数“禁掉”,然后通过特定的一个接口提供创建在堆上的对象的功能。

class HeapOnly
{
public:// 对外提供接口static HeapOnly* CreateObject(){return new HeapOnly;}private:// 设置为私有,并且只声明不定义HeapOnly() {}HeapOnly(const HeapOnly&);// 显式deleteHeapOnly(const HeapOnly&) = delete;
};

这里“禁掉”构造和拷贝构造函数的方法和第一个类一样。 

注意:

  • 向外部提供的CreateObj函数必须设置为静态成员函数,因为外部调用该接口就是为了获取对象的,而非静态成员函数必须通过对象才能调用,这就变成鸡生蛋蛋生鸡的问题了。
  • 我们没有必要对赋值运算符重载设置为私有&&只声明不实现或者加上=delete(禁掉),因为赋值运算符重载是两个已经存在的对象,既然已经存在,那势必这俩对象就已经在堆区创建好了,所以它们之间进行赋值操作并不会出错。
  • 拷贝构造是拿一个已经存在的对象去构造另一个对象,此对象是先前未存在的,且拷贝构造后是在栈上的,自然不符合题意,因此需要把拷贝构造给禁掉。

方法二: 将析构函数私有化

class HeapOnly
{
public:......private://析构函数私有化~HeapOnly(){}
};

能这样实现的原因:

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

在这样的基础上有衍生出了一个问题:在调用delete的时候会调用类的析构函数,但是这个时候析构函数私有化了就无法调用,因此就不能直接用delete来释放new出来的对象,而是要在类中提供一个特定的接口来完成类似于delete的操作。

// 静态成员函数释放new的对象
static void DelObj(HeapOnly* ptr)
{delete ptr;
}void DelObj()
{delete this;
}

三、栈上创建对象的类

这个类和上面的差不多,都可以将构造函数私有化,然后设计静态方法创建对象返回即可。

class StackOnly
{
public://静态成员函数,内部调用构造函数创建对象static StackOnly CreateObj(){return StackOnly();//传值返回 —— 拷贝构造}private://构造函数私有StackOnly(){}
};

但是这样有一个问题是: 无法避免外部调用拷贝构造函数在静态区、堆区……创建对象

StackOnly h1 = StackOnly::CreateObj();//栈区
static StackOnly h2(h1);//调用拷贝构造在静态区创建对象
StackOnly* h3 = new StackOnly(h1);//调用拷贝构造在堆区创建对象

那么就需要屏蔽operator new函数和operator delete函数

private:// 私有化,只声明不定义void* operator new(size_t size);void operator delete(void* p);// 显式deletevoid* operator new(size_t size) = delete;void operator delete(void* p) = delete;

四、不能被继承的类

方法一:

该类的构造函数设置为私有即可,因为子类的构造函数被调用时,必须调用父类的构造函数初始化父类的那一部分成员,但父类的私有成员在子类当中是不可见的,所以在创建子类对象时子类无法调用父类的构造函数对父类的成员进行初始化,因此该类被继承后子类无法创建出对象。

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

方法二: 

C++11中提供了final关键字,被final修饰的类叫做最终类,最终类无法被继承,此时就算继承后没有创建对象也会编译出错。

class NonInherit final
{......
};

五、单例模式

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

单例模式有两种实现模式:

  • 饿汉模式

在程序最开始的时候就创建一个唯一的实例对象

实现方式:

  1. 将构造函数私有化,并将拷贝构造和拷贝赋值设为私有或删除,防止外部随意创建对象或拷贝
  2. 在类里创建一个static静态对象的指针,在进入程序入口之前就完成单例对象的初始化
  3. 提供一个static静态成员函数,用来获取单例对象的指针
class Singleton
{
public://静态成员函数内部获取单例对象的指针static Singleton* GetInstance(){return _spInst;}private:Singleton(){}//C++98 防拷贝Singleton(const Singleton&);Singleton& operator=(Singleton const&);//C++11 防拷贝Singleton(const Singleton&) = delete;Singleton& operator=(Singleton const&) = delete;static Singleton* _spInst;//声明
};Singleton* Singleton::_spInst = new Singleton;//定义,在程序入口之前就完成单例对象的初始化

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

  • 饿汉模式在程序运行主函数之前就完成了单例对象的创建,由于main函数之前是不存在多线程的,因此饿汉模式下单例对象的创建过程是线程安全的。
  • 后续所有多线程要访问这个单例对象,都需要通过调用GetInstance函数来获取,这个获取过程是不需要加锁的,因为这是一个读操作。
  • 如果线程通过GetInstance获取到单例对象后,要用这个单例对象进行一些线程不安全的操作,那么这时就需要加锁了。
  • 懒汉模式

也叫延迟加载,在需要的时候创建。因为有些单例对象构造十分耗时或者占用很多资源,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。

实现方式:

  1. 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
  2. 提供一个指向单例对象的static指针,并在程序入口之前先将其初始化为空。
  3. 提供一个全局访问点获取单例对象。
class Singleton
{
public://3、提供一个全局访问点获取单例对象static Singleton* GetInstance(){//双检查if (_inst == nullptr){_mtx.lock();if (_inst == nullptr){_inst = new Singleton;}_mtx.unlock();}return _inst;}
private://1、将构造函数设置为私有,并防拷贝Singleton(){}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;//2、提供一个指向单例对象的static指针static Singleton* _inst;static mutex _mtx; //互斥锁
};//在程序入口之前先将static指针初始化为空
Singleton* Singleton::_inst = nullptr;
mutex Singleton::_mtx; //初始化互斥锁

和饿汉模式相比,这里多出了一个互斥锁的变量。这是因为GetInstance函数第一次调用时需要对static指针进行写入操作,这个过程不是线程安全的,因为多个线程可能同时调用GetInstance函数,如果不对这个过程进行保护,此时这多个线程就会各自创建出一个对象。

饿汉模式和懒汉模式的对比:

  • 懒汉模式需要考虑线程安全问题和释放问题,实现相对于来说较为复杂,饿汉模式不存在以上问题实现简单。

  • 懒汉模式是一种懒加载模式需要的时候再初始化创建对象,不会影响程序的启动,饿汉模式则相反,程序启动阶段就创建初始化对象,会导致程序启动慢。

  • 如果有多个单例类,需要B依赖A,要求A单例创建初始化之后,B才能创建初始化,那么B就不能是饿汉模式,无法保证顺序。

结语

这一篇文章可以看作是对C++知识的融会贯通,学会利用C++类的特性来创建一些特殊类,基本上这些特殊的类在我们当前学习阶段是用不上的,到了后面完成项目的时候会用上,比如说线程池中,一个项目基本上只有一个线程池,那么就需要单例模式。

下一篇将会介绍C++中类型转换相关的知识,有兴趣的朋友可以关注一下。

相关文章:

  • ActiveMQ 性能优化与网络配置实战(一)
  • qemu学习笔记:QOM
  • 算法--模拟题目
  • C++中常用的十大排序方法之1——冒泡排序
  • 【数据结构】线性表--顺序表
  • 常用机械传动方式对比
  • 【笔记】深度学习模型训练的 GPU 内存优化之旅④:内存交换与重计算的联合优化篇
  • 腾讯云BI VS quickbi 企业选型(从企业实际功能使用和费用对比)
  • 提升采购管理,打造核心竞争力七步战略采购法详解P94(94页PPT)(文末有下载方式)
  • 【Bootstrap V4系列】学习入门教程之 页面内容排版
  • Qwen3 发布:优化编码与代理能力,强化 MCP 支持引领 AI 新潮流
  • PHP之CURL通过header传参数及接收
  • 如何快速定位网络中哪台主机发起ARP攻击
  • 前端八股 6
  • 【Linux】C语言补充知识
  • 西门子数字化研发设计制造一体化规划案例P87(87页PPT)(文末有下载方式)
  • PHP-Cookie
  • 攻防世界 - Misc - Level 6 | Wireshark
  • 字节一面:后端开发
  • 卡洛诗西餐的文化破圈之路
  • 我国首个少数民族非遗纺织类国标正式实施
  • AI世界的年轻人|他用影像大模型解决看病难题,“要做的研究还有很多”
  • 孕妇乘坐高铁突发临产,广西铁路部门协助送医平安产子
  • 中国海油总裁:低油价短期影响利润,但也催生资产并购机会
  • 俄外长:俄将在不损害伙伴关系前提下发展对美关系
  • 农业农村部:把住能繁母猪存栏量“总开关”,引导养殖场户优化母猪存栏结构、合理控制产能