【C++】多态---下(多态的原理)

news2024/11/15 5:50:28

前言:

     在多态---上中我们了解了什么是多态,以及多态的使用条件等。本章将进行更深入的学习,我们详细理解多态的原理。

目录

(一)虚函数表

(1)虚函数表的引入

(2)虚表

1、基类的虚表

2、派生类的虚表

3、小结

(二)多态的原理

(1)到底什么是多态?

(2)多态虚函数的调用(进一步详解)

(三)动态绑定与静态绑定

(四)探索虚表

(1)虚函数重写的过程

(2)虚表的打印

(3)虚表存放在哪个区域

(五) 多继承 - 虚表打印

(五)菱形继承、菱形虚拟继承


(一)虚函数表

(1)虚函数表的引入

首先,我们先来做一道笔试题:

sizeof(Base)是多少?

#include<iostream>
using namespace std;
class Base
{
public:
	void Func1()
	{
		cout << "Func1()" << endl;
	}

	virtual void Func2()
	{
		cout << "Func3()" << endl;
	}

	virtual void Func3()
	{
		cout << "Func1()" << endl;
	}

private:
	int _b = 1;
	char _ch;
};

//有了虚函数之后对象中就有了一个表 -- 虚表(虚函数表)
int main()
{
	Base b;
	cout << sizeof(Base) << endl;

	return 0;
}

根据我们之前计算结构体大小的经验和内存对齐等知识,得出的答案应该是8.

但是运行结果却不太对劲:

 这是为什么呢???
这里我们就引入了虚函数表的指针:

  • 有了虚函数之后对象中就有了一个表 – 虚表(虚函数表)
  • 虚函数都会放到虚表当中去,虚表中有虚函数的指针

我们调试一下,如图:

 如介绍的一样,b对象中多了一个指针,这个指针就是虚函数表的指针。

只有虚函数才会进虚表,用来实现多态。

解释:

  • v - 是virtual的单词首字母
  • f - 是function的单词首字母
  • ptr - 是pointer的单词缩写

(2)虚表

1、基类的虚表

我们给出一段代码:
 


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;
	}

	void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;

	return 0;
}

 子类的虚表:

 

  • 一个对象的vfptr也是在构造函数的时候才初始化的
  • 虚表中只存虚函数的地址
  • 在对象里面间接存的

  •  普通函数和虚函数都是存在一个地方的,编译好了之后都是放在代码段。
  •  虚表本质上是函数指针数组,存的是虚函数的指针

2、派生类的虚表

还是以上面那个代码为例,调试得到派生类对象d的组成部分如下:

综上:

  • 很明显和上述结果一样,子类对象中还是存在一个虚表。
  • 父类对象的虚表里面存的是父类的虚函数地址
  • 子类对象的虚表里面存的是子类的虚函数地址

3、小结

我们再把上面调试的部分整合到一起再来观察:

 我们发现在我们重写了func1之后,两个虚表中的func1的地址不一样,但是func2的地址却是一样的。

这是因为我们再子类中对Func1进行了重写(覆盖),重写完实际上是一个区别于父类的Func1的新函数,所以地址不一样;而Func2我们并没有进行重写,他继承了下来,所以地址不变。


我们还发现对于构成多态的每一个类都有自己的虚表(他们的地址并不一样)

也就是说对于基类虚表的第一个位置存的是基类的虚函数地址,对于子类虚表的第一个位置存的是子类的虚函数地址。


这样我们业科技理解虚函数重写又名覆盖的原因了:

  • 虚函数重写 – 语法层的概念 – 派生类对继承基类虚函数实现进行了重写
  •  虚函数覆盖 – 原理层的概念 – 子类的虚表,拷贝父类虚表进行了修改,覆盖重写那个虚函数(可以这样理解)

(二)多态的原理

(1)到底什么是多态?

  • 多态是父类指针指向父类对象就调用父类的虚函数,指向子类的对象就去调用子类的虚函数。
  • 所以到底要去调用哪个函数不是按照指针的类型定的,而是去到指向的对象中去查表。
  • 指向谁就在谁的虚表中找虚函数对应的地址 – 这是多态

