C++相关概念和易错语法(32)(单例模式、类型转换)

news2024/9/21 5:36:25

1.单例模式

(1)设计模式是什么?

简单来说,被反复使用,多数人知晓、经过分类的代码设计经验的总结就叫设计模式,它建立在特殊类的设计之上,实现特殊的功能,运用的知识也十分综合。如迭代器模式就是一种常见的设计模式。下面再分享一个常见的设计模式,单例模式。

(2)单例模式

单例模式:全局中定义的一个类只能创建一个对象,即单例模式可以保证系统中该类只有一个实例,并提供一个访问该对象的全局访问点,该实例化的对象被所有程序模块共享,即能在不同模块访问这个唯一的对象。

单例模式又分为饿汉模式和懒汉模式,下面分别介绍

(3)饿汉模式

先看一下下面的代码,会输出什么呢?


#include <iostream>
using namespace std;

class A
{
public:
	static A& constructor()
	{
		return a;
	}

	A(const A& a) = delete;//移动版本也不会生成
	A& operator=(const A& a) = delete;//移动版本也不会生成

private:
	A(int m = 10)
		:_m(m)
	{
		cout << "私有化A()" << endl;
	}
	~A()
	{
		cout << "私有化~A()" << endl;
	}

	int _m;

	static A a;
};

A A::a(5);

int main()
{
	cout << sizeof(A) << endl;

	return 0;
}

结果是

相信看到这个结果会有很多疑惑,下面将逐一回答

①为什么A的静态成员变量类型是A还没有导致无限递归?

我们先来看看无限递归的情况是怎样的

在这种情况下编译会直接报错,但是为什么加了static就没事了呢?

类中的static函数和变量虽然和其它成员函数和变量都是在同一个类下面声明的,但其实有很大差别。类中的static对象属于整个类而不是单个对象,当实例化类的时候并不会为static对象单独开辟空间。只有当要使用的时候直接到类里面取,我们可以将类里面的static对象当作公共区域,每个实例化对象按需来取。

但使用公共区域来解释类中类又不太严谨。还有一种理解方式可以更好地解释为什么A不会无限递归。我们先忽略所有的静态对象,剩下的就是类A该有的东西。注意我们所忽略的静态成员变量A a中,A对应的内容也是如此,没有静态对象。按照这样理解,我们这个时候就能发现静态成员变量A a中并没有另一个A a,因为A中我们先是忽略了所有静态对象。最后我们将这些忽略的对象还原回去。

这就像在一座房子里有一个很小的房子模型,这个模型非常精妙,包含了这个房子的一切,就是模型里没有另一个房子模型,不然的话就会无限递归了。其实我们再进一步,这个小模型放在房子外面和房子里面都长一样。模型在外面的话那么这个模型和房子就一模一样,造出来的房子也一模一样。模型在房子里面的话房子虽然多了一个小模型,但小模型的样子没有变,而后面我们造房子是按照模型来造的,根本不会受到影响。

这里需要我们仔细体会,找到自己的理解方式。总的来说静态成员变量可以说是一个仅受类域限制的对象,这个对象在类里还是类外都不会对这个类造成任何影响,只有调用时才会感受到影响。

用已有的知识解释下面的现象

我们发现计算类的大小时,静态成员变量都不会算在其中,这也进一步验证了刚刚的解释。

②为什么使用static函数?

关于这点,我在特殊类中也讲过。static函数属于整个类而不是单个对象。类的所有成员函数(包括static)都会在编译或链接时存入代码段。不管实不实例化这些函数都只会有一份,所以static在唯一性上体现不出不同,但像我前面所说,static对象只是受类域限制,其他东西和类没有关系,所以static函数没有this,避免了先有鸡还是先有蛋的问题。

③为什么可以在全局定义类中的私有成员?

要注意私有成员保护的是调用,即不能在类外面调用

只要是成员函数,非静态成员变量都能在全局定义(能够随便调用类里面的私有对象),注意格式,static定义时不写

④为什么什么都不做就会调用A的构造和析构?

