【C++进阶之路】继承篇

news2024/12/29 9:35:42

文章目录

  • 前言
  • 一、概念
  • 二、性质
    • 1.赋值转换
    • 2.作用域——隐藏/重定义
    • 3.默认成员函数
      • ①构造函数
      • ②拷贝构造
      • ③析构函数
      • ④赋值重载
    • 4.友元函数
    • 5.多继承
      • ①单继承—— "一脉单传"
      • ②多继承——"一父多子"
      • ③菱形继承—— "一子多父"
      • ④菱形虚拟继承
  • 三、总结

前言

前面我们讲过面向对象的第一大特性——封装,接着我们要面对的就是第二大特性——继承,那继承是啥呢?从功能的角度来说,就是复用
比如:我们都有人的特性(性别,外貌,身份证号),那在社会上,我们可能还是学生、老师、工人等具有身份意义的信息,那如果在描述学生时,我们还需要把人的特性描述一遍,那未免有点太繁琐了,因此,学生,老师,工人就可以复用人的特性信息,再此基础上再添加对应身份特有的信息即可。

一、概念

前面我们通过举例,能够简单理解继承,下面我们来说一下,具体的定义:

  • 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段。
  • 继承允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类
  • 继承是类设计层次的复用。

接下来我们从空间的角度来谈一下继承,一个对象具体划分,可以分为:

  1. 成员函数——常量区
  2. 非静态成员变量——取决于类实例化的作用域(全局:静态区,局部:栈区)
  3. 静态的成员变量——静态区

如何验证空间上的继承呢?我们先讲继承的用法,之后会证明。


下面我们从权限的角度来理解,如何定义一个继承类:

在这里插入图片描述

  • 继承方式有三种——公有继承(最常用),保护继承和私有继承(不常用)。
  • 说明:如果继承方式不写,默认为私有继承。

那继承方式有什么用呢?一张表即可说明:

在这里插入图片描述
总结:

  • 不管什么继承,基类的private成员在派生类中不可见
  • 继承方式,按访问权限小的进行继承,比如基类的public成员,采用protected继承,按权限小的继承,继承之后,基类的public成员是派生类的protected成员。
  • 不写继承方式,默认为私有继承。

protected成员与private成员区别:基类的protect成员在继承之后还能通过派生类在类里进行使用,而private则无法访问。

此时我们再来完成上面的证明:

代码:

class A
{
public:
	void func()
	{
		cout << "A::func()"<<endl;
	}
	int _a = 1;
	static int _c;
};

int A::_c = -1;
class B : public A
{

public:
	int _b = 0;
};
int main()
{
	A a;
	B b;
	//成员函数
	a.func();
	b.func();
	cout << endl;

	//非静态的成员变量
	cout << &a._a << endl;
	cout << &b._a << endl << endl;

	//静态的成员变量
	cout << &a._c << endl;
	cout << &b._c << endl;

	return 0;
}
  • 调试查看反汇编的函数地址:
    在这里插入图片描述
    结论:调用的是用一个函数,因此成员函数继承的是使用权。

再来运行代码:

在这里插入图片描述

  • 结论:非静态成员变量继承的是一份成员变量的拷贝。静态成员变量继承的是使用权。

总结:

成员函数和静态成员变量继承的是使用权。非静态成员变量继承的是一份成员变量的拷贝。

二、性质

1.赋值转换

在讲赋值转换之前,我们得先来搞懂,继承的引用和指针的用法。

class A
{
public:
	void func()
	{
		cout << "A::func()"<<endl;
	}
	int _a = 1;
	static int _c;
};

int A::_c = -1;
class B : public A
{

public:
	int _b = 0;
};
int main()
{
	B b;

	int i = 0;
	int& ri = i;
	const double& di = i;

	A& rb = b;
	A* rptr = &b;

	return 0;
}

我们之前讲过,不同类型的引用中间会生成临时变量,而临时变量具有常属性,因此这里的i转换为double中间会生成临时变量,因此需要加上const。

但是继承之后的类转换为基类,不会生成临时变量,因此没有加上const。这种现象被称之为向上转换,也就是能子类向父类进行转换。

原理:
在这里插入图片描述

  • 相当于是权限的缩小,也就是切割。

拓展:

一个指向派生类的基类指针可以通过安全转换,来转换为子类的指针,从而达成向上转换。

class A
{
public:
	void func()
	{
		cout << "A::func()"<<endl;
	}
	int _a = 1;
	static int _c;
};

int A::_c = -1;
class B : public A
{

public:
	int _b = 0;
};
int main()
{
	B b;
	A* rptr = &b;
	B* rrptr = static_cast <B*>(rptr);
	//B* rrptr = dynamic_cast <B*>(rptr);
	//这个是虚函数才能使用的(多态会将)。
	return 0;
}
  • 说明:能进行引用和指针进行转换的前提是public继承,否则无法转换。

明白了这些,赋值转换就不难理解了。

class A
{
public:
	void func()
	{
		cout << "A::func()"<<endl;
	}
	int _a = 1;
	static int _c;
};
int A::_c = -1;
class B : public A
{

public:
	int _b = 0;
};
int main()
{
	B b;
	A a;
	a = b;
	//编译器生成的为 A & A::operator = (const A &);
	
	//b = a;报错,因此引用支持向下转换,不支持向上转换。
	//且无法进行强制类型转换。
	return 0;
}
  • 赋值转换也就是用了引用的语法,对子类进行切割。

2.作用域——隐藏/重定义

为了理解作用域的性质,我列出一段代码便于理解:

class  Person
{
public:
	int func(int i)
	{
		cout << "Person::func" << endl;
	}

	int _num = 0;

};

class Student : public Person
{
public:
	void func()
	{
		cout << "Student::func" << endl;
		cout << _num << endl;
	}
	int _num = 1;
};

int main()
{
	Student stu;
	stu._num = -1;
	stu.func();
	//stu.func(1);
	return 0;
}

代码的运行结果为:
在这里插入图片描述

  • 为啥不会报错呢?
  • 结果为啥是这样呢?
  • 这里的func构成重载吗?
  • 将注释的代码放开,会产生什么结果?为什么?

我们首先要明白作用域是编译器查找的范围,而作用域包括局部域,全局域,命名空间域,类域作用域限定符的作用是指定在某个域里面查找,否则就报错。

然后我们再来说明这里为啥不会报错,因为继承下来的东西,并不在一个类域里面,又因为不同类域是独立存在的,因此互相会产生屏蔽左右,那么我们一般称由命名相同产生屏蔽的现象,称为隐藏或者重定义。

明白这两个概念,我们再来分析第二个问题,首先对_num赋值-1,先在Studen的类域进行查找,如果找到就停止,很显然这里是对Student的_num进行赋值,然后调用func函数,同理,先对Student进行查找,如果找到就停止,这里找到了,调用的是Student里的func函数,最后查找_num,首先再当前局部域查找,如果没有就在当前类域进行查找,如果还没有就在基类的类域进行查找,如果还没有就在全局域进行查找,如果还找不到就报错。很显然这里是到当前类域就找到了,因此是Student的类域里的_num。

有了隐藏/重定义的概念,这里的func显然是不构成重载的,因为重载要求在同一个类域!

最后将注释的代码放开,会产生编译报错的结果,因为编译器很懒,它找到就不再找了,所以这里查找的还是Student的func。

3.默认成员函数

①构造函数

class A
{
public:
	A()
		:_a(1)
	{}
int _a;
};
class B : public A
{
public:
	B()
		:_b(0)
	{}
int _b;
};
int main()
{
	B a;
	return 0;
}

调试 f11逐语句运行:

在这里插入图片描述

  • 不难看出,在调用子类的构造函数时,先调用了父类的构造函数,构造派生类的成员,然后再调用子类的构造函数,对子类成员进行构造。

为啥要这样设计呢?
个人理解:总不能一个人干两份活吧?你干你的,我干我的,这样分工比较明确,至于先后顺序,可能是因为子类的成员可能会用父类成员的一些值初始化

说明一点:如果子类的初始化列表,没有显示调用父类的构造函数,则调用默认构造函数,如果没有,则报错,这也说明了如果父类没有默认构造,要在子类显示的调用父类的构造函数。

举例:

class A
{
public:
	A(int val)
		:_a(val)
	{}
int _a;
};
class B : public A
{
public:
	B()
		:_b(0)
		,A(1)
	{}
int _b;
};
int main()
{
	B a;
	return 0;
}
  • 强调一点,初始化的顺序与初始化列表的顺序无关,这里构造函数先调用初始化列表中的A的构造函数,再走子类的初始化列表。

②拷贝构造

class Person
{
public:

	Person(const char* name = "张三", int age = 18)
	{
		_name = name;
		_age = age;
	}
	Person(const Person& per)
	{
		_name = per._name;
		_age =  per._age;
	}
private:
	string _name;
	int  _age;
};

class Student :public Person
{
public:
	Student(const char* name = "张三", int age = 18,int id = 12345)
		:Person(name,age)
		,_id(id)

	{
		_id = id;
	}
	Student(const Student& stu)
		:Person(stu)
	{
		_id = stu._id;
	}
private:
	int _id;
};

int main()
{
	
	Student stu2("李四",19,8888);
	Student stu1 = stu2;
	return 0;
}
  • 拷贝构造跟构造函数的区别不大,这里在实现过程中,尤其是子类的拷贝构造在显示地调用父类的构造函数时,会发生向下转换(引用),这里很关键!

  • 另外强调一点,如果不显示调用拷贝构造,会调用默认构造函数,但这样可能不会完成拷贝的效果,如果没有默认构造会报错!

最后总结:构造函数的调用顺序先父后子。

③析构函数

class A
{
public:
	~A()
	{}
};
class B : public A
{
public:
	~B()
	{}
};
int main()
{
	B b;
	return 0;
}

调试运行:
在这里插入图片描述

  • 不难看出,析构子类,先调用子类的析构函数,再调用父类的析构函数

思考一下为啥会这样?

这就跟构造函数有点关系了,我们构造的时候,提过先构造父类的成员变量,再构造子类的成员变量,这样是为了增加子类的信息灵活度,可以让子类的成员跟父类沾上边,如果沾上边的话,析构如果先析构父类,那子类跟父类沾上边的成员的数据就失效无法使用,如果在子类的析构中再进行使用,那么可能就会产生越界等危险行为,因此先析构子类的成员,再析构父类的成员就显得必要了。

④赋值重载

  • 现代写法
class A
{
public:
	A(int a = 0, int b = 0)
		: _a(a)
		, _b(b)
	{}
	void swap(const A& a)
	{
		_a = a._a;
		_b = a._b;
	}
	A& operator = (A a)
	{
		swap(a);
		return *this;
	}
private:
	int _a;
	int _b;

};
class B : public A
{
public:
	B(int a = 0, int b = 0, int c = 0, int d = 0)
		:A(a,b)
		,_c(c)
		,_d(d)
	{}
	void swap(const B& b)
	{
		A::swap(b);
		_c = b._c;
		_d = b._d;
	}
	B& operator =(B b)
	{
		swap(b);
		return *this;
	}
	int _c;
	int _d;
};
int main()
{
	B b1(1,2,3,4);
	B b2(4,3,2,1);
	b1 = b2;
	return 0;
}
  • 普通写法
class A
{
public:
	A(int a = 0, int b = 0)
		:_a(a)
		, _b(b)
	{}
	A& operator = (const A& a)
	{
		if (this != &a)
		{
			_a = a._a;
			_a = a._b;
		}
		return *this;
	}
private:
	int _a;
	int _b;

};
class B : public A
{
public:
	B(int a = 0, int b = 0, int c = 0, int d = 0)
		:A(a,b)
		,_c(c)
		,_d(d)
	{}
	B& operator =(const B& b)
	{
		if (this != &b)
		{
			//因为基类的私有成员,在派生类中不可见,所以我们需要调用基类的赋值进行拷贝。
			A::operator =(b);
			_c = b._c;
			_d = b._d;
		}
		return *this;
	}
	int _c;
	int _d;
};
  • 继承多的就是重定义+向上转换

4.友元函数

C++11 标准不允许友元函数的声明有默认参数,除非友元声明是一个定义
个人理解:为了防止不合适参数,如还未被定义的类的匿名对象,可能直接报错。

class B;
class A
{
	friend void func(const A& a,  const B& b);
public:

private:
	int _a = 1;
};