ps:

同一个类型都是指向一张表的,同一个类型的不同对象它们的虚表都是一样的。(重点)


(2)多态虚函数的调用(进一步详解)

父类指针调用多态时:

我们来看一下汇编代码:

 

所以,就引入了以下的结论:

  •  多态调用: 运行时决议 – 运行时确定调用函数的地址(去对象的虚表中找函数的地址)
  •  普通调用: 编译时决议 – 编译时确定调用函数的地址(普通函数地址放在符号表,方便链接)

 

多态能够实现的依赖基础是:虚表完成了覆盖:

  • 父类对象的虚表里面存的是父类的虚函数地址
  • 子类对象的虚表里面存的是子类的虚函数地址

注意:

  • 这些地址不是直接存在对象里的,是间接存的,对象里存的是一个指针,这个指针指向的表是虚表,虚表中存的是虚函数的地址。
  • 不要和继承中菱形虚拟继承中的虚基表弄混了,虚基表中存的是偏移量。

 父类引用调用多态时:

  • 因为引用和指针一样都能发生切片,指针和引用底层是一样的。

(三)动态绑定与静态绑定

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

  • C++中:动态是运行时,静态是编译时
  •  静态库: 链接阶段去链接的
  •  动态库: 程序运行起来才会去加载动态库

动静态多态:

  • 编译时 – 静态的多态: 函数重载·
  • 运行时 – 动态的多态: 本节内容讲的这个

(四)探索虚表

(1)虚函数重写的过程

我们来看下面的代码:

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;
	}

	void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
	
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	Derive d;

	return 0;
}

调试得到:

  • 子类的虚表是继承了父类的虚表
  • 子类的虚表可以认为是将父类的虚表拷贝过来,然后将自己重写的虚函数Func1进行覆盖
  • Func2是从父类虚表留下来的,Func1是子类在虚表中重写的

但是子类中明明还有一个虚函数Func4,我们为什么看不到?

其实

  • 和继承那一章节一样,vs的监视窗口在复杂的情况下被处理过,看到的就不准了
  • 此时就需要我们看内存窗口了
  • VS窗口看到的虚函数表不一定是真实的,可能被处理过
  • 虚函数的地址不在对象里面,而是在虚表指针指向的虚表里面

所以下面我们去内存中看:

我们进入虚函数地址查看得:
我们对应的看到了Func1和Func2的函数地址存放在虚函数表里; 

那么黄色框中是否是Func4的地址呢?

我们为了验证,下面我们讲解如何打印虚表。

(2)虚表的打印

下面我们来验证一下:(目的是确认Func4的指针在不在虚表)

  •  取内存值,打印并调用,确认是否是func4

补充:

  • Vs平台下,虚表最后末尾统一放了一个空指针
  • g++平台下不会,g++就得写死
  • 知道几个虚函数,就要尝试打印几个虚函数地址

  • 如何打印虚表:
  • 我们前面也提到了,vfptr是一个函数指针数组首元素(函数指针)的地址(指针),而虚表则是一个函数指针数组

  • 因为函数指针的指针(二级指针)不好定义,我们先typedef一下方便后续使用:
  • 正常的typedef:typedef void(*)() V_FUNC; – 不支持,定义不出来
  • 函数指针有要求,定义变量或者进行typedef都得放在中之间
  • 正确定义:
  • typedef void( * V_FUNC)( );

打印虚表见如下代码:

 

​//正确定义:
typedef void(*V_FUNC)();

//void PrintVFTable(V_FUNC a[]) -- 数组在传参的时候都会退化成了指针

