[C++] C++特殊类设计 以及 单例模式:设计无法拷贝、只能在堆上创建、只能在栈上创建、不能继承的类, 单例模式以及饿汉与懒汉的场景...

news2024/11/25 4:36:39

|inline


特殊类

1. 不能被拷贝的类

注意, 是不能被拷贝的类, 不是不能拷贝构造的类.

思路就是 了解什么时候 会以什么途径 发生拷贝, 然后将路堵死.

拷贝发生一般发生在 拷贝构造赋值重载

所以, 只要把类的这两个成员函数堵死, 此类就不能拷贝了

  1. C++98

    在C++11之前, 可以通过这种方法 设计不能拷贝的类:

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

    这个类, 将 拷贝构造函数 和 赋值重载函数 设置为private, 并且只声明不实现.

    此时 这个类对象就无法被拷贝.

    int main() {
        CopyBan cB1;
        CopyBan cB2(cB1);
        CopyBan cB3 = cB1;
    
        CopyBan cB4;
        cB4 = cB1;
    
        return 0;
    }
    

    |wide

    需要设置为private. 因为 如果设置为public, 那么函数实现是可以在类外完成的. 如果有人在类外实现了拷贝构造和赋值重载. 那么此类还是可以完成拷贝

  2. C++11

    C++11 提出了delete关键词的扩展用法, 在类的默认成员函数后加上= delete, 表示删除此默认成员函数.

    自然 就可以直接使用= delete来堵死拷贝途径

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

    即使设置为public也可以

    |wide

2. 只能在堆上创建的类

怎么设计只能在堆上创建的类?

只能在堆上创建, 也就是说 类的对象只能通过new创建.

而 一般情况下, 创建类 可以调用 构造函数拷贝构造函数

怎么才能让对象只能通过new获得呢?

如果还需要保证实例化对象时语句的用法与创建普通对象一致. 那做不到.

只能在堆上创建, 那就针对创建对象就 只提供一个默认在堆上创建对象的接口, 并 把其他实例化对象的接口封死

实现如下:

class HeapOnly {
public:
    static HeapOnly* CreateObj() {
        return new HeapOnly;
    }

    HeapOnly(const HeapOnly&) = delete;

private:
    // 构造函数私有
    HeapOnly() {}
};

我们实现了一个static的成员函数CreateObj(), 会new一个HeapOnly对象并返回.

并且, 将 构造函数设置为私有, 还删除了拷贝构造函数, 使此类不能在外面调用构造函数实例化对象, 更不能拷贝构造.

int main() {
    HeapOnly h1;
    static HeapOnly h2;
    HeapOnly* ph3 = new HeapOnly;

    HeapOnly copy(*ph3);

    return 0;
}

此时, 此类就不能再用传统的方式实例化了:

|inline

而是只能通过调用static成员函数CreateObj(), 创建在堆上的对象:

int main() {
    HeapOnly* ph4 = HeapOnly::CreateObj();
    HeapOnly* ph5 = HeapOnly::CreateObj();

    delete ph4;
    delete ph5;

    return 0;
}

这段代码 就可以编译通过了:

|inline


只能在堆上创建的类, 还有一种实现方式, 就是 删除掉析构函数.

因为 在栈上或全局/静态存储区域上实例化的对象 会在超出作用域时自动调用析构函数自动销毁, 且无法通过 delete 关键字来手动销毁, 所以 删除析构函数 编译器就会禁止在栈上或静态区实例化对象.

3. 只能在栈上创建的类

只能在栈上创建, 也就表示不能使用new. 还有一个细节, 那就是也不能在静态区创建.

那么, 就只能按照上面 只能在堆上创建的类 的方法进行实现.

class StackOnly {
public:
    static StackOnly CreateObj() {
        return StackOnly();
    }

    StackOnly(const StackOnly&) = delete;

private:
    // 构造函数私有
    StackOnly() {}
};

这样此类, 就只能通过 调用static成员函数CreateObj()实例化对象.

传统的实例化方式都不再支持:

int main() {
    StackOnly stO1;
    StackOnly* stO2 = new StackOnly;
    StackOnly stO3(stO1);

    return 0;
}

|inline

只能调用 创建接口:

int main() {
    StackOnly stO1 = StackOnly::CreateObj();
    StackOnly::CreateObj();

    return 0;
}

|inline


