【C++】多态

目录

文章目录

前言

一、多态的概念

二、多态的定义及实现

三、重载/重写/隐藏的对比

四、纯虚函数和抽象类

五、多态的原理

总结



前言

        本文主要讲述C++中的多态,涉及的概念有虚函数、协变、纯虚函数、抽象类、虚表指针和虚函数表等。


一、多态的概念

多态分为:

  1. 编译时多态(静态多态):主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,
  2. 运行时多态(动态多态):运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票(5折或75折);军人买票时是优先买票。

我们把编译时一般归为静态,运行时归为动态。

本文主要讲解的就是动态多态


二、多态的定义及实现

1.定义

多态的构成条件:

  • 首先是继承
  • 多态是一个继承关系的下的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象优惠买票。

实现多态还有两个必须条件:

  1. 必须是基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖

说明:要实现多态效果,第⼀必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象;第二派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到。

虚函数:

  • 关键字:virtual
  • 类成员函数前面加 virtual 修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加 virtual 修饰。(包括静态成员函数也不能加virtual)

虚函数演示:

class Person
{
public:virtual void BuyTicket()//虚函数{cout << "买票-全价" << endl;}
};

虚函数的重写(覆盖):

  • 派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。(只有函数体不相同)
  • 注意:在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。(简单点说,派生类会直接把虚函数声明继承下来,重写时只改变函数体)

虚函数重写演示:

class Person
{
public:virtual void BuyTicket()//虚函数{cout << "买票-全价" << endl;}
};class Student : public Person//继承
{
public:virtual void BuyTicket()//虚函数重写{cout << "买票-打折" << endl;}
};


 2.实现

 定义多态演示1:买票

#include <iostream>
using namespace std;class Person
{
public:virtual void BuyTicket()//虚函数{cout << "买票-全价" << endl;}
};class Student : public Person//继承
{
public:virtual void BuyTicket()//虚函数重写{cout << "买票-打折" << endl;}
};int main()
{Person p1;Student s1;Person* ptr1 = &p1;//必须是基类的指针或者引用ptr1->BuyTicket();//被调用的函数必须是虚函数Person& ptr2 = s1;//必须是基类的指针或者引用ptr2.BuyTicket();//被调用的函数必须是虚函数return 0;
}

运行结果:


演示2:动物叫

#include <iostream>
using namespace std;class Animal
{
public:virtual void talk() const//虚函数{}
};class Dog : public Animal//继承
{
public:virtual void talk() const//虚函数重写{cout << "汪汪" << endl;}
};class Cat : public Animal//继承
{
public:virtual void talk() const//虚函数重写{cout << "(>^ω^<)喵" << endl;}
};void LetsHear(const Animal& a)//基类指针或者引用调用
{a.talk();
}int main()
{Dog g;Cat c;LetsHear(g);LetsHear(c);return 0;
}

运行结果:


3.多态场景的一个选择题:

#include <iostream>
using namespace std;class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }
};class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}

以上程序输出结果是什么?()

A:A->0        B:B->1        C:A->1        D:B->0        E:编译出错        F:以上都不正确

  • 首先,初学时这道题很难选对。主函数中,派生类指针 p 调用 test 函数。
  • test 函数是在父类中,这里需要注意,此时 test 的 this 指针是 A类 而不是 B类,我们虽然形象上说继承是把父类的成员拿到子类中,但其实不是真正的拿,而是当编译器调用父类成员时,如果在子类找不到就会去父类中找。
  • 我们知道 test 函数中 this 指针是 A类 的类型时,test 中就使用父类指针调用 func 函数,此时构成多态,因为 func 是虚函数,并且在A类中完成了重写,这里需要注意的是A类中 func 函数虽然没有加 virtual,但还是重写,只要父类加了 virtual 就行。
  • 多态构成条件:1.父类指针或引用调用虚函数(满足),2.虚函数完成重写(满足)。所以 test 中调用的 func 是B类中的 func。此时你可能以为正确答案是 D。
  • 正确答案是:B,因为还有一个点需要注意,上文已经说过,虚函数重写只需要函数体不同即可,这是因为在子类中,是直接将父类的虚函数声明拿过来,所以在子类中缺省值其实是 1 而不是 0。

运行结果验证:

  • 这题其实是一个警示,告诫我们重写虚函数时要保持缺省参数一致,并且子类虚函数最好也要带上 virtual 关键字。


