当前位置: 首页 > news >正文

使用继承与派生的6大要点

概述

        面向对象编程技术非常看重软件的可重用性,在C++中,可重用性是通过继承机制来实现的。继承机制允许程序员在保持原有类的数据和功能的基础上进行扩展,增加新的数据和功能,从而构成一个新的类,也称为派生类。原有类,一般称之为基类。派生类不仅拥有基类的成员,还拥有自身新增加的成员。继承与派生是C++的重要组成部分,也是C++的基础知识。掌握好了继承与派生,就对面向对象编程技术有了更深刻的理解。关于继承与派生的入门知识,这里就不赘述了,下面将介绍继承与派生相关的一些知识要点。

访问权限

        派生类从基类派生时,有三种继承方式,分别是:公有继承、保护继承、私有继承,分别对应关键字public、protected、private。

        公有继承时,基类中public成员和protected成员在派生类中的访问权限不变,private成员在派生类中不可访问。

        保护继承时,基类中public成员和protected成员在派生类中的访问权限变为protected,private成员在派生类中不可访问。

        私有继承时,基类中public成员和protected成员在派生类中的访问权限都变为private,private成员在派生类中不可访问。

        可通过下表更清晰地看到不同继承方式下,基类成员在派生类中的访问权限。

基类public成员

基类protected成员

基类private成员

公有继承

public

protected

不可访问

保护继承

protected

protected

不可访问

私有继承

private

private

不可访问

        派生类能否改变基类成员在自身的访问权限呢?答案是肯定的,通过using关键字即可。参看下面的示例代码。

class CBase
{
public:CBase();int m_nData1;protected:int m_nData2;private:int m_nData3;
};CBase::CBase() : m_nData1(6), m_nData2(88), m_nData3(999)
{NULL;
}class CDerived : public CBase
{
public:using CBase::m_nData2;protected:using CBase::m_nData1;using CBase::m_nData3;                    // 编译错误
};CDerived derived;
printf("data1 is %d\n", derived.m_nData1);    // 编译错误
printf("data2 is %d\n", derived.m_nData2);    // 编译正常

        可以看到,通过using 基类::基类成员的方式,可以修改基类成员在派生类中的访问权限。m_nData1原来为public,修改后变为protected。m_nData2原来为protected,修改后变为public。由于m_nData3在基类CBase中为private,故在派生类中无法访问,因此无法通过using CBase::m_nData3修改private成员的访问权限。

构造顺序

        构造派生类的对象时,会按照顺序依次调用以下函数。

        1、所有基类的构造函数。注意是根据继承基类的顺序,而不是派生类中初始化列表的顺序来调用的。

        2、派生类所有成员变量的构造函数。注意是根据声明成员变量的顺序,而不是初始化列表的顺序来调用的。

        3、派生类的构造函数。

        在派生类的初始化列表中,如果没有显式调用基类和成员变量的构造函数,则自动调用基类和成员变量默认的构造函数。另外,销毁派生类对象时,调用析构函数的顺序与上面的顺序正好相反。

        可以通过下面的示例代码更好地理解构造顺序和析构顺序。

class CTemp1
{
public:CTemp1(){printf("CTemp1 constructor\n");}~CTemp1(){printf("CTemp1 destructor\n");}
};class CTemp2
{
public:CTemp2(int nNumber){printf("CTemp2 constructor: %d\n", nNumber);}~CTemp2(){printf("CTemp2 destructor\n");}
};class CTemp3
{
public:CTemp3(){printf("CTemp3 constructor\n");}~CTemp3(){printf("CTemp3 destructor\n");}
};class CBase1
{
public:CBase1(){printf("CBase1 constructor\n");}~CBase1(){printf("CBase1 destructor\n");}
};class CBase2
{
public:CBase2(int nNumber){printf("CBase2 constructor: %d\n", nNumber);}~CBase2(){printf("CBase2 destructor\n");}
};class CBase3
{
public:CBase3(){printf("CBase3 constructor\n");}~CBase3(){printf("CBase3 destructor\n");}
};class CDerived : public CBase1, public CBase2, public CBase3
{
public:CDerived() : m_tmp2(66), m_tmp1(), CBase2(88), CBase1() {printf("CDerived constructor\n");}~CDerived(){printf("CDerived destructor\n");}private:CTemp1 m_tmp1;CTemp2 m_tmp2;CTemp3 m_tmp3;
};CDerived derived;