static A a在全局中定义也就意味着它的生命周期和全局对象、静态对象一样,在调用main函数前创建,在程序结束时销毁。在开始执行main函数的时候,a已经调用了构造函数创建了。

⑤如何实现单例模式的?

和特殊类一样,我们要根据需求来处理,由于我们需要单例,把这个单例放在类里面作为static成员变量是最好的选择,因为这样我们就可以直接私有化构造函数,将拷贝赋值全部封死,完全失去了任何创建新对象的机会。唯一的一份对象只能通过static来获取,这里其实也借助了static作为成员变量的一个特点。因此我们只能通过接口获取A的唯一的实例化对象,符合单例模式需求。

⑥为什么叫做饿汉模式?

当调用main函数之前,static A a就在全局中定义出来了,也就是说,我们将这个对象准备好了才开始执行main函数的代码。就像亲人在家早早做好饭,饥饿的人一回家就能吃饭。这个代码也是同样的意思,先把这些特殊类的对象准备好,当我需要的时候直接拿来用就可以了。

⑦这个模式有什么缺点吗?

多个饿汉模式的单例会导致启动慢,在main函数前不存在多线程的概念,main函数前的处理也不会有什么反馈,有的时候让人无法分辨到底是启动慢还是程序陷入死循环了。并且某些对象初始化内容较多(有的程序启动要读很多文件)。还有一个致命的问题:A和B两个饿汉,如果对象初始化存在依赖关系,很难控制顺序(A和B在不同文件中),这个时候就需要懒汉模式来处理了。

(4)懒汉模式

理解了饿汉模式,懒汉模式就很简单了。总的来说就是在第一次调用的时候才会创建对象,并且因为能够显式创建对象,创建顺序的问题也能解决。

看一下下面的代码


#include <iostream>
using namespace std;

class A
{
public:
	static A* constructor(int m = 10)//模拟构造函数
	{
		if (a)
			return a;
		return new A(m);
	}

	A(const A& a) = delete;//移动版本也不会生成
	A& operator=(const A& a) = delete;//移动版本也不会生成

private:
	A(int m = 10)
		:_m(m)
	{
		cout << "私有化A()" << endl;
	}
	~A()
	{
		cout << "私有化~A()" << endl;
	}

	int _m;

	static A* a;
};



class B
{
public:
	static B* constructor(int m = 10)//模拟构造函数
	{
		if (b)
			return b;
		return new B(m);
	}

	B(const B& b) = delete;//移动版本也不会生成
	B& operator=(const B& b) = delete;//移动版本也不会生成

private:
	B(int m = 10)
		:_m(m)
	{
		cout << "私有化B()" << endl;
	}
	~B()
	{
		cout << "私有化~B()" << endl;
	}

	int _m;

	static B* b;
};

A* A::a = nullptr;

B* B::b = nullptr;


int main()
{
	B* p1 = B::constructor(0);

	A* p2 = A::constructor(5);

	return 0;
}

结果是

注意堆区的空间回收的时候都不会调用析构,和生命周期结束调用析构存在区别。

懒汉模式依然是借助static变量来写的,只不过先用nullptr初始化保证程序启动快,后续需要的时候先检查是不是nullptr,是的话就创建变量,不是的话就直接用现有的对象返回数据。我们显式调用可以保证严格控制创建对象顺序,并且整体运行速度也有上升。

之所以叫懒汉模式,是因为这类似于饿了才去做饭,需要的时候才去干的生活模式。我们写代码的时候要慎用饿汉模式,懒汉模式在很多方面都有优势。

2.类型转换

在C/C++中,我们难免会遇到类型转换的问题,如int和double之间的转换,short和int之间转换。注意权限的缩小和放大仅限于指针和引用,其余的都叫类型转换类型转换只要是自定义类型都要手动写转换的构造函数。接下来就讲讲C++中类型转换的规则。

类型转换分为隐式类型转换显式(强制)类型转换。后面我会讲哪些场景必须使用强制类型转换。C++有四种显式类型转换:static_cast、reinterpret_cast、const_cast、dynamic_cast

注意:这四个操作符都是以static_cast<int>(a)方式写,但并不是仿函数,它们和仿函数有很大区别