4.协变

  • 派生类重写基类虚函数时,与基类虚函数返回值类型可以不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变的实际意义并不大,所以我们了解一下即可。
  • 简单点说:只要父类虚函数的返回值类型与子类虚函数返回值类型构成父子关系(继承关系),也构成虚函数重写。
  • 也就是说,虚函数重写不一定非要返回值相同,因为有协变这种特殊情况。

协变演示:

#include <iostream>
using namespace std;class A{};
class B : public A{};class Person
{
public:virtual A* BuyTicket()//虚函数{cout << "买票-全价" << endl;return nullptr;}
};class Student : public Person
{
public:virtual B* BuyTicket()//协变{cout << "买票-打折" << endl;return nullptr;}
};void Func(Person* ptr)
{ptr->BuyTicket();
}int main()
{Person p1;Student s1;Func(&p1);Func(&s1);return 0;
}

运行结果:

  • 父类虚函数与子类虚函数返回值 A* 和 B* ,A类和B类构成父子关系(继承关系),这就是协变,构成虚函数重写,也就支持多态。
  • 当然,返回值是当前父子类也是一样的。

演示:

#include <iostream>
using namespace std;class Person
{
public:virtual Person* BuyTicket()//虚函数{cout << "买票-全价" << endl;return nullptr;}
};class Student : public Person
{
public:virtual Student* BuyTicket()//协变{cout << "买票-打折" << endl;return nullptr;}
};void Func(Person* ptr)
{ptr->BuyTicket();
}int main()
{Person p1;Student s1;Func(&p1);Func(&s1);return 0;
}

运行结果:

总结,协变只要虚函数返回值构成父子关系即可。


5.析构函数的重写

  • 基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写。
  • 虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成 destructor,所以基类的析构函数加了 vialtual修饰,派生类的析构函数就构成重写。

为什么基类的析构函数建议设计成虚函数?

我们可以通过下面一个例子来说明:

#include <iostream>
using namespace std;class A
{
public:virtual ~A()//写成虚函数{cout << "~A()" << endl;}
};class B : public A
{
public:~B()//无论加不加virtual都构成虚函数重写{delete[] _p;cout << "~B()" << endl;}protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;//假如不构成多态,p2就调用不到B类的析构导致内存泄漏delete p2;return 0;
}

运行结果:

  • 第一个析构是 p1 调用A类的析构,第二、三个析构是p2调用B类的析构,因为继承所以最后还会析构父类。
  • 这里析构形成多态就不怕因为调用不到 B 类的析构而导致内存泄漏等问题了。

从反汇编处就能看到A、B类的析构函数名字都被处理成一样的了,所以满足虚函数重写条件之一的函数名相同:


6.override和final关键字

  • 从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来 debug 会得不偿失,因此C++11提供了override,可以帮助用户检测是否完成重写。
  • 如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。

override示例:

#include <iostream>
using namespace std;class Person
{
public:virtual void BuyTicket()//虚函数{cout << "买票-全价" << endl;}
};class Student : public Person
{
public://加上override可以判断是否完成重写,比如这里故意写成函数名virtual void BuyTicke() override{cout << "买票-打折" << endl;}
};int main()
{Person p1;Student s1;Person* ptr1 = &p1;ptr1->BuyTicket();Person& ptr2 = s1;ptr2.BuyTicket();return 0;
}

报错信息:

final示例:

#include <iostream>
using namespace std;class Person
{
public:virtual void BuyTicket() final//不能重写{cout << "买票-全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket() override{cout << "买票-打折" << endl;}
};int main()
{Person p1;Student s1;Person* ptr1 = &p1;ptr1->BuyTicket();Person& ptr2 = s1;ptr2.BuyTicket();return 0;
}

报错信息:


三、重载/重写/隐藏的对比


四、纯虚函数和抽象类

  • 在虚函数的后面写上 =0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。
  • 包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。
  • 纯虚函数某种程度上强制了 派生类重写虚函数,因为不重写实例化不出对象。

演示:纯虚函数和抽象类

#include <iostream>
using namespace std;//抽象类
class Car
{
public:virtual void Drive() = 0;//纯虚函数
};class Benz : public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};class Bmw : public Car
{
public:virtual void Drive(){cout << "Bwm-操控" << endl;}
};int main()
{Car* p1 = new Benz;p1->Drive();Car* p2 = new Bmw;p2->Drive();return 0;
}

