C++之多态

文章目录

一、多态的概念

多态的定义与类型

二、多态的实现

三、虚函数

 虚函数的概念

虚函数的重写/覆盖

协变

析构函数的重写/覆盖

override,final关键字

override

final

纯虚函数与抽象类

三个概念辨析

四、多态实现的原理

虚函数表指针

动态绑定与静态绑定

虚函数表

虚函数表的作用

虚函数表的结构


前言

在上一节中,我们学习了C++中面向对象的三大特性之一的继承,我们通过继承更加灵活地使用代码,我们可以不用再重复写那些冗余的代码,对于一些成员相同或类似的类,我们可以通过继承来生成一个派生类。在这节,我们再来学习C++面向对象中三大特性的多态。


一、多态的概念

在学习内容之前,我们先来了解一下我们将要学习的东西是什么。我们上节通过字面意思简单地认识到继承就是从一个东西中获取一些东西过来。这里我们再来通过字面认识一下这个多态:多种行为形态。我们把它引入编程再理解一下:一个函数或方法在不同的情况下具有不同的行为。

多态是C++中面向对象编程的一个核心概念,允许同一个接口有多种实现方式,从而提高代码的灵活性和可扩展性。以下是多态的详细解释:

多态的定义与类型

多态(Polymorphism)指的是同一个函数或方法在不同的情况下表现出不同的行为。在C++中,多态分为两种类型:

  1. 编译时多态(静态多态):包括函数重载和运算符重载,编译器在编译阶段就确定调用哪个函数。
  2. 运行时多态(动态多态):通过虚函数和动态绑定实现,编译器在运行时根据实际对象类型调用相应的函数。

二、多态的实现

了解完了多态的概念,我们如何去实现这个多态呢?我们先从概念出发,多态是让一个函数或方法在不同的情况下表现出不同的行为,难道它和我们之前学习的函数重载有点关系吗?这里我提前给出答案——它与我们之前所学习的函数重载有点关系但是又有所区别。我们如果想要去实现多态必须满足以下两个重要条件

1.我们必须使用基类的指针或基类的引用去调用虚函数;

2.我们所调用的函数必须是虚函数,而且派生类的虚函数必须是经过重写/覆盖的。

(上面的两个条件我们仔细想想,其实是十分合理的:我们使用基类的指针或者基类的引用的话,我们才能既指向基类对象又能够指向派生类对象。至于虚函数我们在后面会着重介绍的,我们如果想要使一个函数具有多种行为,我们就需要在不同的派生类中对那个函数的函数体进行修改)

三、虚函数

 虚函数的概念

这里,我们再来学习一个新的概念——虚函数。它就相当于我们之前所学习的函数的一个子类,它具有函数的特性,另外又增加了一些特性。

我们将类成员函数前加上关键字virtual,那么这个成员函数就被称为虚函数。虚函数只能够是类成员函数,对于非成员函数是不可以的。(这里我再多嘴一句:这个virtual关键字,我们其实在上节的继承中就已经见过了,不过当时我们使用virtual是为了解决在多继承中菱形继承所带来的数据冗余和二义性的影响。我们注意不要和那个弄混了,这个就是一个关键字的不同用法)

class Animal
{
public:virtual void eat(){}protected:string _name;int _age;
};

虚函数的重写/覆盖

我们如果对于虚函数的修改叫做虚函数的重写/覆盖。我们不能够简单地在派生类中将函数体修改一下就行了。我们对于虚函数的重写/覆盖有一个强制要求:我们要确保在派生类中重写的虚函数必须函数名,函数返回值,函数参数列表与基类的虚函数保持一致。上面要求那三个相同,对于前两个是很容易看出来的,但是最后一个我们需要注意一下:有时候基类的虚函数参数给了一个缺省值,而派生类的虚函数的参数也给了一个缺省值,两个给定的缺省值不同,这时侯我们选择使用基类的缺省值(前提已经是多态关系了)

注意:我们写虚函数的时候,我们必须保证基类的虚函数中的virtual关键字写上这个是绝对不能缺的,派生类中的virtual关键字可写可不写,因为派生类是从基类中继承过来的,如果基类中已经是虚函数了,那么派生类中也是虚函数了,但是我们建议都带上virtual,这样别人看代码就一眼了之了。