//void PrintVFTable(void(**a)())-- 不用typedef的写法
void PrintVFTable(V_FUNC* a)
{
	printf("vfptr:%p\n", a);

	//**切记这里要记得清理解决方案** -- 不然会有非法访问
	//g++的话在这里就要写死,因为它的虚表中不存在空指针
	for (size_t i = 0; a[i] != nullptr; i++)
	{
		//printf("[%d]:%p\n", i, a[i]);

		printf("[%d]:%p->", i, a[i]);

		//用函数的地址直接去调用函数 -- 通过函数打印出结果便于观察
		V_FUNC f = a[i];
		f();
	}
	cout << endl;
}
  • 我们只需要取到虚表首元素的地址就可以打印虚表了
  •  虚表指针一般是存在对象头上的,也就是前四个字节

我们该如何取对象头上【四个字节】呢?

  • 取子类对象头四个字节是不可以通过强转的:
  • 不相近的类型强转也转不了 – 没有一定关联性的类型不能直接转
  • int* p = (int)d; – 这样是不行的

解决办法(重点理解):

  • 可以将指针强转,先取对象的地址,再强转成int*
  • 指针之间是可以互相转换的 – 任何类型之间的指针都可以互相强转
  • 解引用就拿到了子类对象的前四个字节的地址
  • 再将该地址的类型强转成 函数地址的指针(二级指针) 类型 – 这样才能传的过去
int main()
{
	Base b;
	Derive d;

	//函数指针数组的地址指针

	PrintVFTable((V_FUNC*)(*((int*)&d)));//取到对象头4byte的虚表指针

	return 0;
}

我们通过运行窗口:

  • 见图,结果和我们预想的一样,监视窗口将Func4给隐藏掉了
  • 同时我们还可以直接通过函数指针调用虚函数。

(3)虚表存放在哪个区域

虚表存在哪个区域?

  • 虚表应该是一个类型共用一个虚表,所有这个类型对象都存这个虚表指针
  • Base b1;Base b2;Base b3;Base b4;这几个虚表应该是一样的
  • 所以虚表应该存在一个长期存储的区域

 

深入理解:(重点) 

按理来说在编译的时候就建好了虚表,对象在构造的时候才初始化虚表,其实不是初始化虚表,而是把这个类型的虚表找到,虚表的地址放在对象的头四个字节上。而是在对象初始化列表的时候挨个给vfptr。


首先我们排除虚表是存在栈上的:

  • 因为栈是用来建立栈帧的
  • 栈帧运行结束就销毁了
  • 那么虚表也是时而创建,时而销毁吗
  • 显然不可能

其次我们再排除虚表是存在堆上的:

  • 因为堆区是空间是动态申请的
  • 那么是在什么时候申请,什么时候释放呢
  • 第一个对象申请吗,最后一个对象释放吗?
  • 很显然会很麻烦,可能性也不大

剩下的我们只能猜两个区域:     静态区/数据段       常量区/代码段。

这里猜测放在 常量区/代码段 更合理,因为 常量区/代码段 放的是全局数据和静态数据,因为函数指针数组放在静态区不正常,放在 常量区/代码段 相对来说就很合理。

我们大概的用栈/堆/静态区的一些数据的地址来和虚表地址进行比较:

int c = 2;

int main()
{
	Base b1;
	Base b2;
	Base b3;
	Base b4;

	//打印虚表
	PrintVFTable((V_FUNC*)(*((int*)&b1)));
	PrintVFTable((V_FUNC*)(*((int*)&b2)));
	PrintVFTable((V_FUNC*)(*((int*)&b3)));
	PrintVFTable((V_FUNC*)(*((int*)&b4)));

	//方向验证 -- 对比验证
	int a = 0;
	static int b = 1;
	const char* str = "hello world";
	int* p = new int[10];

	printf("栈:%p\n", &a);
	printf("静态区/数据段:%p\n", &b);
	printf("静态区/数据段:%p\n", &c);
	printf("常量区/代码段:%p\n", str);
	printf("堆:%p\n", p);
	cout << endl;

	printf("虚表:%p\n", (*((int*)&b4)));
	cout << endl;

	//成员函数取地址都得这么玩
	//函数编译完了是一段指令,第一句指令的地址就可以认为是函数的地址
	printf("函数地址:%p\n", &Derive::Func3);
	printf("函数地址:%p\n", &Derive::Func2);
	printf("函数地址:%p\n", &Derive::Func1);
	
	return 0;
}

