嵌入式全栈开发学习笔记---C++(多态)

news2024/11/13 12:06:28

目录

多态polymorphic

多态成立的三个条件

1、要有继承

2、要有虚函数重写

3、用父类指针(父类引用)指向子类对象

重载与重写区别

动态联编和静态联编

多态原理

构造函数中调用虚函数能否实现多态?

用父类指针指向子类数组

虚析构函数

动态类型识别

第一种方法:自定义类型

第二种方法:dynamic_cast动态转换

第三种方法:typeid获取类型信息

抽象类

抽象类注意事项

抽象类在多继承中的使用

纯虚函数的应用

多态案例


上节学习了继承和派生,这节开始学习多态!

多态polymorphic

所有的面向对象编程都具备这几个特征:抽象、封装、继承、多态

多态是面向对象编程的最后一个特征。

多态可以理解为多种形态。

先看这样一个代码:

很多人觉得通过这个p调用的是派生类Child里的show,但结果调用的是基类里面的show

为什么?

这就涉及到静态联编,编译的时候编译器发现p是基类类型,所以通过p调用的就是基类里面的show。

也就是说在编译期间(还不是运行的时候)的链接过程(编译分为四个步骤:预处理,编译,汇编和链接,这个在C语言的时候写过了,忘记的自己去翻翻看)中,直接把p->show()后面的这个“show”符号链接成基类的show函数的地址。

这就是所谓的静态联编,“静态”指的是在编译的时候,运行是属于“动态”,联编就是编译与链接。

但是这显然不是我们想要的结果,所以我们需要做一点修改,在基类的show函数前加上“virtual”。

派生类里面这里加不加都行

加上之后结果就不一样了,调用的是派生类里面的show

此时用基类指针指向基类对象,调用的就是基类里面的show

所以多态可以理解为:同一条语句有不同的执行结果,取决于指针指向谁。

这也就是一开始我们说的:“所谓多态就是多种形态”。

之前我们讲过一个概念叫“隐藏”,比如基类和派生类里面函数同名时,会把派生类继承过来的同名函数隐藏掉,所以调用的时候默认调用的是派生类里面的这个同名函数。

现在我们来学习一下“重写”的概念。

多态成立的三个条件

这就涉及到多态成立的三个条件:

1、要有继承

2、要有虚函数重写

虚函数就是刚刚被我们用virtual修饰的这个成员函数

所谓虚函数重写的意思是一定基类里面有这个虚函数

在派生类里面的这个show前面可以加virtual,也可以不加。而且这两个show的原型必须是一致的,一样的返回值,参数个数是也是一致的。

注:加上这个virtual之后,之前讲的“隐藏”的概念还是成立的。

3、用父类指针(父类引用)指向子类对象

多态一定是跟指针有关系。

以上三个条件只要缺少一个就不叫多态。

重载与重写区别

如果派生类定义了和基类相同原型的函数会怎么样?编译器支持这种写法,并且叫做函数重写。

函数重写。

函数重写就是在派生类中定义与基类原型相同的函数。函数重写只发生在派生类和基类之间。

重载与重写区别:

重载:同一个作用域;

          派生类无法重载基类函数,基类同名函数将被覆盖;

重载是在编译期间根据参数类型和个数决定;

         

重写:发生于基类、派生类之间;

          父类和子类函数有相同的函数原型;

          使用virtual关键字声明后能够产生多态;

          运行期间根据具体对象类型决定调用的函数。(因为new申请的空间是动态内存)

也就是说使用了virtual后,这条语句中p到底指向谁在编译期间(静态)编译器是不知道的,只有在运行的时候,这条语句真的执行了,才在堆空间申请一块内存,然后p才知道指向的是派生类对象,然后才调用派生类的函数,所以加上virtual后,这里属于动态联编。

动态联编和静态联编

1、联编是指一个程序模块、代码之间互相关联的过程。

2、静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。

    重载函数使用静态联编。

3、动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)switch 语句和 if 语句是动态联编的例子(比如得在运行时才能获取键值决定走哪条分支的情况)

C++与C相同,是静态编译型语言;

在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象;

由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象