运行结果:

  • 简单说,抽象类就是专门用来继承的类,它可以表示某一个抽象概念,抽象的东西是不能实例化的。
  • 比如上面抽象类 Car(车),因为不是什么具体的车所以不实例化,当其子类重写纯虚函数后就可以表示 Benz(奔驰) 或者 Bmw(宝马),这样就可以实例化具体的车了。


五、多态的原理

1.虚函数表指针

问:下面编译为32位程序的运行结果是什么()

A.编译报错        B.运行报错        C.8        D.12

#include <iostream>
using namespace std;class Base
{
public:virtual void func1(){cout << "func1()" << endl;}
protected:int _a = 1;char _ch = 'x';
};int main()
{Base b;cout << sizeof(b) << endl;//32位return 0;
}
  • 首先,我们知道关于一个类对象的大小,只计算它的成员变量,函数是储存在静态区的
  • 所以表面上我们只计算 _a 和 _ch 的大小,一个占4字节,一个占1字节,按照内存对齐结果应该是占 8 字节。但是...
  • 实际结果是选 D,12字节,因为这里存在一个虚函数表指针,指针在32位下占4字节,按照内存对齐结果就是12字节。

运行结果:

我们可以通过监视窗口看到虚函数表指针:

  • 这里多了一个变量 _vfptr 就是虚函数表指针,也可以叫虚表指针,它是一个指针数组,专门存储虚函数地址的。
  • 那么C++实现多态的原理就是全靠这个 虚函数表 了。
  • 注意:只有定义了虚函数或者继承了父类的虚函数,才有这个虚函数表指针变量。


2.多态原理

  • 父类定义了虚函数,那么虚函数表是父类子类都有的,对于子类的虚函数表,如果重写了父类的虚函数,那么子类对应虚函数的地址就不一样,这样当父类指针调用子类或者父类时,就会根据虚函数表中不同的地址调用不同的函数,以此就形成了多态。
  • 所谓动态多态:就是程序在运行时根据虚函数表中的函数地址调用不同函数来实现的。


3.动态绑定与静态绑定

  • 对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
  • 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。

反汇编可以看出区别:

动态绑定:先存在寄存器中,然后去寄存器中call对应函数地址

静态绑定:函数地址在编译时就直接确认了的


4.虚函数表

关于虚函数表还有一些需要补充的:

  • 1.基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。
  • 2.派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。
  • 3.派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
  • 4.派生类的虚函数表中包含,(1)基类的虚函数地址,(2)派生类重写的虚函数地址完成覆盖,派生类自己的虚函数地址三个部分。
  • 5.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会在后面放个0x00000000 标记,g++系列编译不会放)

演示:

#include <iostream>
#include <string>
using namespace std;class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}virtual void Func1(){cout << "Func1()" << endl;}
protected:string _name;
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票-打折" << endl;}virtual void Func2(){cout << "Func2()" << endl;}void Func3(){cout << "Func3()" << endl;}
protected:int _id;
};int main()
{Person p1;Student s1;return 0;
}

代码说明: 

  • 这里定义了两个类,基类是Person,Person中定义了两个虚函数:BuyTicket和Func1。
  • 派生类 Student 中有三个函数,虚函数 BuyTicket 是重写的,虚函数 Func2 是自己新增的,另外还有一个普通的函数 Func3。

监视窗口:

  • 首先,红色的线地址不一样,这与上面第一、二条对应,父类与子类的虚表地址是不一样的,如果有多个子类,子类的虚表地址是一样的(这里没有演示可以自行验证)
  • 绿色的线地址不一样,这与上面第三条对应,子类重写了父类虚函数,那么子类中该虚函数地址是不一样的。
  • 蓝色的线地址相同,因为这个虚函数是直接从父类继承的,没有重写,所以地址一致。
  • 这里还有一个问题,就是子类对象 s1 中还有一个虚函数 Func2 和一个普通函数 Func3,Func3 肯定不在虚表中,可是为什么 Func2 也不在呢?其实这是VS出于某种原因故意不显示的,但是我们根据上面第5条,可以在内存窗口看到 Func2 的地址

