特殊类设计(只在堆/栈上创建对象,单例模式),完整版代码+思路

本文主要是介绍特殊类设计(只在堆/栈上创建对象,单例模式),完整版代码+思路,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

类不能被拷贝

类不能被继承

只在堆上创建对象

只在栈上创建对象

operator new

operator delete

只能创建一个对象

设计模式

介绍

常见的设计模式

单例模式

介绍

应用

饿汉模式

介绍

实现

思路

代码

使用

懒汉模式

引入

介绍

实现

思路

代码

使用

显式析构

隐式析构


类不能被拷贝

拷贝有两种方式,拷贝构造和赋值拷贝,所以只需要让创建出的对象不能使用这两个成员函数即可

"不能使用"可以有两种方式

  • 设置为私有
  • 私有后,一般只声明不定义,如果定义了,可能会在类内部使用拷贝操作
  • 使用关键字"delete"
  • c++11后扩展了delete的用法
  • delete除了释放new申请的资源外,如果在默认成员函数后跟上
    =delete,表示让编译器删除掉该默认成员函数
  • 既然已经删除了,也就无法使用了

类不能被继承

之前就有提到过,如果构造函数是私有的,则无法创建对象

那在继承概念中,如果基类无法被创建,自然子类也创建不出来,也就没有继承的说法了

也可以使用c++11引入的final关键字,表示该类不能被继承

只在堆上创建对象

  • 也就是说,只能通过申请资源的方式创建对象,那么申请到的都是由指针指向的一块空间,也就是说,需要我们返回指针
  • 但构造函数显然无法满足这个条件,所以我们干脆禁用构造函数,直接给一个接口函数
  • 禁用就和前面不能拷贝类似,有两种方式

  • 注意!!!要把这个接口函数设置成静态的
  • 这个函数本来就是用来创建对象的,但普通成员函数的调用又需要一个对象,这就形成了先有鸡还是先有蛋的问题
  • 所以,直接设置成静态的,就可以用类域调用了
class HeapOnly
{
public:static HeapOnly* CreateObject() //一定要是静态的嗷!!!{return new HeapOnly;}
private:// C++98// 1.只声明,不实现,本身就不需要实现// 2.声明成私有HeapOnly() {}HeapOnly(const HeapOnly&);C++11    //HeapOnly(const HeapOnly&) = delete;
};

只在栈上创建对象

也就是要禁止申请空间,首先可以采用上面的方式,直接给接口,构造禁掉

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

类对象在new的时候,实际上会先调用operator new,然后再调用构造函数

operator new

  • 是C++中用于动态分配内存的内置运算符
  • 主要作用是分配一块连续的内存空间,以便在其中存储对象或数据
  • 可以被重载

所以我们可以声明一个删除的operator new函数,这样外部就不能用new申请资源了

class StackOnly
{
public:StackOnly(){}StackOnly(const StackOnly& tmp) {}
private:void* operator new(size_t size) = delete;int _a = 1;
};

还可以禁掉释放资源的函数,这样申请到的资源也就无法释放,那编译器就不会允许你在堆上申请

还记得前面的operator new吗,类似的,delete也会先调用operator delete

operator delete

  • 和operator new配套的运算符,用于释放动态分配内存的内置运算符
  • 通过传递要释放的内存块的指针,它将该内存块返回给系统或内存管理器,以便将其重新分配给其他用途
  • 允许重载

所以,和上面操作类似

class StackOnly
{
public:StackOnly(){}StackOnly(const StackOnly& tmp) {}
private://void* operator new(size_t size) = delete;void operator delete(void* p) = delete;int _a = 1;
};

只能创建一个对象

设计模式

介绍

  • 设计模式是一种用于解决软件设计问题的经验和最佳实践的复用方案
  • 它们提供了在特定情境下的通用解决方案,有助于创建更可维护、灵活和可扩展的软件
  • 设计模式是从实践中总结出来的,并被广泛接受和使用,以解决常见的设计问题

常见的设计模式

这里只介绍一下单例模式

单例模式

介绍

单例模式是一种创建型设计模式,其目的是确保一个类只有一个实例,并提供一个全局访问点来访问该实例

应用
  • 当需要共享某个资源,例如配置信息、日志记录、数据库连接、线程池等,单例模式可以确保全局只有一个资源实例,避免资源的浪费和冲突
  • 当需要维护全局状态,例如应用程序的状态或设置,单例模式可以提供一个中心点来管理和访问这些状态
  • 也就是程序运行过程中,只需要一份/只能有一份的时候,单例模式可以防止创建出多份对象

饿汉模式