协变

协变是虚函数重写中的一个特殊情况,我们在上面虚函数的重写/覆盖中我们已经提出要求——三同(函数名,函数返回值,函数参数列表),这里我们再来介绍一种特殊情况,区别于上面的那个要求,但是它也能够重写虚函数。派生类重写虚函数时,其函数返回值与基类的虚函数返回值不同,即基类的虚函数的函数返回值为基类对象指针或引用类型,派生类的虚函数的函数返回值为派生类对象指针或引用类型,称之为协变。

class A{};
class B:public A{};class Person
{
public:virtual A* func(){cout << "我是一个人" << endl;return nullptr;}
protected:string _name;int _age;
};class Student :public Person
{
public:virtual B* func(){cout << "我是一名学生" << endl;return nullptr;}
protected:int _id;string _address;
};void Fun(Person& ptr)
{ptr.func();
}int main()
{Person p;Student s;Fun(p);Fun(s);return 0;
}

从其运行结果,我们可以看出来这两个已经形成了多态关系,否则的话运行结果都是基类中的成员函数输出结果。

析构函数的重写/覆盖

对于析构函数的重写与普通的成员函数重写有点区别,在编译器编译时期,它们就将那些析构函数都转化为destructor()的形式,这样那些析构函数就都构成了隐藏关系,我们再在基类的析构函数上加上关键字virtual,那么与派生类的析构函数就构成了重写。

注意:我们对析构函数进行重写的话,我们一定要将基类的析构函数加上virtual,如果我们不加virtual的话,我们在进行释放资源的时候就会造成内存泄露了,我们不能够将派生类中的资源释放出去,反而会重复调用基类的析构函数,重复释放基类中的资源。