(1)static_cast<T>

①内置类型之间

static_cast<T>适用于大多数类型转换的场景。它必要时能调用构造函数,专门进行安全转换

这种转换方式都能使用隐式类型转换(不意味着隐式类型转换只支持这一种),我们写不写都是能运行的,写了相当于给别人一个更明显的标志。这类转换方式有个很大的特点就是数据的意义不会发生大的改变,如我前面int转short,也可以说是内置类型之间的转换

指针类型和void*之间,这种只能进行强转,不能隐式类型转换

编译器认为像int*和char*之间的转换不够安全,所以就不支持static_cast强转

针对这种有互相关联的类型,我们可以选择不写,走隐式类型转换也是可以的。

 ②内置类型 -> 自定义类型

这类转换我们应该见过很多了,单参数构造函数支持隐式类型转换

如果不想隐式类型转换可用explicit,但显式转换不受影响

多参数比较特殊,它不支持使用static_cast强转

多参数构造要么直接写()构造函数,要么使用列表初始化,static_cast识别不了列表,不能使用

③自定义类型 -> 内置类型

这应该我第一次提到用自定义类型转内置类型。如果有一个A a,我想转为int,那么我应该会写作int b = (int)a,由于这个(int),我们应该写一个重载函数出来,C++中这个重载函数是operator int,其实operator()也很合理,但这有弊端,不仅和仿函数的冲突了,还无法确定类型。因此重载一个int函数其实也很清晰。

重载函数形式:operator 类型

我们看到,a直接隐式转换成了int,因为我们写了重载函数,cout的operator<<函数又有int版本,于是就能隐式类型转换

但如果写两个重载就不明确了,会报错


自定义类型转内置类型一般建议强转,这样至少看上去一目了然。

④自定义类型之间

我们只需要在类中写对应的构造函数即可,会自动匹配上的。

⑤父子类转换

继承体系下,父子类指针都能进行互指,父类指针可以指向子类,子类也能指向父类

如果不是指针,仅支持父类指向子类的空间,下面图中a2不是赋值兼容转换,而是强制类型转换(效果一样)

⑥非指针内置类型的const与不带const的对象转换

这只是赋值操作,不涉及什么权限等,了解即可

(2)reinterpret_cast<T>

和运算符的名字一样,reinterpret,重新解释。也就是说使用这种类型转换的操作数在转换前后的意义是有一个比较大的转变的,需要我们慎重考虑使用,它并不安全。

不仅如此,reinterpret_cast<T>并不聪明,它不会调用构造也不会截断数据,一般只负责按位重解释,改变某个空间的解释含义,这也导致它的使用场景很低。

①任意指针之间转换

reinterpret_cast可以强转static_cast不能强转的对象。

我们需要强制类型转换,可以使用(int*)p这种方式,也可以使用reinterpret_cast来操作

②指针和内置类型的转换

前面说的,reinterpret_cast<T>适合不改变数据改变它的解释的操作,这就使得我们可以在指针和内置类型之间做转换。


一般来说不建议使用reinterpret_cast<T>,它经常做出一些未定义的事

(3)const_cast<T>

const_cast是用来给const修饰的指针或引用进行强转的。注意const修饰的和无const修饰的对象是不能通过这个强转的(const修饰的迭代器之间转换也是如此),必须要写转换的构造函数,由static_cast转换。

这个结果说明了const的风险,有的编译器在编译阶段会像宏替换那样将部分变量直接替换成常量,就算对应空间的值被修改,也不会看出来

(4)dynamic_cast

这是专门针对有虚函数的继承体系的更安全的转换方式(仅指针和引用)。支持RTTI(运行时类型识别),如typeid、dynamic_cast、decltype都有,dynamic_cast通过检查虚函数表里面的标识,以此来判断是父还是子

dynamic_cast会自动根据虚函数表判断谁指谁,如果是子类指向父类返回空,可以用这个来进行安全检查

向下转型(父类开辟的空间交给子去管理):编译器认为不安全,返回空