介绍
  • 也称为预先实例化模式,是一种单例模式的实现方式
  • 在饿汉模式中,单例实例在类加载时就被创建,因此在整个程序生命周期中,该实例都是唯一的
实现
思路
  • 因为只能有一个,所以构造/拷贝构造/赋值拷贝都必须禁用
  • 为了确保只有一份实例,我们的接口函数必须返回的是同一个对象
  • 并且这个对象应该是静态的,不然怎么实现返回的都是一个对象,必须要让它的作用域是全局,而不是某个对象
  • 由于是预先实例化,所以提前在程序开始前实例化,也就是在全局中实例化
代码
class Singleton {
public:static Singleton& GetInstance() { //每次调用接口都只返回那一个对象return _instance;}void add(int t) {_arr.push_back(t);}void print() {for (auto t : _arr) {std::cout << t << " ";}}
private:Singleton() {}; //注意这里要有函数体(也就是要定义它),因为我们的_instance需要被初始化Singleton(const Singleton&);Singleton& operator=(const Singleton&);static Singleton _instance;std::vector<int> _arr;
};
Singleton Singleton::_instance; 
使用
int main() {Singleton::GetInstance().add(1);//使用类域调用对象接口,并且用返回值直接调用函数,因为对象是私有的,不能被显式定义Singleton::GetInstance().add(2);Singleton::GetInstance().add(3);Singleton::GetInstance().print();return 0;
}

懒汉模式

引入
  • 饿汉模式存在很多缺陷,如果类很大,那程序启动所需的时间将会很长
  • 或者如果某个类需要依赖其他类,但我们无法保证究竟哪个类先被实例化
  • 所以,为了解决这些问题,懒汉模式就被引入了
介绍
  • 懒汉模式推迟了实例的创建,直到首次访问该实例时才进行初始化
  • 避免在程序启动时需要大量资源初始化时,产生不必要的开销
  • 但是,在多线程环境下不是线程安全的
实现
思路
  • 既然需要在首次访问时创建对象,那么就在第一次调用接口时才创建对象,之后返回该对象即可
  • 该如何判断是否为第一次呢?
  • 可以考虑用一个指针,如果该指针有指向的对象,就说明不是第一次了
  • 但是,这个对象必须得是动态开辟出来的,不然构造出来的是个右值对象,没法取地址
  • 所以,就要面临释放资源的问题
  • 但是,由于我们的变量是个指针,没法自动析构,所以要定义一个接口del来手动释放
代码
namespace lazy {class Singleton {public:static Singleton& GetInstance() { //每次调用接口都只返回那一个对象if (_p==nullptr) {//_p = &Singleton(); //如果_p指向普通对象,这里就是右值了,无法取地址_p = new Singleton;//所以必须要动态申请,这样的话就需要释放资源了//但其实一般单例模式不需要释放,随程序结束就自己释放了//但我们可能中途需要释放,那析构函数就没有用了,_p是静态对象,不会在中途自己调用析构//所以需要定义一个接口}return *_p;}static void DelInstance() { //因为我们的对象是私有的,在外部访问不到,定义成static方便一点if (_p) {delete _p;//这里会调用析构_p = nullptr;}}void add(int t) {_arr.push_back(t);}void print() {for (auto t : _arr) {std::cout << t << " ";}}private:Singleton() {};Singleton(const Singleton&);Singleton& operator=(const Singleton&);~Singleton() //_p离开作用域并不会调用析构,所以需要手动调用,也就是在del接口中{std::cout << "~Singleton()" << std::endl;//其他操作}std::vector<int> _arr;static Singleton* _p;//和_instance一样,要定义成静态的,保证_p的唯一性};Singleton* Singleton::_p = nullptr;}
使用
  • 使用其实和饿汉模式差不多,只不过懒汉的构造调用时间不同,并且增加了释放资源的接口
  • 注意!!!因为我们定义的是个指针,所以不会自动析构
  • 所以,唯一的接口就是那个del,在它里面可以调用析构
  • 我们可以手动调用del
  • 也可以改造一下让他具有析构函数的性质(离开作用域自动调用)
显式析构

像上面那样写,可以让我们显式调用del函数来释放资源,并且在释放前完成某些操作

int main() {lazy::Singleton::GetInstance().add(1); lazy::Singleton::GetInstance().add(2);lazy::Singleton::GetInstance().add(3);lazy::Singleton::GetInstance().print();lazy::Singleton::DelInstance();//显式调用lazy::Singleton::GetInstance().add(4);lazy::Singleton::GetInstance().print();return 0;
}

注意,这里最后程序没有自动调用析构,因为我们的静态成员是_p指针,它来指向被申请的空间,而不是直接创建一个对象

隐式析构
  • 如果我们想要在程序结束前也完成某些操作,就可能不太方便,需要我们手动调用接口,况且我们可能也不知道什么时候程序结束
  • 所以,可以利用智能指针的特性,将del接口放在某个类的析构函数中
  • 这样随着类析构,也就自动调用了del函数
  • namespace lazy {class Singleton {public:static Singleton& GetInstance() { //每次调用接口都只返回那一个对象if (_p==nullptr) {//_p = &Singleton(); //如果_p指向普通对象,这里就是右值了,无法取地址_p = new Singleton;//所以必须要动态申请,这样的话就需要释放资源了//但其实一般单例模式不需要释放,随程序结束就自己释放了//但我们可能中途需要释放,那析构函数就没有用了,_p是静态对象,不会在中途自己调用析构//所以需要定义一个接口}return *_p;}static void DelInstance() { //因为我们的对象是私有的,在外部访问不到,定义成static方便一点if (_p) {delete _p;//这里会调用析构_p = nullptr;}}class func { //用于程序结束自动调用del接口public:~func() {Singleton::DelInstance();}};void add(int t) {_arr.push_back(t);}void print() {for (auto t : _arr) {std::cout << t << " ";}}private:Singleton() {}; //注意这里要有函数体(也就是要定义它),因为我们的_instance需要被初始化Singleton(const Singleton&);Singleton& operator=(const Singleton&);~Singleton() //vs2019下,静态对象好像不会自动调析构,而是直接释放资源了{std::cout << "~Singleton()" << std::endl;//其他操作}std::vector<int> _arr;static Singleton* _p;static func _f;//定义一个静态对象//得是静态的,不然一个对象一个_f,会对_p析构多次};Singleton* Singleton::_p = nullptr;Singleton::func Singleton::_f; 
    }
  • 这样,即使我们没有显式调用del,也可以在程序结束前自动调用del

这篇关于特殊类设计(只在堆/栈上创建对象,单例模式),完整版代码+思路的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/288172

相关文章

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

利用Python调试串口的示例代码

《利用Python调试串口的示例代码》在嵌入式开发、物联网设备调试过程中,串口通信是最基础的调试手段本文将带你用Python+ttkbootstrap打造一款高颜值、多功能的串口调试助手,需要的可以了... 目录概述:为什么需要专业的串口调试工具项目架构设计1.1 技术栈选型1.2 关键类说明1.3 线程模

Python Transformers库(NLP处理库)案例代码讲解

《PythonTransformers库(NLP处理库)案例代码讲解》本文介绍transformers库的全面讲解,包含基础知识、高级用法、案例代码及学习路径,内容经过组织,适合不同阶段的学习者,对... 目录一、基础知识1. Transformers 库简介2. 安装与环境配置3. 快速上手示例二、核心模

Nginx location匹配模式与规则详解

《Nginxlocation匹配模式与规则详解》:本文主要介绍Nginxlocation匹配模式与规则,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、环境二、匹配模式1. 精准模式2. 前缀模式(不继续匹配正则)3. 前缀模式(继续匹配正则)4. 正则模式(大

Java的栈与队列实现代码解析

《Java的栈与队列实现代码解析》栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶,栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等,本文给大家介绍J... 目录栈的概念(Stack)栈的实现代码队列(Queue)模拟实现队列(双链表实现)循环队列(循环数组

使用Java将DOCX文档解析为Markdown文档的代码实现

《使用Java将DOCX文档解析为Markdown文档的代码实现》在现代文档处理中,Markdown(MD)因其简洁的语法和良好的可读性,逐渐成为开发者、技术写作者和内容创作者的首选格式,然而,许多文... 目录引言1. 工具和库介绍2. 安装依赖库3. 使用Apache POI解析DOCX文档4. 将解析

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

使用Python实现全能手机虚拟键盘的示例代码

《使用Python实现全能手机虚拟键盘的示例代码》在数字化办公时代,你是否遇到过这样的场景:会议室投影电脑突然键盘失灵、躺在沙发上想远程控制书房电脑、或者需要给长辈远程协助操作?今天我要分享的Pyth... 目录一、项目概述:不止于键盘的远程控制方案1.1 创新价值1.2 技术栈全景二、需求实现步骤一、需求

Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码

《Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码》:本文主要介绍Java中日期时间转换的多种方法,包括将Date转换为LocalD... 目录一、Date转LocalDateTime二、Date转LocalDate三、LocalDateTim

jupyter代码块没有运行图标的解决方案

《jupyter代码块没有运行图标的解决方案》:本文主要介绍jupyter代码块没有运行图标的解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录jupyter代码块没有运行图标的解决1.找到Jupyter notebook的系统配置文件2.这时候一般会搜索到