从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。

多态原理

当类中声明虚函数时,编译器会在类中生成一个虚函数表。虚函数表是一个存储类成员函数指针的数据结构。虚函数表是由编译器自动生成与维护的。

virtual成员函数会被编译器放入虚函数表中,当存在虚函数时,每个对象中都有一个指向虚函数表的指针(C++编译器给父类对象、子类对象提前布局vptr指针;当进行函数调用时,C++编译器不需要区分子类对象或者父类对象,只需要再base指针中,找vptr指针即可。)

VPTR一般作为类对象的第一个成员 。

直接上代码理解:

我们来研究一下Parent这个类占几个字节,现在这个类是个空类,在内存中空类是占一个字节,就只有一个占位字符在里面

那如果有了多态在里面,如下面这段代码,Parent这个类占几个字节?

结果是占了4个字节

所以其实多态就是靠这4个字节实现的,这4个字节是一个指针(我这个是32位系统,如果是64位系统,指针占8个字节)。而且这个指针是存放在类的前面4个字节。

接下来我们在每个类中加上一个成员变量再看看这两个类的大小

看看一下地址分布情况

内存布局:

这个指针我们就称为虚函数表指针。

凡是有虚函数的类,都会有一个虚函数表(里面保存的是函数的地址)

接下来我们以这个代码为例讲解多态的实现原理,从这两句代码开始讲起

首先p指向了Child,

接下来通过p指向show函数的步骤是:先通过p找到Child的对象,访问对象的前4个字节里面的虚函数指针,虚函数指针里面存放的是0x700,是Child虚函数表的地址。

然后再通过虚函数表找到show这个函数的地址是0x200,然后程序才跳到show函数所在空间,然后执行show。

注意:在多态中,子类的重写的show前面加不加virtual都一样,都是虚函数,子类继承了父类的虚函数和虚函数表,按理来说Child虚函数表里面应该是有一个0x100,但是被隐藏了,非要调用,函数名前面可以加作用域。

虽然把所有成员函数都声明为虚函数,也就是都加上Virtual也没事,但是不要把所有的成员函数声明成虚函数,因为这样调用的效率太低了。

比如父类中有个成员函数print,子类中没有这个函数的重写,但是我们也把virtual加在print前面让它成为虚函数,这样编译也不会报错,但是不建议这样做。

那接下来我们让基类指针指向基类对象的话,实现原理又是怎样的?

此时p指向parent的对象的前4个字节,里面的虚函数指针存放的parent虚函数表的地址,根据这个地址找到虚函数表,然后再根据虚函数表上的写的show的地址找到show,再执行。

以上就是多态实现的原理。

构造函数中调用虚函数能否实现多态?

构造的顺序是先构造父类、再构造子类

当调用父类的构造函数的时候,虚函数指针vfptr 指向父类的虚函数表;

当父类构造完,调用子类的构造函数的时候,虚函数指针 vfptr 指向子类的虚函数表

结论:构造函数中无法实现多态

直接上代码理解

这里是基类指针指向派生类对象,之前我们说过,创建派生类对象的时候,再初始化时要先调用基类的构造函数初始化,再调用派生类的构造函数。所以在这段代码中创建派生类对象后,会调用Parent里面的无参构造函数Parent(),而这个无参构造函数里面调用了虚函数show,那此时这个show是子类里面的show还是基类里面的show?

结果是调用父类Parent里面的show。

为什么这里不会调用子类的show?

因为调用基类构造函数的时候,派生类对象还没有形成,不存在多态。因为多态的形成一定是通过它最前面的那个指针实现的。

用父类指针指向子类数组

指针也是一种数据类型,C++类对象的指针p++/--,仍然可用。

指针运算是按照指针所指的类型进行的。

父类p++与子类p++步长不同;不要混搭,不要用父类指针++方式操作子类对象数组

直接上代码

也可以通过指针p来访问数组吗?

结果是不可以

因为成员变量形成不了多态,编译器发现p是Parent*类型,当时基类里面存在b。

那是否可以通过这个指针p来访问函数呢?

结果是不可以