我们大概的比较一下,发现圈中的几个地址非常相近。

所以:

结合上图几个地址来看,充分的说明了虚表是存在 常量区/代码段 中的:

  • 由结果可得,虚函数和普通函数的地址都差不多
  • 说明虚函数和普通函数的地址都是放在一起的
  • 只不过虚函数要把地址放到虚表里去

(五) 多继承 - 虚表打印

多继承代码:


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;
}

我们调试监视窗口:

 

在之前多继承和菱形继承的基础上,我们再来理解这里:

  • Derive类中继承了两个类Base1和Base2
  • 那么Derive对象中应该就有两张虚表

问题来了,Derive中的fucn3放在哪一张虚表中呢?

  • 显然通过监视窗口观察还是遇到了和上述同样的问题,虚表中内容看不全

我们来打印一下子类中的两个虚表:

  • 我们该如何打印虚表呢?
  • 在之前的多继承中,我们是能知道子类对象的内存结构的
  • Derive对象中Base1对象在前,Base2对象在后,然后是d1成员变量
  • 所以我们和上述办法一样,取到Derive对象头四个字节,就可以打印Base1的虚表

Base2的虚表我们该如何打印呢?

根据Derive对象中内存布局,我们可以知道,Base1中的vfptr后面是Base1的成员变量b1,紧接着就是Base2对象中的vfptr,然后紧接着的是Base2的成员变量b2。
所以我们只需要跳过Base1对象中vfptr指针之后的成员,就可以找到Base2对象中的vfptr了

  • 去掉红框框出来的,我们取到的是红色箭头指向的Base1中的vfptr,是我们之前取到头四个字节的办法
  • 而下面我们是先取到d的地址,加一整个Base1对象大小个字节就能指向Base2中的vfptr了
  • 因为&d是Base1的指针,Base1的指针加减是跳过一整个对象大小的字节
  • 我们需要先将&d强转成char* 类型的指针,这样指针加减就是跳过一个字节了
     

 

 打印出结果如下:

 所以没有重写的虚函数放在第一张虚表,第二张虚表不放。

 

 


补充:

  • 首先这三个指针的值是不一样的
  • 其次这三个指针的意义也是不一样的
  • 这里发生了切片

图示分析:

 

  • ptr1和ptr2之间差了8个字节,正好是一个Base1的大小
  • ptr1和ptr3指向同一个位置并且大小一样,但是意义不一样
  • ptr1向后“看”的是Base1,ptr3向后看的是Derive

我们还发现一个问题:

  • Base1的虚表和Base2的虚表中第一个位置都被子类对象重写了才对
  • 那也就是说,两个虚表的第一个位置都应该是同一个函数的指针才对
  • 但是根据打印虚表的结果来看,并不相同

我么再直接把func1地址打印出来比较:

竟然都不一样!

原因:

  • 这是Windows的自己的机制,多了几层封装
  • 因为它们都不是真函数的地址

我们通过底层的实现来分析:

通过汇编来实现这个过程:

 

 

通过汇编逐层调用的结果来看:

  • 虽然地址不一样但是都调用到了同一个函数
  • 说明它们虽然表面不一样,但是都最终调转到了同一个地址去调用同一个函数
  • 最终都调用到了 “ 006528A0 ” 这个地址!!

对于:

为什么在调用Base2::func1()的时候会比调用Base1::func1()的时候多跳了几层?(重点)

  • Derive对象Base2虚表中func1时, 是Base2指针ptr2去调用
  • 但是这时ptr2发生切片指针偏移(指向Derive对象中Base2那一部分),就需要修正
  • 中途需要修正存储this指针的ecx的值(ecx寄存器存的是this指针)
  • 因为现在调用的是Derive对象的func1那么传给func1的this指针应该是指向Derive对象 “头部” 的指针!!

修正图:

(五)菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的
模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看
了,一般我们也不需要研究清楚,因为实际中很少用。如果好奇心比较强的读者,可以去看下面
的两篇链接文章。
https://coolshell.cn/articles/12165.html
https://coolshell.cn/articles/12176.html

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

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

相关文章

RHCE——时间服务器(ntp)

1.配置ntp时间服务器&#xff0c;确保客户端主机能和服务主机同步时间 2.配置ssh免密登陆&#xff0c;能够通过客户端主机通过redhat用户和服务端主机基于公钥验证方式进行远程连接 一.配置ntp时间服务器&#xff0c;确保客户端主机能和服务主机同步时间 1、软件安装 [rootl…

宝可梦朱紫太晶化效果小记

首先&#xff0c;不得不吐槽一下&#xff0c;switch上这么多代宝可梦下来&#xff0c;好玩是好玩&#xff0c;但是整体效果和优化不能说糟烂&#xff0c;只能说稀碎。 看这个朱紫的截帧都给我看吐了&#xff0c;上点心啊老任 回到效果&#xff0c;首先是实现方式 主要有俩点 …

3.Earth Engine语法Javascript版(基本属性2)

1.地图MAp 1. Map.add(item)这个方法通常是在地图展示区加入各种ui使用&#xff0c;如ui.Label 2.Map.centerObject(object, zoom)设置地图居中位置&#xff0c;参数object是矢量数据或者影响数据&#xff1b;zoom是缩放级别。 3.Map.addLayer(ee.Object, visParams, name, …

树莓派利用python-opencv使用CSI摄像头调用监控视频

目录 一、安装python-opencv。 二、使用工具Xshell7和MobaXterm 三、连接并打开CSI摄像头 3.1连线如图所示&#xff1a; 3.2打开摄像头 四、编写摄像头代码调用摄像头 一、安装python-opencv。 一定要选择配置好的安装python-opencv&#xff0c;不要去配置安装&#xff0c…

012 - C++指针

本期我们将学习 C 中的指针。 指针是一个令很多人都很痛苦的内容&#xff0c;然而指针其实没有大家想象中的那么复杂。另外我先要说明本期我们要讨论的是原始的指针&#xff0c;还有一种常用的指针叫智能指针&#xff0c;这个我们在之后的内容中会接触学习。 计算机处理内存&…

LeetCode_二叉搜索树_中等_236.二叉搜索树的最近公共祖先

目录1.题目2.思路3.代码实现&#xff08;Java&#xff09;1.题目 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有根树 T 的两个结点 p、q&#xff0c;最近公共祖先表示为一个结点 x&#xff0c;满足 x 是 …

jQuery讲解|这一章就够了|(超详细|保姆级)

&#x1f648;作者简介&#xff1a;练习时长两年半的Java up主 &#x1f649;个人主页&#xff1a;老茶icon &#x1f64a; ps:点赞&#x1f44d;是免费的&#xff0c;却可以让写博客的作者开兴好久好久&#x1f60e; &#x1f4da;系列专栏&#xff1a;Java全栈&#xff0c;计…

【设计模式】生产者消费者模型

带你轻松理解生产者消费者模型&#xff01;生产者消费者模型可以说是同步与互斥最典型的应用场景了&#xff01;文末附有模型简单实现的代码&#xff0c;若有疑问可私信一起讨论。 文章目录一&#xff1a;为什么要使用生产者消费者模型&#xff1f;二&#xff1a;生产者消费者模…

JDK 17:Java 17 中的新特性简介

Java 开发工具包 (JDK) 17 将是一个长期支持 (LTS) 版本&#xff0c;预计来自 Oracle 的扩展支持将持续数年。该功能集定于 6 月 10 日冻结&#xff0c;届时 JDK 17 将进入初始阶段。作为 OpenJDK JDK 17 的一部分提交的功能包括&#xff1a; 特定于上下文的反序列化过滤器允许…

计算机网络 实验一