向上转型:如果显式写dynamic_cast,就是强制类型转换,否则就是赋值兼容转换

我们可多方位验证不安全时返回的是空

我们平时很少自己显式写出这四个操作符,理解各自的用途即可。

相比之下,知道什么时候该自己去写转换函数(const迭代器转换),怎么写,以及知道什么时候是隐式类型转换,什么时候是赋值兼容转换。

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

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

相关文章

协议集合(学习笔记)

按照数据的传送方式&#xff0c;通信协议可分为以下2种。 串行通信&#xff1a;串行&#xff08;Serial&#xff09;指的是逐个传输数据位&#xff0c;一次只传输一个位。 并行通信&#xff1a;并行&#xff08;Parallel&#xff09;指的是同时传输多个数据位&#xff0c;一次…

VMware 中 kali Linux的安装与使用

文章目录 前言 一、安装虚拟机 二、使用步骤 总结 前言 随着信息技术的飞速发展&#xff0c;虚拟化技术已经成为现代企业和个人用户不可或缺的一部分。通过虚拟化技术&#xff0c;我们可以在一台物理计算机上运行多个独立的操作系统和应用程序&#xff0c;从而实现资源的高效利…

基于WiFi的智能照明控制系统的设计与实现(论文+源码)

1系统方案设计 本设计智能照明控制系统&#xff0c;结合STM32F103单片机、光照检测模块、显示模块、按键模块、太阳能板、LED灯模块、WIFI模块等器件构成整个系统&#xff0c;在功能上可以实现光照强度检测&#xff0c;并且在自动模式下可以自动调节照明亮度&#xff0c;在手动…

【spring】例子2:mvc web开发

领域类 开发时编译时用lombok提供支持 最终生成的包里不包含lombok

【Android】程序开发组件—探究Jetpack

引言 Jetpack是一个开发组件工具集&#xff0c;它的主要目的是帮助我们编写出更加简洁的代码&#xff0c;并简化我们的开发过程&#xff0c;在这么多的组件当中&#xff0c;最需要我们关注的其实还是架构组件&#xff0c;接下来就对Jetpack的主要架构组件进行学习&#xff01;…

数据结构-----栈、队列

一、栈 1、栈(stack)是限定仅在表尾进行插入和删除操作的线性表。 把允许插入和删除的一端称为栈顶&#xff08;top)&#xff0c;另一端称为栈底&#xff08;bottom)&#xff0c;不含任何数据元素的栈称为空栈。栈又称为后进先出&#xff08;Last In First Out)的线性表,简称L…

OpenAI gym: Trouble installing Atari dependency (Mac OS X)

题意&#xff1a; 使用OpenAI Gym库时&#xff0c;安装Atari环境可能会遇到一些依赖问题&#xff0c;尤其是在Mac OS X系统上 问题背景&#xff1a; Im new to OpenAI gym. Ive successfully installed OpenAI gym on my Mac OS X (High Sierra 10.13.3) laptop and made a D…

C语言程序设计(算法的概念及其表示)

一、算法的概念 一个程序应包括两个方面的内容: 对数据的描述:数据结构 对操作的描述:算法 著名计算机科学家沃思提出一个公式: 数据结构 +算法 =程序 完整的程序设计应该是: 数据结构+算法+程序设计方法+语言工具 广义地说,为解决一个问题而采取的方法和步骤…

学不会虚拟列表?10分钟带你实现高度固定的Vue虚拟列表方案及原理

前言 本文主要介绍长列表的一种优化方案&#xff1a;虚拟列表。本文主要是对传统的虚拟列表方案进行更加详尽的刨析&#xff0c;以便我们能够更加深入理解虚拟列表的原理。 虚拟列表目录 1、为什么需要使用虚拟列表2、什么是虚拟列表与懒加载的区别(重要) 3、实现思路4、通过节…

企业选ETL还是ELT架构?

作为数据处理的重要工具&#xff0c;ETL工具被广泛使用&#xff0c;同时ETL也是数据仓库中的重要环节。本文将从解释ETL工具是怎么处理数据&#xff0c;同时介绍ELT和ETL工具在企业搭建数据仓库的重要优势。 一、什么是ETL? ETL是Extract-Transform-Load的缩写&#xff0c;将…