class B :public A
{
public:

private:
	int _b = 2;
};
void func(const A& a, const B& b = B())
{
	cout << a._a << endl;
	cout << b._a << endl;
	//cout << b._b << endl;
}
int main()
{
	B b;
	func(b,b);
	return 0;
}

代码结果:
在这里插入图片描述

将注释的代码放开:
在这里插入图片描述

  • 结论:友元类是无法被子类继承的,友元是仅限于突破父类作用域,也就是说,友元函数可以访问子类中的父类,也就是作用域在父类中,而子类的作用域则无法访问。

总结:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,但是子类中的基类的私有和保护成员可以通过基类的友元进行访问。

5.多继承

①单继承—— “一脉单传”

在这里插入图片描述

②多继承——“一父多子”

在这里插入图片描述

③菱形继承—— “一子多父”

  • 多继承衍生出来的问题。
    在这里插入图片描述
class Person
{
protected:
	string _sex;
};
class Student : public Person
{
protected:
	int _id;
};
class Teacher : public Person
{
protected:
	string _profession;
};
class Assistant : public Student,public Teacher
{
protected:
	int _age;
};
int main()
{
	Assistant a;
	a._sex = 1;
	return 0;
}

对象模型:
在这里插入图片描述

说明:多继承从左往右进行继承。单继承先继承父类。也就是多继承从左往右开始,从上往下进行画对象模型,单继承从上往下,先画父类再画子类。

  • 很显然,第一个问题,一个人不可能有两种性别吧?这样就导致了数据的冗余
  • 第二个问题,上述代码会报错,因为不知道访问的是哪一个_sex,必须得指定作用域,这样就导致了二义性

那如何解决这个问题呢?

④菱形虚拟继承

如何实现呢?

class Person
{
public:
	int _number = 10;
};
class Student : virtual public Person
{
public:
	int _id = 6;
};
class Teacher :virtual public Person
{
protected:
	int _telephone= 1;
};
class Assistant : public Student, public Teacher
{
public:
	int _age = 18;
};
int main()
{
	Assistant a;
	return 0;
}
  • 在基类衍生出来的第一代派生类的继承方式前加上virtual——虚拟继承

那其解决数据冗余和二义性的原理是什么呢?

第一步画出对象模型:
在这里插入图片描述
我们看出来,原来存Person的位置变了,取而代之的是类似与地址的数据。

我们再来验证一下是不是我们想的那样。
在这里插入图片描述

  • 很显然是的。通过偏移量来进行计算Person位置,从而进行访问。
  • 我们也能观察到偏移量是按照类的对象模型从上往下的规律排列的

有人就要问了,为啥要用一张表存偏移量,而不是直接在原来的位置放上Person 的地址,答案其实很简单,想要知道Person的地址,不也得进行计算吗? 况且如果每个实例化的类都进行计算,那必然是损耗效率的,但是实例化的类都有一个特点那就是 相对位置不会发生改变!因此我们只需计算一次,然后直接根据相对位置进行计算,实例化的类共用一份即可,这样也提升了效率

还有一点,这样在向上转换时就不容易出错

int main()
{
	Assistant a;
	Student stu;
	Student& stu1 = a;
	stu1._number = 1;
	//基类的引用,访问父类的虚基表,得到偏移量1,从而访问Person。
	stu._number = 2;
	//基类的对象,访问基类的虚基表,得到偏移量2,从而访问Person。(偏移量1 != 偏移量2)
	return 0;
}
  • 这样访问的方式相同,由于偏移量的矫正,都能够访问正确的基类!

三、总结

  • 继承由于多继承引发的菱形虚拟继承而变得复杂。如java等OO语言就舍弃了多继承。

说明:OOP——Object Oriented Programming(面向对象编程)

  • 继承的缺陷在于提高了耦合度
    举个例子:
    1 . 黑盒测试:不知道实现,只知道其功能,那我们只需要进行功能上的测试即可。
    2 . 白盒测试:实现暴露出来,也知道其功能,那我们还得理解其实现,才能进行测试。
    继承,更像是一种白盒测试,我们从基类中继承的protect成员还能够进行使用,一旦基类的成员名一改,就会导致派生类的成员无法使用,这也就是耦合度提高的原因。
    组合,更像是一种黑盒测试,我只用功能,你的底层细节我不关心,这样即使你的细节改了,对我没有影响,这降低了耦合度。
    总结:组合更符合高内聚,低耦合的概念。因此我们更提倡使用组合。至于继承应该具体场景下分析再进行使用,尤其是多继承和菱形虚拟继承!