上述示例的输出如下:

CBase1 constructor
CBase2 constructor: 88
CBase3 constructor
CTemp1 constructor
CTemp2 constructor: 66
CTemp3 constructor
CDerived constructor
CDerived destructor
CTemp3 destructor
CTemp2 destructor
CTemp1 destructor
CBase3 destructor
CBase2 destructor
CBase1 destructor

        从输出可以得出以下几点。

        1、在CDerived的初始化列表中,先初始化了m_tmp2m_tmp1,但依然先调用了三个基类的构造函数。

        2、在CDerived的初始化列表中,先初始化了CBase2,然后才初始化了CBase1,但依然按照继承基类的顺序先调用了CBase1的构造函数。

        3、在CDerived的初始化列表中,没有初始化CBase3,但依然调用了CBase3的默认构造函数。如果CBase3没有默认构造函数,则编译出错。

        4、在CDerived的初始化列表中,先初始化了m_tmp2,然后才初始化了m_tmp1,但依然按照声明成员变量的顺序先调用了m_tmp1的构造函数。

        5、在CDerived的初始化列表中,没有初始化m_tmp3,但依然调用了m_tmp3的默认构造函数。如果m_tmp3没有默认构造函数,则编译出错。

        6、析构的顺序与构造的顺序正好相反。

同名覆盖

        派生类和基类可以拥有同名的成员,此时,派生类会把基类中所有同名的成员(包括多个重载版本的函数)都覆盖掉。要想调用基类的成员,必须加作用域。

class CBase
{
public:CBase() : m_nData(66){NULL;}void ShowData(){printf("CBase data is %d\n", m_nData);}void ShowData(const std::string &strData){printf("CBase data is %s\n", strData.c_str());}int m_nData;
};class CDerived : public CBase
{
public:CDerived() : CBase(), m_nData(88){NULL;}void ShowData(){printf("CDerived data is %d, %d\n", m_nData, CBase::m_nData);}int m_nData;
};CDerived derived;
derived.ShowData();
derived.ShowData("CSDN");                    // 编译出错
printf("data is %d\n", derived.m_nData);derived.CBase::ShowData();
derived.CBase::ShowData("CSDN");
printf("data is %d\n", derived.CBase::m_nData);CBase *pBase = &derived;
pBase->ShowData();

        上述示例的输出如下:

CDerived data is 88, 66
data is 88
CBase data is 66
CBase data is CSDN
data is 66
CBase data is 66

        可以看到,调用派生类对象derived的成员函数ShowData()和成员变量m_nData时,都是使用的派生类中的成员。调用派生类对象derivedShowData("CSDN")时,因为同名会覆盖基类中多个重载版本的函数,导致基类中重载字符串参数的函数在派生类中被隐藏了,不可见,故会报编译错误。加上作用域后,便可以指定访问基类中的成员变量和成员函数。另外,将基类指针指向一个派生类对象时,用基类指针调用的都是基类中的成员(不涉及虚函数,属于静态绑定)。

多继承

        多继承是指派生类同时从多个基类派生。如果多个基类中有同名的public和protected成员,则会引起歧义,导致编译出错。由于基类中的private成员在派生类中不可访问,故同名的private成员不会引起问题。解决多基类同名的方法为:使用作用域访问指定基类中的同名成员。可参看下面的示例代码。

