c++多态(深度刨析)

C++系列-----多态

文章目录

  • C++系列-----多态
  • 前言
  • 一、多态的概念
  • 二、多态的定义及实现
    • 2.1、多态构成的条件
      • 2.1.1、虚函数
      • 2.1.2、虚函数的重写
    • 2.2、C++11 override 和 final
    • 2.3、重载、覆盖(重写)、隐藏(重定义)的对比
    • 2.4、抽象类
    • 2.5、 接口继承和实现继承
  • 三、多态的原理
    • 3.1、虚函数表
    • 3.2、多态的原理
    • 3.3 、静态绑定与动态绑定``
  • 四、单继承和多继承的虚函数表
    • 4.1 、单继承的虚函数表
    • 4.2 、多继承的虚函数表
  • 总结


前言

在开始学习多态之前,首先要掌握继承的概念、实现、原理,这篇文章可以帮助大家学习->继承详解。

一、多态的概念

C++多态性(Polymorphism)是面向对象编程(OOP)的一个重要特性之一,它允许我们使用统一的接口来处理不同类型的对象。多态性使得程序更加灵活、可扩展并且易于维护。

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
举例:对于买票这个行为来说,相较于普通人学生可以买半价票,军人可以优先买票,这就是不同的对象去完成买票这个行为,的不同状态。

二、多态的定义及实现

2.1、多态构成的条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

在这里插入图片描述

下面我们对上述条件进行分析

2.1.1、虚函数

virtual:c++11,提供的关键字

虚函数:就是被virtual修饰的类成员函数

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

这里BuyTicket()就是用关键字virtual修饰成的虚函数。

2.1.2、虚函数的重写

在介绍多态时,我们对继承中,成员函数的重定义(隐藏)作了介绍,要和这里区分开。

虚函数的重写(覆盖):要想完成虚函数的重写必须满两个条件:1、 是虚函数。2、三同,三同是指派生类中有一个跟基类虚函数返回值类型、函数名字、参数列表(类型),相同的成员函数,这样就称子类的虚函数重写了基类的虚函数。我们看如下例子:

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

上面的派生类Student 的 BuyTicket() 与Person 的 BuyTicket() 构成了重写。注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

重写的两个例外:
1.协变

上面我们说,要构成重写必须满足三同,但是c++规定,在派生类重写基类虚函数时,与基类虚函数返回值类型不同可以不同。但是必须满足基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,这称为协变。

在这里插入图片描述
2、析构函数的重写

在讲解继承时我们就提到,编译器对析构函数进行的特殊处理,影响了析构函数在继承者的使用。那么它为什么要进行特殊处理呢?

class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{Person* tmp = new Student;
}

在这里插入图片描述

来看这一句代码,它是否能实现多态呢?我们上面说的三同,但是析构函数并不能满足(函数名不同),而重写的原因,就是为了让它具备实现多态的条件 ,于是编译器将析构函数在编译阶段统一处理为:destructor()。下面我们来看一下即使,这个处理给我们在使用多态时造成了不便,为什么编译器依然要这样做。

class Person {
public:~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:~Student() { cout << "~Student()" << endl; }
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

上述代码,析构函数,不构成重写条件(不能实现多态调用)。执行结果:
在这里插入图片描述
我们想要将动态申请创建的 Person和Student对象释放掉,但是结果,并不如愿,这是为什么呢?我们在继承部分学过的赋值兼容规则,子类的对象,指针赋值给父类时,会发生切割、切片。p2指针只会指向属于父类的那一部分。所以时调用了父类的析构函数。 并不能正确的释放掉动态开辟的空间。
对上面的问题,编译器将它处理为析构,很容易就得到解决了.

2.2、C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来检查会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