首先p[2]是数组c的第三个元素,是一个子类对象,调用这个子类对象中的show()函数就出现了段错误。

结论就是:不要用基类指针指向派生类数组,因为步长不一样。

首先一个派生类对象的大小是4104个字节,c[2]是c这个地址往后走了4104*2个字节,而一个parent基类的大小是8个字节,p是parent*类指针,p[2]是p这个地址向后走了8*2个字节,也就是说c[2]和p[2]根本不是一个意思。P[2]就相当于是p+2,如果我们对p+2用*取值的话,取出来的就不是一个合法的对象了,再如果我们想通过p[2]去访问show的话,它根本就找不到这个show在哪,可能访问了一个非法的地址,程序就出现了段错误。

虚析构函数

在什么情况下应当声明虚函数?

构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数

析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象

虚析构函数:通过父类指针释放子类对象

注:之前我们讲过普通成员函数的隐形参数this指针,现在再补充一点,在 C++ 中,this 是一个指向当前对象的指针,我们可以通过 this 来访问当前对象的所有成员。this 实际上是当前类类型的指针,例如,对于类 Box 的成员函数,this 是 Box* 类型的指针。

this 指针可以在类的所有非静态成员函数中使用,包括构造函数和析构函数。我们可以使用 this 指针来访问成员变量和成员函数。

直接上代码讲解:

以上这段代码没有函数重写,所以不涉及多态,因此属于静态联编。当通过p访问show的时候,编译器发现p是Person类型,而Person里面没有show,所以编译就报错了。

因此我们要在Person里面加上show函数并且声明成virtual,这样才能构成虚函数重写

这一次它就能把这个学生信息打印出来了,因为已经构成了多态。

但是你会发现还有一个问题没有解决,那就是它只调用了构造函数,没有调用析构函数。也就是我们只申请了空间,但是没有释放空间。

那么我们直接在main函数中手动释放空间可以吗?

结果还是不可以

因为这里delete p释放的是Person*类型的指针,它只会调用Person里面的析构函数。

怎么办?

我们可以把Person里面的析构函数写出来,然后声明为虚析构函数

这样就可以了,为什么?

因为虚析构函数可以通过基类指针释放派生类对象。

也就是说我们这里写的是delete p,但是编译器会调用p指向的那个对象(派生类对象)的析构函数。

所以基类里面的析构函数我们尽量写成虚析构函数(派生类里面的析构函数加不加virtual都可以)

那构造函数要不要也加上virtual变成虚函数呢?

结果报错了

结论:构造函数不能被声明为虚函数。

因为调用构造函数的时候,对象还没有初始化完成,对象的最前面的4个字节是不是指向虚函数表还不一定。

接下来再看一个跟多态相关的问题

动态类型识别

直接上代码讲解

在这段代码中,现在如果把test(&p)改成test(&c)的话,那test函数的参数要改成Child*类型吗?

其实是不需要的,有了多态之后,这里不需要改成Child*类型。

因为在多态里面,基类指针可以指向基类对象,也可以指向派生类对象,而且指向不同的对象就调用不同的成员函数。

所以当我们有很多的派生类,不知道派生类的具体类型的情况下,我们就可以懵懂地定义一个基类指针,反正运行的时候它会根据指针指向谁就调用谁的成员函数,这就是多态的好处和意义。

现在我们在test函数里面将p强转成child*类型,然后赋值给c,再通过c访问派生类里面的数组b的最后一个元素。

这段代码目前编译正常,但是我们分析一下就有问题了,一开始test传的是基类对象p的地址,然后基类指针p指向的是基类的对象p,但是在test函数体内,我们将基类指针p强转为child*类型(此时编译器看c->b[1024000-1]=1;这句里面的c的确是有数组b,所以语法上就通过了,所以编译没有问题),但是p指针里面的值是没有改变的,也就是说p指针里面存的还是基类对象p的地址,当Child*c=(Child*)p;这句一旦执行完毕,就是派生类指针c指向了基类对象p,而在基类里面没有数组b,这时如果我们通过c访问数组b的话,就出现了段错误。

结论:强制类型转换有的时候能转换,有的时候不能转换,如果p指向派生类对象,可以转换,如果指向基类对象,就不能转换。