⭐计网实验专栏&#xff0c;欢迎订阅与关注&#xff01; ★观前提示&#xff1a;本篇内容为计算机网络实验。内容可能会不符合每个人实验的要求&#xff0c;因此以下内容建议仅做思路参考。 一、实验目的 掌握在Packet Tracer软件中搭建实验平台&#xff0c;配置基本的网络参数…

8D和A3报告

8D和3A报告&#xff0c;他们都不仅仅是记录问题的一种文书&#xff0c;而是解决问题的工具。 A3发展于TPS &#xff08;Toyota Production system&#xff09;&#xff0c;可以用来解决问题&#xff0c;沟通&#xff0c;记录&#xff0c;是一种流程&#xff0c;当人们在使用A3…

MySQL中添加新字段

© Ptw-cwl 要在MySQL中添加新字段&#xff0c;您可以使用ALTER TABLE语句。 以下是添加新字段的基本语法&#xff1a; ALTER TABLE table_name ADD column_name datatype;其中&#xff1a; table_name 是您要在其中添加新字段的表的名称。column_name 是新字段的名称。…

Linux安装Anaconda

目录1.下载Anaconda的安装包2.安装Anaconda3.用conda创建虚拟环境4.安装项目依赖包1.下载Anaconda的安装包 首先需要在官网上选择需要安装的版本。 官网地址&#xff1a;https://repo.anaconda.com/archive/&#xff0c;如选择当前最新版本进行安装&#xff1a; https://repo.…

TWIST阅读笔记

目录TWIST: Two-Way Inter-label Self-Training for Semi-supervised 3D Instance Segmentation摘要本文方法语义引导的实例提议生成提议纠正基于提议的伪标签更新TWIST: Two-Way Inter-label Self-Training for Semi-supervised 3D Instance Segmentation 摘要 利用无标签数…

浙大版《C语言程序设计实验与习题指导(第3版)》题目集实验2合集

实验2-1-1 计算摄氏温度 本题要求编写程序&#xff0c;计算华氏温度100F对应的摄氏温度。计算公式&#xff1a;C5(F−32)/9&#xff0c;式中&#xff1a;C表示摄氏温度&#xff0c;F表示华氏温度&#xff0c;输出数据要求为整型。 输入格式:本题目没有输入。 输出格式:按照下…

Java每日一练(20230413)

目录 1. 子集 II &#x1f31f;&#x1f31f; 2. 快乐数 ※ 3. 整数反转 ※ &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 子集 II 给你一个整数数组 nums &#xff0c;其中可能…

【pip换源操作】解决用pip下载Python第三方库慢问题

python自带的第三方库使用pip安装速度会很慢&#xff0c;还有可能会报错。 常见的报错信息有&#xff1a; check_hostname requires server_hostname raise ValueError(“check_hostname requires server_hostname”) ValueError: check_hostname requires server_hostname EO…

波尔模型的实验验证之类氢粒子光谱类实验

光谱产生的原因&#xff1a;原子中电子在轨道上跃迁产生&#xff0c;如莱曼系为电子从n2,3,4等轨道跃迁到n1的基态轨道产生。 中心的原点为原子核&#xff0c;中心最接接近原子核的圆为n1的电子轨道。 r_na_0n^2&#xff0c;轨道大小正比于n的平方 根据电子轨道图即可以获得…

AE开发之图层渲染20210603

AE开发之图层渲染比例符号化地图的整饰唯一值符号的符号化过程点符号设置&#xff0c;线符号设置标注图层&#xff0c;&#xff08;写得不好&#xff0c;不推荐看) 唯一值符号化&#xff0c;字段进行设置&#xff0c;这里用到了UniqueValueRenderer接口&#xff0c;这里面有一…

用pyocd读写gd32f4系列mcu的otp区

如前一篇文章所述&#xff0c;pyocd是一个调试、编程cortex-m单片机的简单、强大的工具&#xff0c;本文就结合实例讲解pyocd的一些用法。 使用j-link、dap-link等工具在keil或其它ide中调试单片机程序的场景比较常见&#xff0c;而使用这些工具对单片机片内和片外flash存储区…