其实这里有一些问题:

  1. 为什么要delete掉拷贝构造函数?

    因为, 需要保证类对象只能创建在栈上. 如果不delete掉拷贝构造函数, 就可能这样实例化对象:

    static StackOnly stO2(stO1);

    这样创建的话, 创建出来的对象会在静态区而不是栈区

    所以, 需要delete掉拷贝构造函数

  2. CreateObj()是传值返回, 返回一个对象 不是应该需要调用拷贝构造函数吗? 拷贝构造函数不是删除了吗?

    正常返回一个对象是应该调用拷贝构造函数创建临时对象没错, 就像这样:

    static StackOnly CreateObj() {
    	StackOnly st;
        return st;
    }
    

    这样会尝试去调用拷贝构造, 然后发生错误:

    |inline

    但是, 我们使用的 CreatObj() 是这样返回的 return StackOnly();

    调用构造函数构造匿名对象然后再传值返回. 这个过程 编译器会做一些优化

    这个 构造+拷贝的过程, 会被优化为 构造. 所以 才能正常执行


谈到 只能在栈上创建的类, 还有人会想到 delete掉 类的operator newoperator delete专属重载函数.

因为禁掉之后, 此类就没有办法使用new创建对象了, 但是需要注意一点的是 禁掉了new的使用, 只是不让类对象创建在堆上, 而不是实现让类对象只能创建在栈上

比如, static StackOnly sto, 也没有使用new 但他 也没有在栈上创建, 而是在静态区

4. 不能继承的类

不能继承的类也很简单:

  1. C++98

    class NonInherit {
    public:
        static NonInherit GetInstance() {
            return NonInherit();
        }
    
    private:
        NonInherit() {}
    };
    

    只将构造函数设置为private就可以实现 子类无法实例化对象.

    也可以算是 不能被继承.

    |wide

  2. C++11

    C++11更简单了:

    class NonInherit final {};
    

    只需要一个关键词, 就表示此类是最终的, 无法被继承:

    |wide

5. 单例模式 - 只能创建一个实例对象的类

在介绍单例模式之前, 先解释一下什么是 设计模式

怎么理解设计模式呢? 设计模式 实际是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结

使用设计模式有很多的优点:

首先就是 提供了一种标准化的思考方式, 可以帮助开发者更好地理解、分析和解决软件设计问题

设计模式还可以提高代码的可读性、可维护性和可扩展性, 使软件更易于维护和更新…

被人熟知的设计模式有 23种, 不过本篇文章只介绍一种: 单例模式

单例模式

那么, 究竟什么是单例模式呢?

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

说白了, 单例模式也是一种特殊类: 只能创建一个实例对象的类

不过, 单例模式的实现 根据创建对象的实际不同 一般分为两种方式:

饿汉模式

饿汉模式, 是指 在进程启动时 就创建这唯一的一个实例对象(单例对象). 就像 一个饿极了的人看到食物就去吃一样.

那么, 该如何实现呢?

单例模式, 只能创建一个实例对象.

那么 首先就应该把构造函数设置为private的, 用来防止在外部调用构造函数实例化其他对象.

还要防止通过拷贝构造或者赋值重载创建新的对象.