【深度学习】多层感知机的从零开始实现与简洁实现

可以说&#xff0c;到现在我们才真正接触到深度网络。最简单的深度网络称为多层感知机。 多层感知机由多层神经元组成&#xff0c;每一层与它的上一层相连&#xff0c;从中接收输入&#xff1b;同时每一层也与它的下一层相连&#xff0c;影响当前层的神经元。 和以前相同&…

【深入解析】AI工作流中的HTTP组件:客户端与服务端执行的区别

在当今快速发展的技术环境中&#xff0c;AI工作流的设计和实现变得愈发重要。尤其是在处理HTTP组件时&#xff0c;前端执行与后端执行之间的区别&#xff0c;往往会对系统的安全性和数据的准确性产生深远的影响。今天&#xff0c;我们就来深入探讨这一话题&#xff0c;揭示前端…

音频基础学习四——声音的能量与分贝

文章目录 前言一、能量与分贝1.音频能量2.分贝3.两者的区别4. 应用场景 二、分贝的计算方式1.具体数学公式2.具体算法示例3.对于算法的释义大小端为什么通过计算得到的是负值范围实际结果 总结 前言 很多博客中经常会把声音的能量和分贝说成是一个东西&#xff0c;这种说法是错…

原型模式prototype

此篇为学习笔记&#xff0c;原文链接 https://refactoringguru.cn/design-patterns/prototype 能够复制已有对象&#xff0c; 而又无需使代码依赖它们所属的类 所有的原型类都必须有一个通用的接口&#xff0c; 使得即使在对象所属的具体类未知的情况下也能复制对象。 原型对…

云计算之云原生(上)

目录 一、消息队列RocketMQ 1.1 功能介绍 1.1.1 业务消息首选&#xff1a;消息队列 RocketMQ 1.1.2 【收发流量隔离约束】读写分离控制提高集群稳定性 1.1.3 【Dashboard 仪表盘】实时观测实例状态 1.1.4 【消息轨迹追踪】消息生命周期状态一目了然 1.1.5 【实时扩缩容】…

单点登录OAuth2.0

OAuth 2.0&#xff08;Open Authorization 2.0&#xff09;是OAuth协议的第二个版本&#xff0c;于2012年正式成为RFC 6749标准。在OAuth 2.0之前&#xff0c;OAuth 1.0版本已经为Web应用提供了一定程度的授权功能&#xff0c;但随着时间的推移&#xff0c;这些版本逐渐显露出一…

“Docker网络模式详解与应用“

目录 前言 Docker内置网络 bridge 基本概念 案例 工作原理 使用场景 host 基本概念 案例 工作原理 使用场景 none 基本概念 案例 &#xff01;&#xff01;&#xff01;大佬救命 container 基本概念 案例 自定义网络 自定义bridge 基本概念 案例 Docker…

界面控件KendoReact中文教程 - 如何创建动态进度条?

Kendo UI致力于新的开发&#xff0c;来满足不断变化的需求。现在我们非常自豪地宣布&#xff0c;通过React框架的Kendo UI JavaScript封装来支持React Javascript框架。Kendo UI for React能够为客户提供更好的用户体验&#xff0c;并且能够更快地构建更好的应用程序。 KendoR…

树莓派外设驱动WiringPi库

树莓派外设驱动WiringPi库 文章目录 树莓派外设驱动WiringPi库一、树莓派安装WiringPi库二、WiringPi库的使用方法 一、树莓派安装WiringPi库 wiringPi库其实已经很熟悉了&#xff0c;在香橙派中大量使用过&#xff0c;这个库中集成了很多使用的功能性函数&#xff0c;树莓派安…

设计模式-行为型模式-状态模式

1.状态模式的定义 允许一个对象在其内部状态改变时改变他的行为&#xff0c;用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题&#xff0c;状态模式将一个对象的状态从该对象中分离出来&#xff0c;封装到专门的状态类中&#xff0c;使得对象的状态可以灵活变化&…