内存窗口:

  • 我们在内存窗口输入s1的虚表地址就能看到:
  • 红色的线对应的就是 BuyTicket 虚函数,绿色对应的就是 Func1 虚函数,(注意:因为是小端存储,所以地址是倒序),根据上面第5条虚函数表末尾会以 0x00000000 标记,我们可以大胆猜测画蓝色线的地址就是 Func2。

最后补充两条:

  • 6.虚函数存在哪的?虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。
  • 7.虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,vs下是存在代码段(常量区)。


总结

        以上就是本文的全部内容了,感谢你的支持!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

k8s部署prometheus+alertmanager+grafana监控

1、下载prometheus.yaml文件 根据github上面的版本对应说明&#xff0c;选择我们要下载的版本&#xff0c;github地址 rootiZj6c72dzbei17o2cuksmeZ:~# wget https://github.com/prometheus-operator/kube-prometheus/archive/refs/tags/v0.14.0.tar.gz rootiZj6c72dzbei17o2cu…

扩展卡尔曼滤波

1.非线性系统的线性化 标准卡尔曼滤波 适用于线性化系统&#xff0c;扩展卡尔曼滤波 则扩展到了非线性系统&#xff0c;核心原理就是将非线性系统线性化&#xff0c;主要用的的知识点是 泰勒展开&#xff08;我另外一篇文章的链接&#xff09;&#xff0c;如下是泰勒展开的公式…

【从0实现muduo库系列】第二讲:基础类型与工具类

0 章节重点 重点内容 视频讲解&#xff1a;《CLinux编程进阶&#xff1a;从0实现muduo C网络框架系列》-第2讲.基础类型与工具类 代码改动 cp -r lesson1 lesson2 实现&#xff1a;base/Types.h 实现&#xff1a;base/copyable.h和noncopyable.h 实现&#xff1a;base/Str…

Qemu-STM32(十):STM32F103开篇

简介 本系列博客主要描述了STM32F103的qemu模拟器实现&#xff0c;进行该项目的原因有两点: 作者在高铁上&#xff0c;想在STM32F103上验证一个软件框架时&#xff0c;如果此时掏出开发板&#xff0c;然后接一堆的线&#xff0c;旁边的人估计会投来异样的目光&#xff0c;特别…

鸿蒙HarmonyOS NEXT应用崩溃分析及修复

鸿蒙HarmonyOS NEXT应用崩溃分析及修复 如何保证应用的健壮性&#xff0c;其中一个指标就是看崩溃率&#xff0c;如何降低崩溃率&#xff0c;就需要知道存在哪些崩溃&#xff0c;然后对症下药&#xff0c;解决崩溃。那么鸿蒙应用中存在哪些崩溃类型呢&#xff1f;又改如何解决…

K8S-etcd服务无法启动问题排查

一、环境、版本信息说明 k8s&#xff1a;v1.19.16 etcdctl version: 3.5.1 3台etcd&#xff08;10.xxx.xx.129、10.xxx.xx.130、10.xxx.xx.131&#xff09;组成的集群。 二、问题根因 129节点的etcd数据与其他两台数据不一致&#xff0c;集群一致性校验出错导致无法加入集…

【视觉提示学习】3.21论文随想

. . Frontiers of Information Technology & Electronic Engineering. 2024, 25(1): 42-63 https://doi.org/10.1631/FITEE.2300389 中文综述&#xff0c;根据里面的架构&#xff0c;把视觉提示学习分成两类&#xff0c;一类是单模态提示学习&#xff08;以vit为代表&…

基于SpringBoot的“校园招聘网站”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“校园招聘网站”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统整体功能图 局部E-R图 系统首页界面 系统注册…

爱普生晶振FC2012AA汽车ADAS主控制系统的理想选择

在汽车智能化的浪潮中&#xff0c;先进驾驶辅助系统&#xff08;ADAS&#xff09;正迅速成为现代汽车的核心技术之一。ADAS 系统通过集成多种传感器、摄像头和高性能芯片&#xff0c;实现对车辆周围环境的实时监测和智能决策&#xff0c;为驾驶者提供全方位的安全保障。而在这一…

基于 ABAP RESTful 应用程序编程模型开发 OData V4 服务

一、概念 以个人图书管理为例&#xff0c;创建一个ABAP RESTful 应用程序编程模型项目。最终要实现的效果&#xff1a; 用于管理书籍的程序。读取、修改和删除书籍。 二、Data Model-数据模型 2.1 创建项目基础数据库表 首先&#xff0c;创建一个图书相关的表&#xff0c;点…