class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};class B :public A
{
public:~B(){cout << "~B()->" <<_p<< endl;delete _p;}
protected:int* _p = new int[10];
};
int main()
{
//使用基类指针来指向两个类的对象A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

如上代码:我们对基类中的析构函数加上了virtual关键字,于是派生类中的析构函数与基类的析构函数就构成了重写。于是我们在delete释放资源的时候,它们就能够正确释放资源(使用什么类对象的指针或用,我们就调用哪个类的析构函数,这样就避免了重复调用基类的析构函数这一情况)

override,final关键字

override

这两个关键字,是针对于虚函数重写的。有时候,其他的程序员写虚函数的时候,一些编程小白可能看不出来这是虚函数,于是我们就可以加上一个override关键字,然后编程小白在网上一查override这个关键字,于是知道了这个关键字是为了确保我们重写虚函数的正确性。

在C++编程语言中,override是一个关键字,用于明确指示编译器当前定义的函数是在重写(覆盖)基类中的一个虚函数。它是C++11标准引入的,旨在提高代码的可读性和可靠性。它通常放在函数的定义中,位于函数参数列表之后,返回类型之前。

主要作用

  1. 明确函数重写意图:通过使用override,开发者可以清楚地表明当前函数是在重写基类中的虚函数,而不是定义一个新的函数。
  2. 编译时检查:编译器在编译时会检查派生类中的函数与基类中的虚函数的签名(函数名称、参数类型、返回类型等)是否匹配。如果不匹配,编译器会报错,从而避免因函数签名不一致导致的错误。
  3. 提高代码可维护性:使用override使得代码更易读,其他开发者可以一目了然地看出哪些函数是在重写基类中的虚函数。

使用场景

override关键字主要用于以下场景:

  • 重写基类的虚函数:在派生类中定义函数时,使用override明确表示该函数是在重写基类中的虚函数。
  • 避免函数签名错误:通过编译时检查,确保派生类中的函数与基类中的虚函数签名一致,避免因拼写错误或参数类型不匹配导致的错误。

注意事项

  • 只能用于虚函数的重写override关键字只能在重写基类中的虚函数时使用。如果基类中的函数不是虚函数,使用override会导致编译错误。
  • 函数签名必须匹配:使用override时,派生类中的函数签名必须与基类中的虚函数完全一致,否则编译器会报错。
  • 提高代码质量:合理使用override可以提高代码的可靠性和可维护性,特别是在处理多态和虚函数时。

总结

override关键字是C++11引入的一个有用工具,它通过明确指示函数重写意图和进行编译时检查,帮助开发者编写更加可靠和高质量的代码。在处理多态和虚函数时,合理使用override可以有效避免因函数签名不匹配导致的错误,同时提高代码的可读性和可维护性

----------------------------------------------------------------------------------

final

至于final关键字,我们在之前继承中就已经见到了,我们当时使用这个关键字来修饰基类,那样我们就能够限制这个基类不能够被其他类继承。这里多态我们又见到了这个关键字,但是它修饰的对象不同,这次它修饰的是基类中虚函数,那样我们就能够限制其派生类不能够重写其虚函数。

在C++编程语言中,final是一个关键字,用于限制类的继承和虚函数的重写。它是C++11标准引入的,旨在提高代码的稳定性和可维护性。

主要作用

  1. 防止类被继承:在类定义中使用final关键字,可以防止该类被进一步继承。这意味着任何尝试从该类派生新类的操作都会导致编译错误。

  2. 防止虚函数被重写:在虚函数的定义中使用final关键字,可以防止该函数在派生类中被重写。这意味着任何派生类尝试重写该函数都会导致编译错误。

使用场景

final关键字主要适用于以下场景:

  • 防止意外继承:当设计一个类时,如果希望该类不能被继承,以确保类的结构和行为不会被意外修改,可以使用final关键字。

  • 防止虚函数被重写:在某些情况下,希望确保某个虚函数在派生类中不再被重写,以保持特定的行为不变,可以使用final关键字。

注意事项

  • 仅适用于C++11及以上版本final关键字是C++11引入的,因此需要确保编译器支持C++11标准。

  • override关键字结合使用:在派生类中重写基类的虚函数时,可以同时使用overridefinal关键字,以明确表示该函数是在重写基类的虚函数,并且不允许进一步重写。

  • 合理使用:虽然final关键字可以提高代码的稳定性,但过度使用可能会限制代码的灵活性。因此,应根据具体需求合理使用final关键字。

总结

final关键字是C++11引入的一个有用工具,它通过防止类被继承和虚函数被重写,帮助开发者编写更加稳定和可维护的代码。在需要确保类结构和行为不变的情况下,合理使用final关键字可以有效防止意外的继承或重写,从而提高代码的质量和可靠性。

纯虚函数与抽象类

对于虚函数转化为纯虚函数,只要在虚函数的参数列表括号后面加上=0即可。对于纯虚函数,我们只要声明一下就行了,可以不用定义实现(因为,纯虚函数本身咱们写出来就是为了让类变为抽象类。抽象类我们从它的名字上可以看出来,这个类并不是一个具体的类,是一种抽象的类,因此我们不能够使用抽象类进行实例化对象。

那么纯虚函数的具体作用到底是什么呢?主要是如下三个方面:

  1. 强制实现接口:纯虚函数(使用= 0声明)确保所有派生类必须提供该函数的具体实现,否则派生类将保持抽象状态,无法实例化。这在设计类层次结构时非常有用,确保每个派生类都遵循相同的接口。

  2. 实现多态:纯虚函数是实现运行时多态的核心。通过基类指针或引用调用纯虚函数时,会根据实际对象的类型调用相应的派生类实现,从而实现多态行为。

  3. 设计抽象接口:纯虚函数允许创建抽象基类,这些类定义了一组必须实现的方法,但不提供具体实现。这在设计灵活且可扩展的系统时非常有用,例如在处理不同文件格式或图形形状时,确保每个派生类都实现必要的方法。


class Car
{
public:virtual void Drive() = 0; //在虚函数后面加上=0,就变成了纯虚函数,//纯虚函数一般只要一个声明即可,可以不用定义,因为实际并没有什么作用//有纯虚函数的类是抽象类,抽象类是不可以进行实例化对象的};class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒适~" << endl;}
};class Bwm :public Car
{
public: virtual void Drive(){cout << "Bwm-快速~" << endl;}
};int main()
{//Car car;  //我们是不可以使用抽象类进行实例化对象的,但是我们可以使用抽象类作为类型Car* pBenz = new Benz;pBenz->Drive();Car* PBwm = new Bwm;PBwm->Drive();return 0;
}

如上代码,我们实现了一个纯虚函数,那么基类就是一个抽象类了,我们在main函数中试图想使用基类来实例化对象的,但是报错了。但是我们可以使用抽象类来作为类型(多态中的类型要是基类的指针或引用类型这里仍然是成立的)。这样我们通过纯虚函数这个小细节的增加,就很好地保证了多态的实现,以及基类中对虚函数重写。

三个概念辨析

如下图中的三个概念,初学者容易弄混,这里我单独拿出来介绍一下,它们都是在函数层次的不同概念,但是作用域要求以及对于函数的要求都有所不同,我们要清楚。

四、多态实现的原理

上面介绍了这么多概念,又是如果实现多态,又是多态的相关关键字,净是一些浮在表面的东西,我们要知道如何去用,我们也要通过现象看本质,我们要知道它们的背后的原理。

虚函数表指针

首先我要介绍的就是虚函数表指针,这个东西其实在上面我们已经使用过了,不过我们当时看不到而已。我们的多态的功能是让一个函数或方法在不同的情况下使用不同的行为,那么它们是如何去实现的呢?难道是编译器自动识别的嘛?NONONO,编译器可没你想的那么智能,其实在我们实现多态后,在基类和派生类中就出现了一个虚函数表,这个我们可以通过监视窗口看到有一个指针_vfptr,这个指针的全称是(virtual function table pointer)。这个指针指向的就是我们的虚函数表。虚函数表中放了我们基类和派生类的虚函数的地址,当我们在main函数中调用传递相应的参数时,这个指针就会去虚函数表中去查找相应的虚函数地址,然后进行相应的函数行为。

对于虚函数指针的大小是根据平台所决定的,不同的平台,指针大小不一样。如下图,这是在Vs2022中DebugX86环境下,虚函数指针的大小就是4,但是在X64环境下,它的大小就是8.

动态绑定与静态绑定

对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定

满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数 的地址,也就做动态绑定

虚函数表

在C++中,虚函数表(Virtual Table,简称vtable) 是实现动态绑定(运行时多态)的核心机制。它是编译器自动生成的一个数据结构,用于存储指向虚函数的指针。通过虚函数表,C++能够在运行时确定调用哪个具体的虚函数实现。


虚函数表的作用

  1. 实现动态绑定

    • 当通过基类指针或引用调用虚函数时,编译器无法在编译时确定具体调用哪个函数(因为具体实现可能在派生类中)。
    • 虚函数表允许在运行时根据实际对象的类型动态选择正确的函数实现。
  2. 支持多态

    • 虚函数表是实现多态性的关键。通过虚函数表,不同派生类可以提供不同的函数实现,而基类指针或引用可以透明地调用这些实现。
  3. 存储虚函数指针

    • 虚函数表中存储了所有虚函数的地址,每个类都有自己的虚函数表。
    • 当一个类继承自另一个类时,它的虚函数表会继承父类的虚函数表,并添加新的虚函数或覆盖已有的虚函数。

虚函数表的结构

  1. 虚函数指针(vptr)

    • 每个包含虚函数的类实例中都会有一个指向虚函数表的指针(通常称为 vptr)。
    • vptr 是编译器自动生成的,存储在对象的内存中。
  2. 虚函数表(vtable)

    • 虚函数表是一个数组,其中每个元素是一个函数指针,指向具体的虚函数实现。
    • 每个类都有自己的虚函数表,如果一个类没有虚函数,则不会生成虚函数表。

另外还有几处容易弄错的点

1.基类对象的虚函数表中存放的是基类所有虚函数的地址。同类型的对象共有一张虚函数表,不同类型的对象有各自独立的虚表,所以基类与派生类有各自独立的虚表;

2.派生类有两部分所组成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象中的虚函数表指针并不是同一个,就行基类对象的成员和派生类对象中的基类对象成员不是同一个;

3.派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址;

4.派生类的虚函数表中包含:(1)基类的虚函数地址;(2)派生类重写的虚函数地址完成覆盖;(3)派生类自己的虚函数地址;

5.虚函数表存在哪里呢?这个问题C++标志中并没有明确规定,但是我们可以通过代码可以得知,VS下虚函数表是存在代码区(常量区)。

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

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

相关文章

深入理解 HTML5 Audio:网页音频播放的新时代

在网页开发领域,音频的嵌入和播放一直是一个重要且不断演进的话题。HTML5 的出现,为网页音频播放带来了标准化的解决方案,极大地改善了开发者和用户的体验。 一、HTML5 之前的音频播放状况 在 HTML5 诞生之前,互联网上缺乏统一的网页音频播放标准。当时,大多数音频播放依…

重载和重写的区别

重载 在同一个类中定义多个同名方法&#xff0c; 但参数列表不同&#xff08;参数类型、参数个数或参数顺序不同&#xff09;返回值类型不同。 public class MathOperations {int add(int a, int b) {return a b;}double add(double a, double b) {return a b;} }重写 子…

机器视觉+深度学习,让电子零部件表面缺陷检测效率大幅提升

在精密加工的3C电子行业中&#xff0c;一抹0.1毫米的油渍&#xff0c;一粒肉眼难辨的灰尘或将引发整机性能隐患。当制造业迈入微米级品质竞争时代&#xff0c;产品表面看似微不足道的脏污缺陷&#xff0c;正成为制约企业高质量发展的隐形枷锁。分布无规律的污渍斑点、形态各异的…

Dart逆向之函数调用

我们从Blutter恢复的部分IL中可以看到Dart调用函数的逻辑 // 0x180490: r16 <int> // 0x180490: ldr x16, [PP, #0x8a0] ; [pp0x8a0] TypeArguments: <int> // 0x180494: r30 Instance_MethodChannel // 0x180494: ldr lr, [P…

如何白嫖Grok3 API? 如何使用Grok3 API调用实例?怎么使用Grok3模型?

前段时间&#xff0c;Grok3&#xff08;想要体验Grok3的童鞋可以参考本文&#xff1a;Grok 上线角色扮演功能&#xff0c;教你课后作业手到擒来&#xff0c;Grok3使用次数限制&#xff1f;如何使用Grok3? Grok3国内支付手段如何订阅升级Premium - AI is all your need!&#x…

《超短心法》速读笔记

文章目录 书籍信息概览主线行业篇战法一 人气涨停战法战法二 四维主线战法 主线龙头篇战法三 龙头起爆战法战法四 六合强庄控盘战法战法五 筹码战法之七星连珠 趋势牛股篇战法六 趋势擒龙之暴涨形态战法七 趋势破位起爆战法战法八 强中选强多头战法 涨停晋级篇战法九 强势涨停狙…

git仓库迁移包括提交记录日志

网上找了很多资料都不好用&#xff0c;直到看到一个亲测有效后&#xff0c;整理如下&#xff1a; 1、进入仓库目录下&#xff0c;并且切换到要迁移的分支上 前提是你本地已有旧仓库的代码&#xff1b;如果没有的话&#xff0c;先拉取。 2、更改仓库地址 git remote set-url …

powerDesign 逆向 mysql 生成 物理模型,并用VBS脚本整理comment

学习自&#xff1a;https://www.cnblogs.com/xmyjcs/p/8536233.html 文章目录 Reverse Engineer格式化模型执行 VBS 脚本 Reverse Engineer 下面 DBMS 可以通过 ODBC&#xff08;Open Database Connectivity&#xff0c;开放数据库连接&#xff09;连接&#xff0c; 需要自己先…

Qt文件读写

Qt文件读写&#xff08;Stream流形式&#xff09; 文件读写相关类 1. QFile类 QFile主要用于文件的打开、关闭等功能&#xff1b; [override virtual] bool QFile::open(QIODevice::OpenMode mode);Reimplements: QIODevice::open(QIODevice::OpenMode mode). Opens the fi…

[特殊字符]【高并发实战】Java Socket + 线程池实现高性能文件上传服务器(附完整源码)[特殊字符]

大家好&#xff01;今天给大家分享一个 Java Socket 线程池 实现的高性能文件上传服务器&#xff0c;支持 多客户端并发上传&#xff0c;代码可直接运行&#xff0c;适合 面试、项目实战、性能优化 学习&#xff01; &#x1f4cc; 本文亮点&#xff1a; ✅ 完整可运行代码&a…

Python proteinflow 库介绍

ProteinFlow是一个开源的Python库,旨在简化蛋白质结构数据在深度学习应用中的预处理过程。以下是其详细介绍: 功能 数据处理:支持处理单链和多链蛋白质结构,包括二级结构特征、扭转角等特征化选项。 数据获取:能够从Protein Data Bank (PDB)和Structural Antibody Databa…

WebPages 对象

WebPages 对象 引言 在Web开发领域&#xff0c;WebPages 对象是前端工程师和开发者常用的工具之一。它提供了丰富的API&#xff0c;使我们能够轻松地与网页元素进行交互。本文将深入探讨WebPages对象的概念、特性以及在实际开发中的应用。 概念 WebPages对象是现代浏览器提…

Mysql表的操作(2)

1.去重 select distinct 列名 from 表名 2.查询时排序 select 列名 from 表名 order by 列名 asc/desc; 不影响数据库里面的数据 错误样例 &#xff1a; 但结果却有点出乎意料了~为什么会失败呢&#xff1f; 其实这是因为书写的形式不对&#xff0c;如果带了引号&#xff0c;…

先占个日常,等会写。

引入一个重要的概念 “struct” &#xff08;译为中文&#xff1a;结构体&#xff09; 可用作设出比较复杂的一些变量类型 语法 &#xff1a;struct point name { int x; int y; int z;} point 和 name是任意命名的名字&#xff0c;含义是&#xff0c;声明一个变量类型为st…

SmolDocling:一种超紧凑的视觉语言模型,用于端到端多模态文档转换

paper地址:SmolDocling: An ultra-compact vision-language model for end-to-end multi-modal document conversion Huggingface地址:SmolDocling-256M-preview 代码对应的权重文件:SmolDocling-256M-preview权重文件 一、摘要 以下是文章摘要的总结: SmolDocling 是一…

MySQL SQL Mode

SQL Mode 是 MySQL 中一个重要的系统变量&#xff0c;它决定了 MySQL 应遵循的 SQL 语法规则和数据验证规则。 什么是 SQL Mode SQL Mode 定义了 MySQL 应该支持的 SQL 语法以及执行数据验证的方式。通过设置不同的 SQL Mode&#xff0c;可以让 MySQL 在不同程度上兼容其他数据…

Java bs架构/反射

bs架构 规定的格式是要换行&#xff0c;而打印流天然换行 线程池可以直接处理thread&#xff0c;thread继承自runnable 在Java中&#xff0c;线程池的pool.execute()方法用于提交一个任务给线程池执行。这个方法接受一个Runnable对象作为参数。Runnable是一个函数式接口&…

C++手撕单链表及逆序打印

在学习数据结构的过程中&#xff0c;链表是一个非常重要的基础数据结构。今天&#xff0c;我们将通过C手动实现一个单链表&#xff0c;并添加一个逆序打印的功能&#xff0c;帮助大家更好地理解链表的实现和操作。 一、链表简介 链表是一种线性数据结构&#xff0c;其中每个元…

netty中的ChannelPipeline详解

Netty中的ChannelPipeline是事件处理链的核心组件,负责将多个ChannelHandler组织成有序的责任链,实现网络事件(如数据读写、连接状态变化)的动态编排和传播。以下从核心机制、执行逻辑到应用场景进行详细解析: 1. 核心结构与组成 双向链表结构 组成单元:ChannelPipeline…

智能物联网网关策略部署

实训背景 某智慧工厂需部署物联网网关&#xff0c;实现以下工业级安全管控需求&#xff1a; 设备准入控制&#xff1a;仅允许注册MAC地址的传感器接入&#xff08;白名单&#xff1a;AA:BB:CC:DD:EE:FF&#xff09;。协议合规性&#xff1a;禁止非Modbus TCP&#xff08;端口…