class Singleton {
private:
    Singleton() {}

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

这样实现了禁止从外部创建新的对象.

为确保整个进程都可以访问到唯一的实例对象, 需要创建一个static成员变量来存储单例对象:

class Singleton {
private:
    Singleton() {}

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    static Singleton _instance; 	// 声明
};

而且, 饿汉模式要求 在进程打开时就创建唯一的实例对象, 所以创建的操作需要在main函数外面实现:

class Singleton {
private:
    Singleton() {}

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    static Singleton _instance; 	// 声明
};

Singleton Singleton::_instance;

为了使整个进程都可以访问到单例对象, 还需要提供一个static成员函数 用来获取单例对象:

class Singleton {
public:
    static Singleton* getInstance() {
        return &_instance;
    }

private:
    Singleton() {}

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton _instance; // 声明
};
// 定义
Singleton* Singleton::_instance;

至此, 一个最简单的饿汉单例模式 就实现了.

介绍到这里, 可能最大的问题就是: static Singleton _instance; 这个静态的成员对象.

为什么可以在类内部定义一个此类的对象?

在内部定义一个此类的对象当然是不行的.

但是这里 用static修饰了, 那么 这个对象本质上就不是类成员的一部分, 而是一个静态的全局对象.

为什么不直接在类外定义, 而是需要现在类内声明一下?

因为 此类的构造函数是私有的, 只有在类内才可以调用. 所以 要把这个对象声明在类内, 这样就可以在类外定义对象.

那么这就是一个简单的饿汉的单例模式, 进程的任何地方, 都可以通过 SingleTon::getInstance() 获取单例对象:

#include <ostream>

class Singleton {
public:
    static Singleton* getInstance() {
        return &_instance;
    }

private:
    Singleton() {}

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton _instance; // 声明
};
// 定义
Singleton Singleton::_instance;

void func1() {
    std::cout << "func1" << std::endl;

    Singleton* singleT4 = Singleton::getInstance();
    Singleton* singleT5 = Singleton::getInstance();

    std::cout << singleT4 << std::endl;
    std::cout << singleT5 << std::endl;
}

int main() {
    Singleton* singleT1 = Singleton::getInstance();
    Singleton* singleT2 = Singleton::getInstance();
    Singleton* singleT3 = Singleton::getInstance();

    std::cout << singleT1 << std::endl;
    std::cout << singleT2 << std::endl;
    std::cout << singleT3 << std::endl;

    func1();

    return 0;
}

|inline

饿汉模式的单例对象, 是在进程没有进入到main函数时就已经创建了的.


声明在类域中的 静态的用来存储单例对象的对象, 还可以以指针的形式声明和定义:

class Singleton {
public:
 static Singleton* getInstance() {
     return _instance;
 }

private:
 Singleton() {}

 Singleton(const Singleton&) = delete;
 Singleton& operator=(const Singleton&) = delete;

 static Singleton* _instance; // 声明
};
// 定义
Singleton* Singleton::_instance = new Singleton;

不过, 对应的getInstance()的返回值也需要变化一下.

懒汉模式

饿汉模式, 是指 在进程启动时 就创建单例对象

那么懒汉模式呢?

我们已经知道了, 单例模式需要在类域内声明一个static类对象或类指针对象来存储单例对象, 还需要一个static成员函数getInstance()来获取单例对象

那么, 懒汉模式, 是指 在首次执行getInstance()函数时, 才创建单例对象

懒汉模式的实现 与 饿汉模式的实现思路大致相同, 只有创建实例单例对象的时机不同:

class Singleton {
public:
    static Singleton* getInstance() {
        if (_instance == nullptr) {
            _instance = new Singleton();
        }

        return _instance;
    }

private:
    Singleton() {}

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* _instance; // 声明
};
// 定义
Singleton* Singleton::_instance = nullptr;

与饿汉 唯一的区别好像就是, 在调用getInstance()时才实例化单例对象.


使用指针类型存储单例对象时, 此时的单例对象是new出来的, new出来的对象必须调用delete才会调用析构函数. 不然进程结束 操作系统会直接释放掉整个进程所使用的内存.

而某些需求是需要单例对象析构时实现的, 如果不使用delete 来调用单例对象的析构函数, 需求就无法完成.

所以, 可以 以RAII思想实现一个内嵌的垃圾回收机制:

class Singleton {
public:
    static Singleton* getInstance() {
        if (_instance == nullptr) {
            _instance = new Singleton();
        }

        return _instance;
    }

    class CGarbo {
	public:
        // CGarbo的析构函数会 delete 单例对象
        // 然后会调用 Singleton的析构函数, 完成需求
		~CGarbo(){
			if (_instance)
				delete _instance;
		}
	};

	// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
	static CGarbo Garbo;
    
private:
    Singleton() {}
    