要想拯救这段代码,那就得用多态来解决。

C++为了能够在运行时正确判断一个对象确切的类型,加入了RTTI(Run-Time Type Identification)。

RTTI提供了以下两个非常有用的操作符:

 

(1)typeid操作符,返回指针和引用所指的实际类型。

 

(2)dynamic_cast操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用。

 

三种办法可以解决C++动态类型识别问题

第一种方法:自定义类型

C++如何得到动态类型?

C++中的多态根据实际的对象类型调用对应的函数

1、可以在基类中定义虚函数返回具体的类型信息

2、所有的派生类都必须实现类型相关的虚函数

3、每个类中的类型虚函数都需要不同的实现

使用虚函数进行动态类型识别的缺陷

1、必须从基类开始提供类型虚函数

2、所有派生类都必须重写类型虚函数

3、每个派生类的ID必须唯一

直接上代码讲解:

刚刚说过如果指针p指向派生类对象,可以转换,如果指向基类对象,就不能转换。这主要取决于指针p到底指向的是谁,因为它指向谁就调用谁的函数,那要想实现这个效果,我们就得形成多态。

以下这段代码中想形成多态就差函数重写这一个条件

那虚函数我们要怎么写呢?既然说自定义类型,那我们用数字表示也可以,直接用枚举

既然是虚函数重写,我们就每个类都写一个getID函数

那在这种情况下只有getID返回1的情况可以强转

如果换成这样就是不能强转

所以我们就可以通过一个数字ID的方式来判定指针到底指向的是谁,进而我们就知道这个类型到底能不能强转。

既然是自定义,那么我们不一定要把这个“ID”定义成枚举类型,也可以定义成int类型。

最后这里是可以强转成这样的:

虽然这种方法能有效解决问题,但是我们需要写很多辅助代码,比较麻烦。

第二种方法:dynamic_cast动态转换

dynamic_cast是C++里面强转的关键字

新的关键字 dynamic_cast

1、dynamic_cast是C++中的新型关键字

2、dynamic_cast用于基类和派生类之间的转换

3、dynamic_cast要求使用的目标类型是多态

  即要求所在类族至少有一个虚函数

  用于指针转换时,转换失败返回空指针

  用于引用转换时,转换失败将引发bad_cast异常

dynamic_cast的优势

1、不用显示的声明和定义虚函数

2、不用为类族中的每个类分配类型ID

dynamic_cast的缺陷

1、只能用于有虚函数的类族

直接上代码讲解:

在C语言里面这样强转的,但是这个什么时候能转什么时候不能转我们不知道,dynamic_cast就可以解决这个问题

dynamic_cast强转

如果p这个指针原本指向的就是Chinese的对象的话,那么这条语句才会强转成功,否则强转失败,失败返回空指针,所以我们只要判断指针c是不是空的。

注意,dynamic_cast所强转的类型的类里面必须要有虚函数才行,也就是能形成多态的地方才允许这么使用。

第三种方法:typeid获取类型信息

如果获取一个变量的类型?

C++提供了typeid关键字用于动态获取类型信息

1、typeid关键字返回对应参数的类型信息

2、typeid关键字返回一个type_info类对象的常引用(别名))

  当typeid参数为NULL时,抛出bad_typeid异常

3、type_info类的使用需要包含typeinfo头文件

直接上代码讲解

打印出来的结果并不是完整的类型表示符,而是这个类型在编译器里面的编号,比如int就i,char就是c,float就是f,我们自定义的三个类型前面加了数字。不同的编译器打印出来可能不一样。

现在我们用typeid来解决我们之前的问题

这里能不能强转就取决于*p是什么类型,因为p指向一个对象,*p就是一个对象。

再来看一下多态里面的一个概念

抽象类

纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本;

纯虚函数为各派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)

纯虚函数的说明形式:

virtual 类型 函数名(参数列表) = 0;

一个具有纯虚函数的类称为抽象类。

直接上代码:

抽象类示例:图形、圆、矩形

这段看上去没毛病,可是运行后的结果是