说明:
高内聚 —— 一心只干一件事
低耦合 —— 不同功能的关联程度很小

补充——组合与继承:

class A
{
protected:
	int _a;
};
class B :public A
{
	//继承	
};
class C
{
	class A;//组合
};
  • 继承更像是一种is_a关系,即花是植物,而组合更像是has_a的关系,即车里面有轮胎

 今天的分享就到这里了,如果觉得文章不错,点个赞鼓励一下吧!我们下篇文章再见

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

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

相关文章

【数据分享】1999—2021年地级市固定资产投资和对外经济贸易数据(Shp/Excel格式)

在之前的文章中&#xff0c;我们分享过基于2000-2022年《中国城市统计年鉴》整理的1999-2021年地级市的人口相关数据、各类用地面积数据、污染物排放和环境治理相关数据、房地产投资情况和商品房销售面积、社会消费品零售总额和年末金融机构存贷款余额、地方一般公共预算收支状…

第二十章 原理篇:CLIP

参考教程&#xff1a; 【PDF】Learning Transferable Visual Models From Natural Language Supervision https://github.com/openai/CLIP 文章目录 概述方法自然语言监督创建一个足够大的数据集选择高效的预训练方法代码解读 选择和缩放模型训练使用 代码解读model load rel…

软件测试:单点登录之—单点流程

用户认证中心采用票据传递的方式进行用户信息共享&#xff0c;保证登录会话在不同的站点进行创建。用户访问目标站点时通过当前登录的站点创建票据&#xff0c;传递票据到目标站点&#xff0c;目标站点接收到票据之后调用用户中心认证系统接口进行票据认证&#xff0c;认证成功…

【C语言15】单链表,(对于二级指针与一级指针应用的详细讲述)

文章目录 单链表1.单链表的介绍2.单链表的实现2.1.1单链表结点的创建与销毁2.1.2单链表尾插2.1.3单链表打印2.1.4尾删2.1.5头插2.1.6头删2.1.7查找2.1.8在pos位置之后插入数据2.1.9删除pos位置 单链表 1.单链表的介绍 链表是一种物理存储结构上非连续、非顺序的存储结构&#…

科学家用ChatGPT写完1篇论文仅用1小时!多所高校撤销禁令

自2022年11月发布以来&#xff0c;许多人都在使用ChatGPT来帮助他们完成工作&#xff0c;各行各业的人都在使用&#xff0c;从撰写营销材料&#xff0c;到起草电子邮件&#xff0c;再到生成报告。 ChatGPT作为一种人工智能工具&#xff0c;在科研中也显示非常大的应用潜力&…

汽车行业 Y 公司对接斯堪尼亚 SCANIA EDI 项目案例

斯堪尼亚是一家来自瑞典的重型车辆制造公司&#xff0c;成立于1891年&#xff0c;总部位于斯德哥尔摩&#xff0c;主要专注于生产卡车、客车和工业发动机&#xff0c;以及相应的服务与解决方案。斯堪尼亚的产品以其高品质、可靠性和先进技术而闻名。其卡车广泛应用于货运和运输…

大文件传输过程中的网络拥塞控制方法研究

随着网络技术的进步&#xff0c;我们在工作和生活中经常需要传输大文件。但是在实际的文件传输过程中&#xff0c;网络拥塞问题一直是困扰着我们和网络服务商的一个难题。接下来将从网络拥塞的原因、影响以及控制方法等方面详细介绍大文件传输过程中的网络拥塞控制方法。 一、网…

Gnomon室外照明学习

阴影 复制4个 15 开启主光 复制选择45度的那组灯光 关闭阴影 灯光颜色吸取地面 对比 组 X和Z平移归0 整体旋转 太阳调整 吸取图片上面的颜色&#xff0c;天空那组灯 复制一个

【Vscode | R | Win】R Markdown转html记录-Win