  1. final:修饰虚函数,表示该虚函数不能再被重写

在这里插入图片描述

  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写译报错
    在这里插入图片描述

我破坏了重写条件,使Drive()不够成重写

2.3、重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

2.4、抽象类

概念:
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

抽象类不能实例化出对象:
抽象类不能实例化出对象

派生类如果不重写纯虚函数也无法实例化出对象
在这里插入图片描述

2.5、 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。下方代码不仅可以将这个概念给体现出来,还可以帮我们检查,对上面的知识是否理解:

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: 以上都不正确
下面我们来分析一下:

B类型的指针p,指向了动态申请的B类型对象,使用p指针调用test()函数,我们知道类的成员函数存在一个this指针(test(A*this)),那么我们使用p指针调用test()函数,就相当于将使用子类类型的指针赋值给父类类型,这会发生切片,也就是说现在这个指针虽然是父类类型的,但是它指向的是子类对象中,父类的那一部分,这时通过它调用func()就会实现多态调用,调用的是子类重写的func()函数。看到这里可能就会有人选择D选项了,但是我们结合上面说的在进行多态调用时虚函数的重写是接口继承,所以它重写的只是父类中虚函数的实现,而void func(int val = 1) 接口是从父类中继承下来的(特别强调:只是在多态调用时使用的是继承接口,普通调用并不是), 所以结果是B.

三、多态的原理

3.1、虚函数表

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
int main()
{Base a;cout << sizeof(a) << endl;return 0;
}

程序运行的结果是什么呢?

在这里插入图片描述
在32位机器下,计算的a对象的大小是8,这是为什么呢?我们来看一下Base类型的对象中都存储了什么。
在这里插入图片描述
通过监视窗口可以看到a对象中不仅存储了,_b变量,还有一个_vfptr指针,而它指向的空间,存储了的是一个函数类型的指针。
这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?

针对上面的代码我们做出以下改造:

  • 我们增加一个派生类Derive去继承Base。
  • Derive中重写Func1。
  • Base再增加一个虚函数Func2和一个普通函数Fun。

具体代码如下:

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

在这里插入图片描述

通过观察和测试,我们发现了以下几点问题:

1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
2. 基类b对象和派生类d对象虚表是不一样的(看虚表地址),这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的函数指针数组,一般情况这个数组最后面放了一个nullptr(不同编译器在结尾处理方式不同)。
5. 总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

这里所说的拷贝,是帮助我们在行为上理解,具体怎么操作,要看编译器底层处理方式。

3.2、多态的原理

上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket,代码如下:

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person* p)
{p->BuyTicket();
}
int main()
{Person Mike;Func(&Mike);Student Johnson;Func(&Johnson);return 0;
}

通过上面对虚表的学习,我们也大概清楚了每个拥有虚函数的对象都有属于自己的虚表。而自己的虚表中存储的是自己的虚函数。在调用时,会到指针所指向的对象的虚表中找到对应的虚函数进行调用。 具体我们可看下图:

1. 下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
2. 下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。反思一下为什么?

在这里插入图片描述

下面我们来结合汇编语言来看一下:
在这里插入图片描述这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到指向的对象的虚表中去找对应的虚函数。而对于普通调用,是在编译时已经从符号表确认了函数的地址,直接call 地址普通函数的调用。这就与静态绑定和动态绑定有关了。

3.3 、静态绑定与动态绑定``

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
    比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
    行为,调用具体的函数,也称为动态多态。

四、单继承和多继承的虚函数表

这里主要介绍单继承,多继承包含菱形继承情况太过复杂

补充:
在这里插入图片描述从上图可以看到,同一类型的对象共用一张虚表。

4.1 、单继承的虚函数表

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};
int main()
{Base b;Derive d;return 0;
}

对上述代码进行调试
在这里插入图片描述
从监视窗口中我们发现看不见func3和func4。是因为编译器对这里进行了处理。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Base b;Derive d;VFPTR* vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);return 0;
}

解释一下上述代码的思路:

  1. 取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存放虚函数指针的指针数组,这个数组最后面放了一个nullptr。
  2. 先取b的地址,强转成一个int*的指针(为了取对象的头4bytes,这里一定要主要观察自己的编译环境——————64位、32位)。
  3. 指针再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的。
  4. 再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
  5. 虚表指针传递给PrintVTable进行打印虚表。

我们来分析一下运行结果:

在这里插入图片描述
可以看到虽然我们无法在监视窗口看到,完整的存储信息,但是我们通过打印,证实了它的存在。

4.2 、多继承的虚函数表

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{
Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);return 0;
}