因为目前这个代码没有构成多态,因为没有虚函数重写,没有构成多态,所以只是静态联编的过程。

基类指针指向派生类对象我们这里写的是没毛病的

但是编译通过ps去Shape里面找的时候发现没有get_s这个函数,所以就报错了。

所以我们要把虚函数写上,构成虚函数重写,形成多态

但是由于Shape这个类表示的只是很多图形的大类,没有具体的图形,所以不好在get_s()函数中写具体的函数体,按按理这个get_s在基类里面就不应该存在,但是为了形成多态,它又必须存在,怎么办?

如果我们就这样空着函数体,编译就会报错,因为它比如有返回值,返回double类型,如果空着函数体,就会默认返回int类型

我们可以这样写:

这种为了形成多态不得不写的函数,而函数体又没法写,那么可以直接让它等于0(它既然不得不存在,那就让它等于0,留一个接口给它,类似于声明),我们称这个函数为纯虚函数。

含有纯虚函数的类,我们称为抽象类;抽象类派生出来的类可以创建对象。

这样写就没有问题了

此时如果我们通过基类指针创建一个基类对象,编译就会报错

结论:抽象类不能创建对象

因为如果我们创建了一个抽象类的基类对象,之后它想要调用get_s函数的时候,根本没办法执行,因为get_s是一个纯虚函数

完整代码:

#include <iostream>

using namespace std;

class Shape
{
protected:
	int m_a;
	int m_b;
public:
	Shape(int a,int b=0)//如果是圆的话只需要半径一个参数,所以b设为默认参数
	{
		m_a=a;
		m_b=b;
	}
	virtual double get_s()=0;  //纯虚函数
};

class Rectangle:public Shape
{
public:
	Rectangle(int a,int b):Shape(a,b)  //创建Rectangle的对象时,调用Rectangle构造函数,然后调用Shape构造函数初始化,这里将a和b传给Shape()
	{
		
	}
	double get_s()//计算矩形面积
	{
		return m_a*m_b;
	}
};

class Circle:public Shape
{
public:
	Circle(int r):Shape(r)//创建Circle这个类时,传一个参数r给Shape,Shape的另外一个参数默认是0
	{

	}
	double get_s()
	{
		return m_a*m_a*3.14;
	}
	
};

int main()
{
	Shape *ps=new Rectangle(1,2);
	cout<<ps->get_s()<<endl;
	delete ps;

	ps=new Circle(1);//ps被释放后重新申请一块内存给它
	cout<<ps->get_s()<<endl;
	delete ps;
	
	//ps=new Shape(1,2);//抽象类不能创建对象
	//ps->get_s();

	return 0;
}

抽象类注意事项

1、抽象类不能用于直接创建对象实例,可以声明抽象类的指针和引用;

2、可使用指向抽象类的指针支持运行时多态性

3、派生类中必须实现基类中的纯虚函数,否则它仍将被看作一个抽象类

抽象类在多继承中的使用

C++中没有Java中的接口概念,抽象类可以模拟Java中的接口类。(接口和协议)

工程上的多继承

被实际开发经验抛弃的多继承

工程开发中真正意义上的多继承是几乎不被使用的

多重继承带来的代码复杂性远多于其带来的便利

多重继承对代码维护性上的影响是灾难性的

在设计方法上,任何多继承都可以用单继承代替

纯虚函数的应用

C++中没有接口的概念

C++中可以使用纯虚函数实现接口

接口类中只有函数原型定义。

实际工程经验证明

多重继承接口不会带来二义性和复杂性等问题  

多重继承可以通过精心设计用单继承和接口来代替

接口类只是一个功能说明,而不是功能实现。

子类需要根据功能说明定义功能实现。

多态案例

公司员工工号从1号开始,每来一个员工工号加1;

总经理底薪10000;

工程师每小时100元;

销售月薪是销售额的10%;

销售经理底薪5000+部门销售额的5%;

用代码表示每个人每月的薪资。

把继承和多态的一些知识点综合起来练习一下

完整代码:

Employee.h

#ifndef EMPLOYEE_H
#define EMPLOYEE_H