	~Singleton() {}
    
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* _instance; // 声明
};
// 定义
Singleton* Singleton::_instance = nullptr;
CGarbo Singleton::Garbo;

饿汉与懒汉模式 合适场景

饿汉模式:

适合场景: 如果单例对象 在多线程高并发环境下频繁使用, 性能要求较高, 那么使用饿汉模式可以避免资源竞争, 提高响应速度

不适合场景: 如果单例对象的构造十分耗时 或者 占用很多资源, 比如: 加载插件、初始化网络连接、读取文件等等. 如果这时还用饿汉模式 在程序一开始就进行初始化, 就会导致程序启动时非常的缓慢. 并且, 如果存在多个饿汉式单例模式, 那么单例对象的实例化顺序就会不确定

懒汉模式:

上面 饿汉模式的不适合场景, 就是懒汉模式的适合场景.

适合场景: 如果单例对象的构造十分耗时 或者 占用很多资源, 也没有必要在程序启动时就全部加载, 那就可以使用懒汉模式(延迟加载)更好. 如果有多个懒汉式单例模式, 也可以控制单例对象的实例化顺序.

不适合场景: 但是, 多线程高并发的场景下, 懒汉模式有一个问题就是 可能多线程会同时getInstance(), 如果同时整个进程的第一次执行, 那么可能会造成数据混乱, 所以 实际的 getInstance()实现中 需要加锁.

如果加了锁, 就可能在多线程高并发场景下产生资源竞争, 影响效率. 这算是饿汉模式的小问题.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/746929.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于Javaweb实现ATM机系统开发实战(六)开卡用户登录及其功能实现

首先输入用户名密码&#xff0c;测试一下用户登录功能&#xff0c;跳转到了UserLogin页面&#xff0c;发现404&#xff0c;是因为我们的servlet还没有编写&#xff0c;页面无法进行跳转。 还是老规矩&#xff0c;先写servlet&#xff1a; package com.atm.servlet;import com…

计算机网络实验(4)--配置网络路由

&#x1f4cd;实验目的 了解路由器的特点、基本功能及配置方法&#xff1b;使用模拟软件Packet Tracer 5.3熟悉Cisco路由器的操作&#xff1b;配置静态路由和距离矢量路由协议RIP&#xff0c;实现给定网络的连通&#xff1b;从而加深对IP编址、路由转发机制、路由协议、路由表…

作为一个程序员一定要掌握的算法之遗传算法

目录 一、引言 1.1 目的 1.2 意义 二、遗传算法介绍 2.1 遗传算法的基本思想 2.2 遗传算法与其他算法的主要区别 2.3 基于Java的遗传算法设计思想 三、遗传算法的具体实现 3.1 系统功能模块图和说明 3.2 代码和说明 3.2.1 初始化 3.2.2 选择运算 3.2.3 交叉运算 3…

go语言 Sort包

Sort包 1.常见的类型进行排序 类型功能sort.Float64s([]float64)对float64切片进行升序排序sort.Float64sAreSorted([]float64)bool判断float64切片是否为升序sort.SearchFloat64s([]float64,float64)int在升序切片中查找给定值,找到则返回下标,找不到则返回适合插入值的下标 …

selenium+python做web端自动化测试框架实战

最近受到万点暴击&#xff0c;由于公司业务出现问题&#xff0c;工作任务没那么繁重&#xff0c;有时间摸索seleniumpython自动化测试&#xff0c;结合网上查到的资料自己编写出适合web自动化测试的框架&#xff0c;由于本人也是刚刚开始学习python&#xff0c;这套自动化框架目…

基于 FPGA 的 HDMI/DVI 显示

文章目录 前言一、HDMI 与 DVI 的区别与联系1.1 DVI 接口含义1.2 HDMI 接口含义1.3 HDMI 与 DVI 的区别1.4 HDMI 与 DVI 的兼容性1.5 HDMI 与 DVI 接口对比 二、DVI 数据链路介绍2.1 输入接口层2.2 TMDS 发送器2.3 TMDS 接收器2.4 输出接口层 三、传输原理与实现3.1 TMDS原理3.…

jvm调优工具详解

一、调优工具 先通过jps命令显示Java应用程序的进程id 1、jmap 查看堆实例个数及占用内存大小&#xff0c;把这些信息生成到当前目录下的log.txt文件 jmap -histo 21932 > ./log.txt #查看历史生成的实例 jmap -histo:live 14660 #查看当前存活的实例&#xff0c;执行…

跨浏览器测试的重要性及需要注意的问题

随着互联网的快速发展&#xff0c;人们使用各种不同的浏览器来访问网站。因此&#xff0c;跨浏览器测试变得尤为重要&#xff0c;以确保网站在各种浏览器上都能正常运行和显示。本文将探讨跨浏览器测试的重要性以及需要注意的问题。 一、跨浏览器测试的重要性 随着浏览器的多…

【JAVA】仿顺丰淘宝智能识别信息模块——DidYourTypeItCorrectly

文章目录 题目项目层级结构解答已完成的部分简介未完成的部分概述代码部分DidYourTypeCorrectly.javaFormModel.javaIntelligentRecognition.javaMVCWindow.javaPlaint.java 运行结果截图结语 题目 模拟顺风地址智能识别&#xff0c;对用户输入的信息&#xff0c;包括&#xf…

iOS五大内存分区

我们知道任何一个程序在运行的时候实际是运行在内存中的&#xff0c;这个内存也就是我们通常所说的主存&#xff0c;也叫运行内存&#xff0c;也叫RAM&#xff08;Random Access Memory&#xff09;&#xff0c;是可以直接与CPU进行交换数据的内部存储器。内存读取速度很快&…

【Solr】删除core中的文档数据

推荐使用xml的方式&#xff0c;详情如下所示&#xff1a; &#xff08;清空文档数据&#xff09; <delete> <query>*:*</query> <!-- 示例模糊删除&#xff1a;<query>name:*老六*</query> --> </delete> <commit/>

代码随想录第25天 | * 491.递增子序列 * 46.全排列 * 47.全排列 II

491.递增子序列 自己的做法&#xff1a; /*** param {number[]} nums* return {number[][]}*/let road [];let path [];var findSubsequences function (nums) {road []; //road会有之前的数据&#xff0c;所以需要每次清空roadbrektraning(nums, 0);let obj {};road.for…

springboot校园二手书交易管理系统

本次设计任务是要设计一个乐校园二手书交易管理系统&#xff0c;通过这个系统能够满足乐校园二手书交易的管理员及卖家用户和用户二手书交易信息管理功能。系统的主要功能包括首页、个人中心、用户管理、卖家用户管理、图书分类管理、二手图书管理、求购图书管理、求购回复管理…

复习opencv:螺丝螺纹缺陷检测

螺牙缺陷检测 简述去噪椒盐噪声高斯噪声 小波变换引导滤波求最大凸包判断曲直全部代码 简述 今天收到了一个检测螺牙缺陷的问题&#xff0c;当复习opencv练个手&#xff0c;记录一下基础知识。这里的代码是检测弯曲的&#xff0c;其他缺陷用yolo处理。东家给的图片有的是有干扰…

激活函数》

一. 常用激活函数 1. Sigmoid函数 优点与不足之处 对应pytorch的代码 import torch import torch.nn as nn# Sigmoid函数 print(**25"Sigmoid函数""*"*25) m nn.Sigmoid() input torch.randn(2) print("原&#xff1a;",input) print("结…

RabbitMQ ---- Work Queues

RabbitMQ ---- Work Queues 1. 轮训分发消息1.1 抽取工具类1.2 启动两个工作线程1.3 启动一个发送线程1.4 结果展示 2. 消息应答2.1 概念2.2 自动应答2.3 消息应答的方法2.4 Multiple 的解释2.5 消息自动重新入队2.6 消息手动应答代码2.7 手动应答效果演示 3. RabbitMQ 持久化3…

RT-Thread 互补滤波器 (STM32 + 6 轴 IMU)

作者&#xff1a;wuhanstudio 原文链接&#xff1a;https://zhuanlan.zhihu.com/p/611568999 最近在看无人驾驶的 Prediction 部分&#xff0c;可以利用 EKF (Extended Kalman Filter) 融合不同传感器的数据&#xff0c;例如 IMU, Lidar 和 GNSS&#xff0c;从而给出更加准确的…

Go——基础语法

目录 Hello World&#xff01; 变量和常量 变量交换 匿名变量 常量 iota——特殊常量 基本数据类型 数据类型转换 运算符 算数运算符 关系运算符 逻辑运算符 位运算符号 ​编辑 赋值运算符 输入输出方法 流程控制 函数 可变参数类型 值传递和引用传递 Hello Wor…

性能测试 jmeter 的 beanshell 脚本的 2 个常用例子

目录 前言&#xff1a; Bean Shell 内置变量大全 例子 1 例子 2 技巧 前言&#xff1a; JMeter是一个功能强大的性能测试工具&#xff0c;而Beanshell是JMeter中用于编写脚本的一种语言。 在利用 jmeter 进行接口测试或者性能测试的时候&#xff0c;我们需要处理一些复杂…

使用GithubAction自动构建部署项目

GitHub Actions 是一种持续集成和持续交付(CI/CD) 平台&#xff0c;可用于自动执行生成、测试和部署管道。 您可以创建工作流程来构建和测试存储库的每个拉取请求&#xff0c;或将合并的拉取请求部署到生产环境。 GitHub Actions 不仅仅是DevOps&#xff0c;还允许您在存储库中…