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

|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()实现中 需要加锁.

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python-计算两个矩阵的相似度。

余弦相似度 在pytorch中&#xff0c;有一个专门的函数用于计算相似度&#xff1a;torch.cosine_similarity() https://pytorch.org/docs/stable/nn.functional.html#cosine-similarity import torch import torch.nn.functional as F input1 torch.randn(100, 128) input2 t…

技术讨论:我心中TOP1的编程语言

欢迎关注博主 六月暴雪飞梨花 或加入【六月暴雪飞梨花】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和技术…

使用STM32 再实现感应开关盖垃圾桶

硬件介绍 SG90舵机 如上图所示的舵机SG90&#xff0c;橙线对应PWM信号&#xff0c;而PWM波的频率不能太高&#xff0c;大约50Hz&#xff0c;即周期0.02s&#xff0c;20ms左右。 在20ms的周期内&#xff0c;高电平占多少秒和舵机转到多少度的关系如下&#xff1a; 0.5ms-----0度…

msvcr110.dll丢失的修复教程,找不到msvcr110.dll解决办法哪个更推荐

msvcr110.dll是微软的Visual C运行库文件之一。它是Microsoft Visual Studio 2012的一部分&#xff0c;用于支持运行在Windows操作系统上使用Visual C编写的应用程序。在Windows系统中非常重要&#xff0c;如果丢失或是损坏就会造成很多程序无法启动运行。 会出现以下的报错提…

【云原生】k8s之存储卷

容器磁盘上的文件的生命周期是短暂的&#xff0c;这就使得在容器中运行重要应用时会出现一些问题。首先&#xff0c;当容器崩溃时&#xff0c;kubelet 会重启它&#xff0c;但是容器中的文件将丢失——容器以干净的状态&#xff08;镜像最初的状态&#xff09;重新启动。其次&a…

简单版本视频播放服务器V1

一直想做个家用版本的视频播放器&#xff0c;通过这个可以实现简单的电脑&#xff0c;通过浏览器就是可以访问电脑里面的视频&#xff0c;通过手机&#xff0c;平板等都是可以访问自己的视频服务了 后端代码&#xff1a; package mainimport ("fmt""io/iouti…

JMeter 中 3 种参数值的传递

目录 前言&#xff1a; (一) 从 CSV 文件读取要批量输入的变量 (二) 利用 Cookie 进行值的传递 (三) 利用正则匹配提取上一个接口的返回数据作为下个请求的输入 前言&#xff1a; 在JMeter中&#xff0c;参数值的传递是非常重要的&#xff0c;因为它允许你在测试过程中动态…

Proxy-Reflect使用详解

1 监听对象的操作 2 Proxy类基本使用 3 Proxy常见捕获器 4 Reflect介绍和作用 5 Reflect的基本使用 6 Reflect的receiver Proxy-监听对象属性的操作(ES5) 通过es5的defineProperty来给对象中的某个参数添加修改和获取时的响应式。 单独设置defineProperty是只能一次设置一…

Spring专家课程Day01_Spring-IOC

​ 文章目录 基础配置1)基础文件结构(Maven项目创建) 一、01_Spring概述_IOC_HelloWorld1.Spring框架的两个核心功能1.1) IOC/DI ,控制反转依赖注入!1.2) AOP,面向切面编程 2.IOC的两种模式2.1)配置文件中配置 Bean2.2)配置文件,组件扫描注解类注解Component 二、02_JavaBean_J…

Ajax简介和实例

目录 什么是 AJAX &#xff1f; AJAX实例 ajax-get无参 ajax-get有参 对象和查询字符串的互转 ajax-post ajax-post 表单 AJAX 是一种在无需重新加载整个网页的情况下&#xff0c;能够更新部分网页的技术。 什么是 AJAX &#xff1f; 菜鸟教程是这样介绍的&#xff1a…

本地Linux 部署 Dashy 并远程访问

文章目录 简介1. 安装Dashy2. 安装cpolar3.配置公网访问地址4. 固定域名访问 转载自cpolar极点云文章&#xff1a;本地Linux 部署 Dashy 并远程访问 简介 Dashy 是一个开源的自托管的导航页配置服务&#xff0c;具有易于使用的可视化编辑器、状态检查、小工具和主题等功能。你…

Gradle下载和配置教程:Windows、Mac和Linux系统安装指南

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

扒开 TCP 的外衣,看清 TCP 的本质

TCP 非常重要&#xff0c;它的内容很多&#xff0c;今天只能讲解其中的一部分&#xff0c;但足以让你超越 80 % 的编程开发人员对于 TCP 的认知。 本篇内容非常多&#xff0c;非常干&#xff0c;希望你花点时间仔细研究&#xff0c;我相信会对你有所帮助。 1. TCP 协议是什么…

云之道知识付费V2小程序V3.1.1独立平台版安装使用教程

据播播资源了解&#xff0c;云之道知识付费小程序是一款专注于知识付费的小程序源码&#xff0c;为内容创业者、自媒体和教育培训机构提供全方位的互联网解决方案。 由播播资源小编全套安装云之道知识付费V2独立版系统&#xff0c;系统支持无限多开&#xff0c;相比上几版出现…

Freertos-mini智能音箱项目---IO扩展芯片PCA9557

项目上用到的ESP32S3芯片引脚太少&#xff0c;选择了PCA9557扩展IO&#xff0c;通过一路i2c可以扩展出8个IO。这款芯片没有中断输入&#xff0c;所以更适合做扩展输出引脚用&#xff0c;内部寄存器也比较少&#xff0c;只有4个&#xff0c;使用起来很容易。 输入寄存器 输出寄存…

2021年美国大学生数学建模竞赛D题音乐的影响解题全过程文档及程序

2021年美国大学生数学建模竞赛 D题 音乐的影响 原题再现&#xff1a; 音乐是人类社会的一部分&#xff0c;是文化遗产的重要组成部分。 作为理解音乐在人类集体经验中所扮演角色的努力的一部分&#xff0c;我们被要求开发一种方法来量化音乐进化。 当艺术家创作一首新音乐时&…

运维小知识(二)——Linux大容量磁盘分区及挂载

centos系统安装&#xff1a;链接 目录 1.&#x1f353;&#x1f353;命令格式化磁盘 2.&#x1f353;&#x1f353;大容量硬盘分区 3.&#x1f353;&#x1f353;自动挂载 整理不易&#xff0c;欢迎一键三连&#xff01;&#xff01;&#xff01; 新系统装完之后&#xff0…

Go实现socks5服务器

SOCKS5 是一个代理协议&#xff0c;它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色&#xff0c;使得内部网中的前端机器变得能够访问Internet网中的服务器&#xff0c;或者使通讯更加安全。SOCKS5 服务器通过将前端发来的请求转发给真正的目标服务器&#…

【ubuntu重装系统后的软件配置_memo】

重装系统后系统环境恢复 备份安装系统常用的一些debvscode 更改sourcespip加速爬长城的家伙式儿安装ROS安装cmake安装git安装zsh顺便开个ssh提升幸福感的映射配置neovimplugins字体插件遇到的问题 锁键盘/鼠标小玩意儿 备份 实验时不起眼的图顺手写的脚本忘记从哪儿下载的资源…

#{} 和 ${} 的区别?

一、区别概述 1.1、主要区别&#xff1a; 1、#{} 是预编译处理&#xff0c;${} 是直接替换&#xff1b;2、${} 存在SQL注入的问题&#xff0c;而 #{} 不存在&#xff1b;Ps&#xff1a;这也是面试主要考察的部分~ 1.2、细节上&#xff1a; 1、${} 可以实现排序查询&#xff…