Rmd文件转html R语言环境Vscode扩展安装及配置配置radian R依赖包pandoc安装配置pandoc环境变量验证是否有效转rmd为html 注意本文代码块均为R语言代码&#xff0c;在R语言环境下执行即可 R语言环境 官网中去下载R语言安装包以及R-tool 可自行搜寻教程 无需下载Rstudio Vscod…

面试题 -- 客户端安全性和框架设计

文章目录 1. 客户端安全性处理方式2. sip是什么&#xff1f;3. 有些图片加载的比较慢怎么处理&#xff1f;你是怎么优化程序的性能的&#xff1f;4. 实现过一个框架或者库以供别人使用么&#xff1f;5. App需要加载超大量的数据&#xff0c;给服务器发送请求&#xff0c;但是服…

如何测试Linux内核

目录 概述 LTP 构建系统 C测试用例 参考资料 Autotest Kmemleak Kmemcheck Linaro LAVA 调试器 GDB KGDB 设备驱动测试 资料获取方法 概述 在本文中&#xff0c;我们将讨论用于测试Linux内核的各种框架和工具。首先&#xff0c;我们将介绍LTP( Linux Test Proje…

Ribbon 启用规则,SelectionCountRule规则在Classic界面下不生效,只有在UCI界面下才生效

Ribbon 启用规则&#xff0c;SelectionCountRule规则在Classic界面下不生效&#xff0c;只有在UCI界面下才生效。

Python pygame(GUI编程)模块最完整教程(7)

上一篇文章&#xff1a; Python pygame(GUI编程)模块最完整教程&#xff08;6&#xff09;_Python-ZZY的博客-CSDN博客 总目录&#xff1a; README.md Python-ZZY/Python-Pygame最完整教程 - Gitee.com 21 OpenGL与Pygame 不会OpenGL的读者可以跳过本章节。 21.1 OpenGL简…

后端一次返回大量数据,前端做分页处理

问题描述&#xff1a;后端接口返回大量数据&#xff0c;没有做分页处理&#xff0c;不支持传参pageNum&#xff0c;pageSize 本文为转载文章&#xff0c;原文章&#xff1a;后端一次返回大量数据&#xff0c;前端做分页处理 1.template中 分页 <el-paginationsize-chang…

局域网内主机ping不通,但是可以调用对方http接口(防火墙阻止了icmp协议)(关闭防火墙或者启用ICMP回显请求(ICMPv4-In))

文章目录 背景可能的原因问题排查及解决 背景 局域网内有一台主机&#xff0c;ping它ping不通&#xff0c;但是可以调用它的http接口&#xff0c;很诡异。。。 可能的原因 可能的原因有以下几种&#xff1a; 防火墙设置&#xff1a;局域网内的主机可能设置了防火墙&#xff…

【Unity3D日常开发】Unity3D中Package Manager加载不出来插件包或者加载出来后无法Install的问题

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 今天在新电脑上打开Unity3D的Package Manager&#xff08;包管…

<MyBatis>前台传参多个条件查询方式(传数组或者拼接字符串)

方式一&#xff1a;前台传参为数组&#xff0c;后台SQ查询案例&#xff1a; 一般为多选场景&#xff1a;查询&#xff1b; 举例如下&#xff1a; 传值&#xff1a;“status” : [“保存”,“关闭”], 不传值&#xff1a;“status”: [], 传给后台&#xff1a; 控制层&#xff1…

清洁力好的洗地机有哪些品牌、清洁力好的洗地机盘点

清洁力好的清洁工具有很多&#xff0c;但是想要清洁力好的并且又省心省力&#xff0c;快捷高效的洗地机可以说是榜上有名&#xff01;在清洁的时候&#xff0c;洗地机的作用相比传统清洁工具使用更加的便捷&#xff0c;并且清洁力不比传统清洁工具差&#xff0c;同时还衍生了更…

八、seata使用及源码分析

一、数据库事务ACID特性 基础概念&#xff1a;事务ACID A&#xff08;Atomic&#xff09;&#xff1a;原子性&#xff0c;构成事务的所有操作&#xff0c;要么都执行完成&#xff0c;要么全部不执行&#xff0c;不可能出现部分成功部分失 败的情况。C&#xff08;Consistency…

【雕爷学编程】Arduino动手做(93)--- 0.96寸OLED液晶屏模块17

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…