阿里云平台服务器操作以及发布静态项目

目录&#xff1a; 1、云服务器介绍2、云服务器界面3、发布静态项目1、启动nginx2、ngixn访问3、外网访问测试4、拷贝静态资源到nginx目录下并重启nginx 1、云服务器介绍 2、云服务器界面 实例详情&#xff1a;里面主要显示云服务的内外网地址以及一些启动/停止的操作。监控&…

注意力机制,本质上是在做什么?

本文以自注意机制为例&#xff0c;输入一个4*4的矩阵 如下&#xff1a; input_datatorch.tensor([[1,2,3,4], [5,6,7,8], [9,10,11,12], [13,14,15,16] ],dtypetorch.float) 得到Q和K的转置如下。 此时&#xff0c;计算QK^T ,得到如下结果 第一行第一个位置就是第一条样本和第…

C语言-数组指针和指针数组

指针 数组指针与指针数组 数组指针 定义 概念&#xff1a;数组指针是指向数组的指针&#xff0c;本质上还是指针 特点&#xff1a; ①先有数组&#xff0c;后有指针 ②它指向的是一个完整的数组 一维数组指针 语法&#xff1a; 数据类型 (*指针变量名)[容量]; 案例&a…

【前四届会议均已完成独立出版及EI检索 | 河南大学、河南省科学院主办,多高校单位承协办】第五届信号图像处理与通信国际学术会议(ICSIPC 2025)

第五届信号图像处理与通信国际学术会议&#xff08;ICSIPC 2025&#xff09; 2025 5th International Conference on Signal Image Processing and Communication&#xff08;ICSIPC 2025&#xff09; 会议官网&#xff1a;http://www.icsipc.org 【论文投稿】 会议时间&…

AI 时代的通信新范式:MCP(模块化通信协议)的优势与应用

文章目录 引言 1. 传统 API 的局限性2. MCP&#xff08;模块化通信协议&#xff09;的核心优势2.1 更好的模块化支持2.2 低耦合性与灵活性2.3 高性能数据传输2.4 适配分布式 AI 计算架构 3. AI 时代的 MCP 应用案例4. 结论&#xff1a;AI 时代的通信新范式 引言 在 AI 驱动的现…

Linux 文件系统的日志模式与性能影响

在 Linux 文件系统中&#xff0c;**日志模式&#xff08;Journaling Mode&#xff09;** 是文件系统保证数据一致性和快速恢复的核心机制&#xff0c;但不同的日志模式会对性能产生显著影响。以下是详细分析及优化建议&#xff1a; --- ### **一、日志模式的核心分类** Linux…

TISAX认证注意事项的详细介绍

TISAX&#xff08;Trusted Information Security Assessment Exchange&#xff09;认证的注意事项犹如企业在信息安全领域航行时必须遵循的灯塔指引&#xff0c;至关重要且不容忽视。以下是对TISAX认证注意事项的详尽阐述&#xff1a; 首先&#xff0c;企业需深入研读并理解TI…

Nodejs 项目打包部署方式

方式一&#xff1a;PM2 一、准备工作 确保服务器上已安装 Node.js 环境建议使用 PM2 进行进程管理&#xff08;需要额外安装&#xff09; 二、部署步骤 1.首先在服务器上安装 PM2&#xff08;推荐&#xff09;&#xff1a; npm install -g pm22.将项目代码上传到服务器&…

springboot整合modbus实现通讯

springboot整合modbus4j实现tcp通讯 前言 本文基于springboot和modbus4j进行简单封装&#xff0c;达到开箱即用的目的&#xff0c;目前本方案仅实现了tcp通讯。代码会放在最后&#xff0c;按照使用方法操作后就可以直接使用 介绍 在使用本方案之前&#xff0c;有必要对modb…

【论文阅读】Contrastive Clustering Learning for Multi-Behavior Recommendation

论文地址&#xff1a;Contrastive Clustering Learning for Multi-Behavior Recommendation | ACM Transactions on Information Systems 摘要 近年来&#xff0c;多行为推荐模型取得了显著成功。然而&#xff0c;许多模型未充分考虑不同行为之间的共性与差异性&#xff0c;以…