//抽象基类
class Employee
{
protected:
	int id;//工号
	static int num;//员工的个数
public:
	Employee();//可在外部实现
	virtual void print_salary()=0;//纯虚函数在派生类中实现
	virtual ~Employee();//因为最后要通过基类指针释放派生类对象,所以要虚析构函数
};

//总经理
class Manager:virtual public Employee
{
protected:
	int base;//基本工资
public:
	Manager(int b);//把基本工资传过来初始化
	void print_salary();//计算并打印工资
};

//工程师
class Engineer:public Employee
{
private:
	int hour;//工时
public:
	Engineer(int h);//把工时传过来初始化
	void print_salary();//计算并打印工资
};

//销售
class SalePerson:virtual public Employee
{
protected:
	int sales;//销售额
	static int sum;//总的销售额
public:
	SalePerson(int m=0);//把销售额传过来初始化;创建销售经理对象时要调用这个构造函数,但是销售经理不需要传销售额,所以把m设为默认参数
	void print_salary();//计算并打印工资
};

//销售经理
class SaleManager:public Manager,public SalePerson  //使用Base和sum;为了让id只继承一份过来,所以把上面Manager的SalePerson改成虚继承Employee
{
public:
	SaleManager(int b);//把基本工资传过来初始化
	void print_salary();//计算并打印工资
};



#endif

Employee.cpp

#include "employee.h"
#include <iostream>

using namespace std;

int Employee::num=0;//静态成员变量在全局初始化,它比主函数先执行
int SalePerson::sum=0;//总的销售额


Employee::Employee()
{
	num++;//人数累计,静态成员变量不能被构造函数的this指针访问,因为它是共享的变量,不属于这里
	this->id=num;//用数字当工号
}

Employee::~Employee()
{
	
}

Manager::Manager(int b)
{
	this->base=b;
}

void Manager:: print_salary()
{
	cout<<"职位:经理    工号:"<<this->id<<"   工资:";//先不换行
	cout<<base<<endl;//换行
}

Engineer::Engineer(int h)
{
	hour=h;
}

void Engineer::print_salary()
{
	cout<<"职位:工程师    工号:"<<this->id<<"   工资:";//先不换行
	cout<<100*hour<<endl;//换行
	
}

SalePerson::SalePerson(int m)
{
	sales=m;
	sum+=sales;
}

void SalePerson::print_salary()
{
	cout<<"职位:销售    工号:"<<this->id<<"   工资:";//先不换行
	cout<<sales*0.1<<endl;//换行
}

SaleManager::SaleManager(int b):Manager(b)  //派生类的对象要先调用基类构造函数初始化,所以用初始化列表给基类传参
{
	//base=b;
}

void SaleManager::print_salary()
{
	cout<<"职位:销售经理    工号:"<<this->id<<"   工资:";//先不换行
	cout<<base+sum*0.05<<endl;//换行
}

Main.cpp

#include "employee.h"
#include <iostream>
#include <time.h>
#include <stdlib.h>

using namespace std;

int main()
{
	//要创建20个员工
	Employee *e[20]={0};//基类指针指向派生类对象
	int idx=0;//对象数组元素下标

	//创建一个总经理
	e[idx++]=new Manager(10000);

	srand(time(NULL));
	//创建10个工程师,工时随机
	for(int i=0;i<10;i++)
	{
		e[idx++]=new Engineer(rand()%100+120);//工时范围120-219
	}

	//创建8个销售,销售额随机
	for(int i=0;i<8;i++)
	{
		e[idx++]=new SalePerson(rand()%20000+100000);//销售额范围100000-299999
	}

	//创建一个销售经理
	e[idx++]=new SaleManager(5000);

	for(int i=0;i<20;i++)
	{
		e[i]->print_salary();
		delete e[i];
	}

	return 0;
}

运行结果:

下节开始学习运算符重载!

如有问题可评论区或者私信留言,如果想要进扣扣交流群请私信!

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

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

相关文章

【免越狱】iOS任意版本号APP下载