在这里插入图片描述
观察上图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

这篇文章还有很多没涉及到的知识,但是那些对我们的帮助并不大,所以就不打算继续写了。

总结

C++中的多态性主要有两种形式:静态多态(编译时多态)和动态多态(运行时多态)。静态多态通过函数重载(Function Overload)实现,即提供同名的不同函数版本,编译器根据传入参数的类型自动选择合适的函数。而动态多态则通过虚函数(Virtual Functions)和指针或引用来完成,如在基类指针或引用上调用实际子类的方法,这就是著名的虚函数表(VTable)机制。动态多态的关键在于派生类对基类虚函数的重写,当基类指针指向子类对象时,会调用子类的实现,这种灵活性让设计更模块化,代码更容易复用。

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

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

相关文章

FPGA开发技能(9)快速生成约束XDC文件

文章目录 1.从Cadence导出csv约束文件2.python程序将csv导出为xdc文件。3.python生成exe4.exe使用注意事项5.传送门 前言&#xff1a; 作为一名FPGA工程师&#xff0c;通常公司会对该岗位的人有一定的硬件能力的要求&#xff0c;最基础的就是需要依据原理图的设计进行FPGA工程内…

css uniapp背景图宽度固定高度自适应可以重复

page {height: 100%;background-image: url(https://onlinekc.a.hlidc.cn/uploads/20241115/350f94aaf493d05625a7ddbc86c7804e.png);background-repeat: repeat;background-size: contain;} 如果不要重复 把background-repeat: repeat;替换background-repeat: no-repeat;

Stable Diffusion核心网络结构——U-Net

​ &#x1f33a;系列文章推荐&#x1f33a; 扩散模型系列文章正在持续的更新&#xff0c;更新节奏如下&#xff0c;先更新SD模型讲解&#xff0c;再更新相关的微调方法文章&#xff0c;敬请期待&#xff01;&#xff01;&#xff01;&#xff08;本文及其之前的文章均已更新&a…

学习threejs,使用AnimationMixer实现变形动画

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.AnimationMixer 动画…

【Linux】指令 + 重定向操作

Linux基本指令 一.Linux基本指令1.mv&#xff08;重要&#xff09;2.cat3.more和less&#xff08;重要&#xff09;4.head和tail5.date6.cal7.find&#xff08;重要&#xff09; 二.Linux相关知识点1. Linux系统中&#xff1a;一切皆文件2. 重定向操作1. 输出重定向2. 追加重定…

SpringBoot源码解析(四):解析应用参数args

SpringBoot源码系列文章 SpringBoot源码解析(一)&#xff1a;SpringApplication构造方法 SpringBoot源码解析(二)&#xff1a;引导上下文DefaultBootstrapContext SpringBoot源码解析(三)&#xff1a;启动开始阶段 SpringBoot源码解析(四)&#xff1a;解析应用参数args 目录…

Vue3.0 + Ts:动态设置style样式 ts 报错

error TS2322: Type ‘{ width: string; left: string; ‘background-color’: unknown; ‘z-index’: number; }’ is not assignable to type ‘StyleValue’ 在 vue3.0 ts 项目中&#xff0c;动态设置样式报错 在 Vue 3 TypeScript 项目中&#xff0c;当你使用 :style 绑…

跨平台WPF框架Avalonia教程 十六

SelectableTextBlock 可选文本块 SelectableTextBlock 块是一个用于显示文本的标签&#xff0c;允许选择和复制文本。它可以显示多行&#xff0c;并且可以完全控制所使用的字体。 有用的属性​ 您可能最常使用这些属性&#xff1a; 属性描述SelectionStart当前选择的起始字…

【MySQL】库的基础操作入门指南

&#x1f351;个人主页&#xff1a;Jupiter. &#x1f680; 所属专栏&#xff1a;MySQL入门指南&#xff1a;从零开始的数据库之旅 欢迎大家点赞收藏评论&#x1f60a; 目录 ☁创建数据库语法说明&#xff1a; 创建数据库案例 &#x1f308;字符集和校验规则查看系统默认字符集…

数据仓库数据湖湖仓一体解决方案

一、资料介绍 数据仓库与数据湖是现代数据管理的两大核心概念。数据仓库是结构化的数据存储仓库&#xff0c;用于支持企业的决策分析&#xff0c;其数据经过清洗、整合&#xff0c;以固定的模式存储&#xff0c;适合复杂查询。数据湖则是一个集中存储大量原始数据的存储库&…

人工智能英伟达越来越“大”的GPU

英伟达&#xff1a;让我们遇见越来越“大”的GPU 在2024年台北ComputeX大会上&#xff0c;英伟达CEO黄仁勋发表了题为《揭开新工业革命序幕》的演讲。他手持一款游戏显卡(很有可能是4090),自豪地宣称&#xff1a;“这是目前最先进的游戏GPU。”紧接着&#xff0c;他走到一台DGX…

知识库搭建:高科技行业的智慧基石与未来展望

一、引言 在科技日新月异的今天&#xff0c;知识密集型作业已成为高科技企业竞争力的核心。面对快速的技术迭代和激烈的市场竞争&#xff0c;如何高效地管理和运用知识资源&#xff0c;成为高科技企业必须面对的挑战。知识库&#xff0c;作为知识管理的核心平台&#xff0c;正…

算法编程题-删除子文件夹

算法编程题-删除子文件夹 原题描述设计思路代码实现复杂度分析 前一段时间面试字节的时候&#xff0c;被问到gin框架的路由结构。gin框架的路由结构采用的一般是前缀树来实现&#xff0c;于是被要求手写前缀树来实现路由的注册和查找。 本文以 leetcode 1233为例介绍一下前缀树…

利用SSH中的弱私钥

import paramiko import argparse import os from threading import Thread, BoundedSemaphore # 设置最大连接数 maxConnections 5 # 创建一个有界信号量&#xff0c;用于控制同时进行的连接数 connection_lock BoundedSemaphore(valuemaxConnections) # 用于控制是否停止所…

力扣整理版七:二叉树(待更新)

满二叉树&#xff1a;如果一棵二叉树只有度为0的结点和度为2的结点&#xff0c;并且度为0的结点在同一层上&#xff0c;则这棵二叉树为满二叉树。深度为k&#xff0c;有2^k-1个节点的二叉树。 完全二叉树&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&am…

如何使用可靠UDP协议(KCP)

希望这篇文章&#xff0c;对学习和使用 KCP 协议的读者&#xff0c;有帮助。 1. KCPUDP 流程图 2. 示例代码 #include <iostream>int main() {// 代码太多&#xff0c;暂存仓库return 0; } 具体使用&#xff0c;请参考代码仓库&#xff1a;https://github.com/ChivenZha…

论文复述:(TRPCA)t-Shatten-p

一个基于TNN-TRPCA的简单创新的论文&#xff0c;Tensor Robust PCA主要是将一个tensor分解为low-rank和sparse两个component&#xff0c;主要思想是引入了weighted tensor Schatten-p norm进行建模。

6_协议与层次划分

在计算机网络中要做到有条不紊地交换数据&#xff0c;就必须遵守一些事先约定好的规则。这些规则明确规定了所交换的数据的格式以及有关的同步问题。这里所说的是狭义的(即同频或同频同相) 而是广义的&#xff0c;即在一定的条件下应当发生什么事件 (例如&#xff0c;应当发送一…

微服务--Gateway网关--全局Token过滤器【重要】

全局过滤器 GlobalFilter&#xff0c; 注入到 IOC里面即可 概念&#xff1a; 全局过滤器&#xff1a; 所有的请求 都会在执行链里面执行这个过滤器 如添加日志、鉴权等 创建一个全局过滤器的基本步骤&#xff1a; 步骤1: 创建过滤器类 首先&#xff0c;创建一个实现了Globa…

Kafka进阶_1.生产消息

文章目录 一、Controller选举二、生产消息2.1、创建待发送数据2.2、创建生产者对象&#xff0c;发送数据2.3、发送回调2.3.1、异步发送2.3.2、同步发送 2.4、拦截器2.5、序列化器2.6、分区器2.7、消息可靠性2.7.1、acks 02.7.2、acks 1(默认)2.7.3、acks -1或all 2.8、部分重…