class CBase1
{
public:CBase1() : m_nNumber(66), m_strText("Hello"), m_bPassed(true){NULL;}int m_nNumber;protected:std::string m_strText;private:bool m_bPassed;
};class CBase2
{
public:CBase2() : m_nNumber(88), m_strText("CSDN"), m_bPassed(false){NULL;}int m_nNumber;protected:std::string m_strText;private:bool m_bPassed;
};class CDerived : public CBase1, public CBase2
{
public:void Show(){// 编译出错printf("data is %d, %s\n", m_nNumber, m_strText.c_str());// 输出:CBase1 data is 66, Helloprintf("CBase1 data is %d, %s\n", CBase1::m_nNumber, CBase1::m_strText.c_str());//  输出:CBase2 data is 88, CSDNprintf("CBase2 data is %d, %s\n", CBase2::m_nNumber, CBase2::m_strText.c_str());}
};

        可以看到,由于CBase1CBase2中均有m_nNumberm_strText,在CDerived中直接访问这两个成员,会引发编译错误。通过指定作用域CBase1::CBase2::,可以在派生类中指定访问CBase1CBase2的成员。

虚函数

        虚函数在C++中主要是为了实现多态机制。所谓多态,也就是用基类的指针指向派生类的实例,然后通过基类的指针调用派生类实例的成员函数。可参看下面的示例代码。

class CBase
{
public:CBase() : m_nData1(66){NULL;}virtual void Test1(){printf("CBase Test1\n");}virtual void Test2(){printf("CBase Test2\n");}virtual void Test3(){printf("CBase Test3\n");}private:int m_nData1;
};class CDerived : public CBase
{
public:CDerived() : CBase(), m_nData2(88){NULL;}virtual void Test1(){printf("CDerived Test1\n");}virtual void Test2(){printf("CDerived Test2\n");}private:int m_nData2;
};CBase *pBase = new CDerived();
pBase->Test1();        // 输出:CDerived Test1
pBase->Test2();        // 输出:CDerived Test2

        可以看到,通过基类指针pBase调用虚函数Test1Test2时,调用的是派生类中的函数。

        那么,虚函数是如何实现的呢?

        在C++中,具有虚函数的类都有一张虚函数表。这个表相当于一个一维数组,用于存储类中所有虚函数的地址。编译器必须保证对象实例最开始的位置存放指向虚函数表的指针,然后才能存放其他成员。可通过下面的示例代码来理解虚函数表的概念。

typedef void (*Test)();CBase base;
Test pTest1 = (Test)*((size_t *)*(size_t *)(&base));
pTest1();
Test pTest2 = (Test)*((size_t *)*(size_t *)(&base) + 1);
pTest2();
int nData = (int)*((size_t *)(&base) + 1);
printf("data is %d\n", nData);CDerived derived;
pTest1 = (Test)*((size_t *)*(size_t *)(&derived));
pTest1();
pTest2 = (Test)*((size_t *)*(size_t *)(&derived) + 1);
pTest2();
Test pTest3 = (Test)*((size_t *)*(size_t *)(&derived) + 2);
pTest3();
int nData1 = (int)*((size_t *)(&derived) + 1);
int nData2 = (int)*((size_t *)(&derived) + 2);
printf("data is %d, %d\n", nData1, nData2);

上述示例的输出如下:

CBase Test1
CBase Test2
data is 66
CDerived Test1
CDerived Test2
CBase Test3
data is 66, 88

        可以看到,我们完全通过指针的操作就访问了类实例中的虚函数和成员变量。在上面的代码中,(size_t *)(&base)为指向虚函数表的指针,*(size_t *)(&base)为虚函数表的地址,相当于一维数组的地址,(size_t *)*(size_t *)(&base)指向虚函数表中第一个虚函数的地址,*((size_t *)*(size_t *)(&base))则是第一个虚函数。其他虚函数和成员变量的指针转换与此类似,这里不再赘述。下图给出了类实例base和derived的内存布局结构,供大家参考。

        上面讨论的都是单继承的情况,多继承时,原理类似,只是更复杂些,这里就不再深入介绍了。

虚继承

        虚继承是为了解决菱形继承中,存在多份公共基类的拷贝,从而导致二义性的问题。在定义派生类时,如果在基类的名字前面加上virtual关键字,则构成虚继承。先通过下面的示例代码来看看菱形继承的问题。

class CBase
{
public:CBase() : m_nNumber(1){printf("CBase default constructor\n");}CBase(int nNumber) : m_nNumber(nNumber){printf("CBase constructor: %d\n", nNumber);}protected:int m_nNumber;
};class CDerived1 : public CBase
{
public:CDerived1() : CBase(66){printf("CDerived1 constructor\n");}
};class CDerived2 : public CBase
{
public:CDerived2() : CBase(88){printf("CDerived2 constructor\n");}
};class CFinal : public CDerived1, public CDerived2
{
public:CFinal() : CDerived1(), CDerived2(){printf("CFinal constructor\n");}void Show(){printf("CFinal show: %d\n", m_nNumber);    // 编译出错printf("CFinal show 1: %d\n", CDerived1::m_nNumber);printf("CFinal show 2: %d\n", CDerived2::m_nNumber);}
};CFinal final;
final.Show();

        上述示例的输出如下:

CBase constructor: 66
CDerived1 constructor
CBase constructor: 88
CDerived2 constructor
CFinal constructor
CFinal show 1: 66
CFinal show 2: 88

        可以看到,CFinal继承了CDerived1CDerived2,而CDerived1CDerived2都继承了CBase。在CFinal中直接访问公共基类CBase中的成员变量m_nNumber时,便会存在二义性问题。虽然可以通过指定作用域来分别访问CDerived1CDerived2中的m_nNumber,但CBase存在多份的问题仍然存在(调用了两次CBase的构造函数)。

        再来看看使用虚继承时的示例代码。

class CBase
{
public:CBase() : m_nNumber(1){printf("CBase default constructor\n");}CBase(int nNumber) : m_nNumber(nNumber){printf("CBase constructor: %d\n", nNumber);}protected:int m_nNumber;
};class CDerived1 : public virtual CBase
{
public:CDerived1() : CBase(66){printf("CDerived1 constructor\n");}
};class CDerived2 : public virtual CBase
{
public:CDerived2() : CBase(88){printf("CDerived2 constructor\n");}
};class CFinal : public CDerived1, public CDerived2
{
public:CFinal() : CDerived1(), CDerived2(){printf("CFinal constructor\n");}void Show(){printf("CFinal show: %d\n", m_nNumber);    // 编译正常printf("CFinal show 1: %d\n", CDerived1::m_nNumber);printf("CFinal show 2: %d\n", CDerived2::m_nNumber);}
};CFinal final;
final.Show();

上述示例的输出如下:

CBase default constructor
CDerived1 constructor
CDerived2 constructor
CFinal constructor
CFinal show: 1
CFinal show 1: 1
CFinal show 2: 1

        可以看到,使用虚继承后,公共基类CBase的构造函数只调用了一次,且调用的是默认构造函数。这是因为虚基类的构造函数比较特殊,与常规的构造函数有所不同。由于从虚基类派生出来的每一个派生类中,其构造函数都调用了基类的构造函数,这样编译器就无法确定到底用哪一个派生类来构造基类对象。最终,编译器会忽略所有派生类中对基类构造函数的调用,而选择调用基类的默认构造函数。

        如果基类的默认构造函数不存在,则编译器将报错。解决该问题的方法是:显式在CFinal中调用CBase的其他构造函数。示例代码如下:

class CFinal : public CDerived1, public CDerived2
{
public:CFinal() : CDerived1(), CDerived2(), CBase(2){printf("CFinal constructor\n");}void Show(){printf("CFinal show: %d\n", m_nNumber);printf("CFinal show 1: %d\n", CDerived1::m_nNumber);printf("CFinal show 2: %d\n", CDerived2::m_nNumber);}
};

相关文章:

使用继承与派生的6大要点

概述 面向对象编程技术非常看重软件的可重用性,在C中,可重用性是通过继承机制来实现的。继承机制允许程序员在保持原有类的数据和功能的基础上进行扩展,增加新的数据和功能,从而构成一个新的类,也称为派生类。原有类&a…...

加一-力扣66-java高效方案

一、题目描述给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。你可以假设除了整数 0 之外,这个整数不会以零开头。示例 1:输入:di…...

记一次 .NET 某游戏网站 CPU爆高分析

一:背景 1. 讲故事 这段时间经常有朋友微信上问我这个真实案例分析连载怎么不往下续了,关注我的朋友应该知道,我近二个月在研究 SQLSERVER,也写了十多篇文章,为什么要研究这东西呢? 是因为在 dump 中发现…...

集群使用——资源管理和租户创建

概述 OceanBase 数据库是多租户的分布式数据库,租户使用的资源建立在资源池上。资源池包含了资源单元,而资源单元则规定了具体资源的量化(如 CPU、Memory、Disk_Size 和 IOPS 等)。 创建租户前,必须规定租户使用的资源…...

谷歌浏览器登录失败,提示【无法同步到“...@gmail.com”】

首先安装Chrome同步助手(Chrome-Sync-Helper,看了很多博客,谷歌浏览器同步问题好像都要用这个),改成.rar,解压,文件夹_metadata重命名为metadata,然后添加到谷歌浏览器的扩展程序中。…...

75 111111

选择题(共75题,合计75.0分) 1. 选项ABCD中显示了所创造的商业价值以及在产品中实施各种功能需要进行的开发工作。团队应优先实施哪项功能? The business value created and the development effort needed to implement the various features in the product are sh…...

分销系统逻辑

相关概念 主营商户: 提供分销商品和佣金的商户分销商: 拥有自己的销售渠道,能够帮助推动产品销售的个人或商户消费者: 购买分销商品的人。佣金: 主营商户返还给经销商的比例抽成 分销功能设计 (1)分销商准入规则设计 无规则: 没有分销商的准入门槛限制&#xf…...

MySQL视图特性

文章目录MySQL视图特性基本使用准备测试表创建视图修改视图影响基表修改基表影响视图删除视图视图规则和限制MySQL视图特性 视图的概念 视图是一个虚拟表,其内容由查询定义,同真实的表一样,视图包含一系列带有名称的列和行数据。视图中的数据…...

RabbitMQ详解(二):Docker安装RabbitMQ

在Docker上安装部署RabbitMQ方便快捷,不需要额外安装Erlang环境,所以写该篇文章先来介绍如何在Docker上部署RabbitMQ。 一、安装并运行 (1)、在docker hub 中查找rabbitmq镜像 docker search rabbitmq:3.9.12-management带有“mangement”的版本&…...

如何使用代码注释:关于JavaScript与TypeScript 注释和文档的自动生成

如何使用代码注释:关于JavaScript与TypeScript 注释和文档的自动生成jcLee95:https://blog.csdn.net/qq_28550263?spm1001.2101.3001.5343 邮箱 :291148484163.com 本文地址:https://blog.csdn.net/qq_28550263/article/detail…...

Echarts 设置面积区域图(areaStyle核心)

第011个点击查看专栏目录Echarts折线区域面积图的视觉效果更加饱满丰富,在系列不多的场景下尤其适用。区域面积图将折线到坐标轴的空间设置背景色,用区域面积表达数据。通过 areaStyle 设置折线图的填充区域样式,将其设为为 {} 表示使用默认…...

pandas——字符串处理【建议收藏】

pandas——字符串处理 作者:AOAIYI 创作不易,如果觉得文章不错或能帮助到你学习,记得点赞收藏评论一下哦 文章目录pandas——字符串处理一、实验目的二、实验原理三、实验环境四、实验内容五、实验步骤1.cat() 拼接字符串2.split()切片字符串…...

反射,枚举,lambda表达式

目录 1、反射 1.1 基本概念 1.2 反射相关的类 1.3 创建 Class 对象 1.4 反射的使用 1.4.1 通过反射创建对象: 1.4.2 获取私有的构造方法 1.4.3 获取私有的成员变量 1.4.4 获取私有的方法 1.5 总结 2、枚举 2.1 认识枚举 2.2 使用枚举 2.3 枚举与反射…...

.Net Core对于RabbitMQ封装分布式事件总线

首先我们需要了解到分布式事件总线是什么; 分布式事件总线是一种在分布式系统中提供事件通知、订阅和发布机制的技术。它允许多个组件或微服务之间的协作和通信,而无需直接耦合或了解彼此的实现细节。通过事件总线,组件或微服务可以通过发布…...

GPIO功能描述

GPIO 文章目录 GPIO1. 功能描述1.1 OSCI/OSCO 引脚1.3 HSEIN/HSEOUT引脚1.2 Bit-Band1.4 VRTCAFx引脚1.5 EWKUPx引脚1.6 QSPI0 引脚1.7 LVDIN引脚1.8 SARADC引脚1.9 ADCIN引脚2. 测试项描述2.1 PAD Location2.2 LBOR和BOR复位2.3 驱动能力2.4 模拟态\高阻态2.5 SWD\JTAG2.6 输出…...

指派问题与匈牙利法讲解

指派问题概述:实际中,会遇到这样的问题,有n项不同的任务,需要n个人分别完成其中的1项,每个人完成任务的时间不一样。于是就有一个问题,如何分配任务使得花费时间最少。通俗来讲,就是n*n矩阵中&a…...

day5——冒泡排序,选择排序和插入排序的学习

选择排序冒泡排序插入排序 选择排序 选择排序的基本思路就是: 首先假定第一个的下表为所有元素中最小的一个, 然后用后面的每一个元素跟这个元素进行比较, 如果后面的元素比这个元素更小一点, 那么就将找到的最小的元素的下标和…...

Windows 数据类型 (Windows Data Types)

参考:https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types 要求 要求值最低受支持的客户端Windows XP [仅限桌面应用]最低受支持的服务器Windows Server 2003 [仅限桌面应用]HeaderBaseTsd.h;WinDef.h;WinNT.hAPIENTRY 系统函数的调用约…...

九龙证券|本周5只新股申购,特斯拉、蔚来、理想的供应商来A股了!

据现在组织,2月13日到17日共有5只新股申购,其间上证主板2只,深证主板1只,北交所2只。 2月14日发动打新的深证主板新股多利科技成立于2010年,是一家专心于轿车冲压零部件及相关模具的开发、出产与出售的企业。从2020年…...

设计模式(持续更新)

本文主要是记录java的设计模式在实际工作中的应用案例,或者是对设计模式的个人理解及备忘 一、单例模式Singleton 工作场景(静态类): 在外部系统对接中,需要调用外部系统A的接口,但是接口是有身份校验的…...

Java 语言特性(面试系列2)

一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...

微信小程序之bind和catch

这两个呢,都是绑定事件用的,具体使用有些小区别。 官方文档: 事件冒泡处理不同 bind:绑定的事件会向上冒泡,即触发当前组件的事件后,还会继续触发父组件的相同事件。例如,有一个子视图绑定了b…...

【第二十一章 SDIO接口(SDIO)】

第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

Leetcode 3577. Count the Number of Computer Unlocking Permutations

Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接:3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯,要想要能够将所有的电脑解锁&#x…...

基于当前项目通过npm包形式暴露公共组件

1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹,并新增内容 3.创建package文件夹...

oracle与MySQL数据库之间数据同步的技术要点

Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异,它们的数据同步要求既要保持数据的准确性和一致性,又要处理好性能问题。以下是一些主要的技术要点: 数据结构差异 数据类型差异&#xff…...

linux 下常用变更-8

1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...

css的定位(position)详解:相对定位 绝对定位 固定定位

在 CSS 中,元素的定位通过 position 属性控制,共有 5 种定位模式:static(静态定位)、relative(相对定位)、absolute(绝对定位)、fixed(固定定位)和…...

css3笔记 (1) 自用

outline: none 用于移除元素获得焦点时默认的轮廓线 broder:0 用于移除边框 font-size&#xff1a;0 用于设置字体不显示 list-style: none 消除<li> 标签默认样式 margin: xx auto 版心居中 width:100% 通栏 vertical-align 作用于行内元素 / 表格单元格&#xff…...

STM32HAL库USART源代码解析及应用

STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...