下载地址 https://pan.quark.cn/s/570e928ee2c4 软件介绍 下载iOS旧版应用&#xff0c;简化繁琐的抓包流程。一键生成去更新IPA&#xff08;手机安装后&#xff0c;去除App Store的更新检测&#xff09;。 软件界面 使用方法 一、直接搜索方式 搜索APP&#xff0c;双击选…

Vue3 + Ts + Vite项目 websoket封装使用

文章目录 一、安装二、封装三、请求地址配置3.1 将接口地址放到 public3.2 引入 ipconfig.js 文件3.3 全局类型声明 四、页面使用4.1 引用4.2 注册 五、说明 一、安装 npm npm install websocket --save-devpnpm pnpm install websocket --save-dev二、封装 在 /src/utils …

Haskell爬虫:连接管理与HTTP请求性能

爬虫技术作为数据抓取的重要手段&#xff0c;其效率和性能直接影响到数据获取的质量与速度。Haskell&#xff0c;作为一种纯函数式编程语言&#xff0c;以其强大的类型系统和并发处理能力&#xff0c;在构建高效爬虫方面展现出独特的优势。本文将探讨在Haskell中如何通过连接管…

OVMR:华为北大联手,基于多模态融合的SOTA开放词汇识别 | CVPR 2024

即插即用的方法OVMR将新类别的多模态线索嵌入到VLM中&#xff0c;以增强其在开放词汇识别中的能力。它最初利用多模态分类器生成模块将示例图像嵌入到视觉标记中&#xff0c;然后通过推断它们与语言编码器的上下文关系来自适应地融合多模态线索。为了减轻低质量模态的负面影响&…

[DICOM活久见] 序列内部的RescaleIntercept不同导致的问题

本文由Markdown语法编辑器编辑完成. 1. 背景: 本文记录在工作中遇到的一些比较罕见的dicom图像. 这对于在未来工作中, 处理图像时, 需要考虑方案的完整性, 会有很大的帮助. 本文介绍的, 是目前我工作10年来, 头一次见到的一个CT序列, 它的序列内的RescaleIntercept值, 不是完…

Ubuntu解压7z压缩包方法

0 Preface/Foreword 1 解压缩指令 1.1 环境安装和检查 环境&#xff1a;检测ubuntu环境是否装有7z工具&#xff0c;如果没有&#xff0c;需要手动安装&#xff0c;安装方法如下&#xff1a; sudo apt-update sudo apt-get install p7zip-full 检测工具是否安装成功&#xff…

qmt量化交易策略小白学习笔记第56期【qmt编程之期权数据--获取历史期权列表--原生Python】

qmt编程之获取期权数据 qmt更加详细的教程方法&#xff0c;会持续慢慢梳理。 也可找寻博主的历史文章&#xff0c;搜索关键词查看解决方案 &#xff01; 获取历史期权列表 函数能帮助用户获取历史期权列表, 包括某日历史在上交所上市的认购合约和认沽合约, 也包括已经退市的…

PTA单词首字母大写

作者 颜晖 单位 浙大城市学院 本题目要求编写程序&#xff0c;输入一行字符&#xff0c;将每个单词的首字母改为大写后输出。所谓“单词”是指连续不含空格的字符串&#xff0c;各单词之间用空格分隔&#xff0c;空格数可以是多个。 输入格式: 输入给出一行字符。 输出格式…

Css:属性选择器、关系选择器及伪元素

css的属性选择器&#xff1a; 注&#xff1a;属性值只能由数字&#xff0c;字母&#xff0c;下划线&#xff0c;中划线组成&#xff0c;并且不能以数字开头。 1、[属性] 选择含有指定属性的元素&#xff0c;用[]中括号表示。 <style> /*注意大小写区分 注意前后顺序 样…

电脑技巧:如何在Win11电脑上调整设置,让屏幕更加护眼?

目录 一、调整屏幕亮度 二、启用夜间模式 三、调整色彩设置 四、使用第三方护眼软件 五、保持良好的用眼习惯 总结 随着长时间使用电脑的人越来越多,护眼问题也变得越来越重要。Win11作为更新的操作系统,提供了更多的设置选项来帮助我们保护眼睛。本文将详细介绍如何在…

针对实验试服务器的使用问题【安装anaconda+Vscode连接+jupyther远程连接】

目录 一、Xshell连接 1-创建连接 2-安装anaconda 3-创建conda环境 4-修改下载镜像源 5-安装torch 二、VScoda连接服务器 1-下载插件Remote-ssh 用于远程连接服务器 2-配置文件 三、jupyther远程连接 四、师兄推荐的入门资料 1-python基础 2-机器学习 五、参考资料…

网络安全:键盘记录器

目录 什么是键盘记录器&#xff1f; 键盘记录器的类型 键盘记录器的工作原理 键盘记录器的有害影响 如何防止键盘记录器攻击&#xff1f; 在网络攻击的世界中&#xff0c;存在着许多形式的威胁&#xff0c;如前所述。 在本章中&#xff0c;我们将讨论网络攻击中的主要恶意…

基于mediamtx+ffmpeg实现视频推流,基于python-deffcode实现视频拉流

软件依赖&#xff1a;mediamtx、ffmpeg python包依赖&#xff1a;deffcode mediamtx下载地址&#xff1a;https://github.com/bluenviron/mediamtx/releases ffmeg下载地址&#xff1a;https://ffmpeg.org/download.html deffcode安装命令&#xff1a;pip install deffcode 1、…

HarmonyOS NEXT仓颉编程语言开发环境搭建(安装DevEco Studio Cangjie Plugin)

仓颉编程语言开发环境搭建主要是两部分&#xff1a; 安装最新版DevEco Studio&#xff1b;在DevEco Studio里面安装仓颉插件&#xff08;DevEco Studio Cangjie Plugin&#xff09;。 本文主要介绍DevEco Studio Cangjie Plugin的使用。 DevEco Studio Cangjie Plugin概述 …

【堆、快速选择排序】探寻TopK问题的解决方案

目录 前言 什么是TopK问题 建堆——优先级队列 快速选择排序QuickSelect 快速选择排序的时间复杂度 前言 TopK问题在面试中常常被问到 —— 比如&#xff0c;在10亿个整数里&#xff0c;找出最大的前100个。在海量数据中查找出重复出现的元素或者去除重复出现的元素也是常…

相亲交友小程序开发功能分析

相亲交友小程序的开发功能分析可以从用户端和管理后台两个主要方面来进行。 用户端功能 注册与登录&#xff1a; 用户可以通过手机号、微信号或其他第三方平台进行注册登录&#xff0c;简化注册流程。 实名认证&#xff1a; 引入实名认证机制&#xff0c;确保用户信息的真实…

统计学习与方法实战——K近邻算法

K近邻算法 K近邻算法备注k近邻模型算法距离度量 k k k值选择分类决策规则构造KDTree k k k近邻查找范围查询 代码结构总结 K近邻算法 备注 kNN是一种基本分类与回归方法. 多数表决规则等价于0-1损失函数下的经验风险最小化&#xff0c;支持多分类&#xff0c; 有别于前面的感…

深度学习——强化学习算法介绍

强化学习算法介绍 强化学习讨论的问题是一个智能体(agent) 怎么在一个复杂不确定的环境(environment)里面去极大化它能获得的奖励。 强化学习和监督学习 强化学习有这个试错探索(trial-and-error exploration)&#xff0c;它需要通过探索环境来获取对环境的理解。强化学习 ag…

嵌入式全栈开发学习笔记---C++(继承和派生)

目录 继承的概念inherit 继承的使用场景 继承的权限 继承对象的模型 继承中的构造和析构 初始化列表的第三个使用场景 场景1&#xff1a;类成员变量被const修饰&#xff1b; 场景2&#xff1a;类对象作为另一个类的成员变量&#xff0c;同时该类没有提供无参构造函数&a…

刷题记录-HOT 100(三)

链表 1、环形链表找环起始结点 使用快慢指针检测环&#xff1a; 初始化两个指针 slow 和 fast&#xff0c;都指向链表的头节点。slow 每次移动一步&#xff0c;fast 每次移动两步。如果 fast 和 slow 相遇&#xff08;即 fast slow&#xff09;&#xff0c;说明链表中存在环。…