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

news2024/11/29 12:42:47

文章目录

  • 📖 前言
  • 1. 虚函数表
      • 1.1 虚函数表的引入:
      • 1.2 基类的虚表:
      • 1.3 派生类虚表:
  • 2. 多态的原理
      • 2.1 多态虚函数的调用和普通函数的调用:
        • 2.1 - 1 到底什么是多态(重点)
        • 2.1 - 2 父类的指针实现多态
        • 2.1 - 3 父类的引用实现多态
        • 2.1 - 4 父类的对象实现多态(懂原理)
      • 2.2 动态绑定与静态绑定:
  • 3. 探索虚表
      • 3.1 虚函数重写的过程:
      • 3.2 单继承 - 打印虚表:
      • 3.3 虚表存在哪个区域:
      • 3.4 多继承 - 虚表打印:
        • 3.5 通过汇编深度探索底层调用

📖 前言

上一章我们学习了多态的形式和如何使用多态,这一章我们将来讲一讲多态的原理,搬好小板凳准备开讲啦…

前情回顾:👉 认识多态 + 多态的条件及其性质


1. 虚函数表

1.1 虚函数表的引入:

先来一道笔试题:sizeof(Base)是多少?

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,而现在答案确实12,这是怎么回事?

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

如图所示:

在这里插入图片描述
只有虚函数才会进虚表,用来实现多态。

解释:

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

1.2 基类的虚表:

先看代码:

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

子类的虚表:
在这里插入图片描述

  • 一个对象的vfptr也是在构造函数的时候才初始化的
  • 虚基表中只存虚函数的地址
  • 在对象里面间接存的
  • 普通函数和虚函数都是存在一个地方的,编译好了之后都是放在代码段。
  • 虚表本质上是函数指针数组,存的是虚函数的指针

1.3 派生类虚表:

先来看一下有虚函数的类实例化对象的大小:
在这里插入图片描述
结果:子类的大小是12。

综上小结:

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

并且我们发现:
在这里插入图片描述

  • 在我们重写了func1之后,两个虚表中的func1的地址不一样,但是func2的地址却是一样的。
  • 对于基类虚表的第一个位置存的是基类的虚函数地址,对于子类虚表的第一个位置存的是子类的虚函数地址。
  • 虚函数重写 – 语法层的概念 – 派生类对继承基类虚函数实现进行了重写
  • 虚函数覆盖 – 原理层的概念 – 子类的虚表,拷贝父类虚表进行了修改,覆盖重写那个虚函数(可以这样理解)

2. 多态的原理

2.1 多态虚函数的调用和普通函数的调用:

2.1 - 1 到底什么是多态(重点)

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

多态调用:
在这里插入图片描述

  • 第一个p看到的是父类对象
  • 第二个p看到的则是被切片的父类对象

2.1 - 2 父类的指针实现多态

普通调用和多态调用:

在这里插入图片描述
我们来看一下汇编:
在这里插入图片描述
所以,就引入了以下的结论:

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

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

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

注意:

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

2.1 - 3 父类的引用实现多态

在这里插入图片描述

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

2.1 - 4 父类的对象实现多态(懂原理)

1.父类赋值给子类对象,也可以切片,为什么实现不了多态?
在这里插入图片描述
(1)从汇编语法编译器的角度分析:

  • 编译器的做法非常的死板,就是去检查是否符合多态的条件
  • 符合多态的条件就是运行时决议,要去虚表中找
  • 不符合就是编译时决议,这时候就是用类型确定,是什么类型就去调用谁的

(2)从最底层来分析为什么不支持对象实现多态:

  • 对象切片的时候,是将子类中父类的那一部分拷贝

2.问题来了,切片拷贝的时候,父类那一部分的虚表指针要拷贝过去吗?

在这里插入图片描述
答案:

  • 子类只会拷贝成员给父类对象,不会拷贝虚表指针

3.为什么呢?

  • 因为拷贝虚表指针之后就混乱了
  • 父类对象中到底是父类的虚表指针还是子类虚表指针?
  • —— 都有可能。
  • 那下面调用是调用父类的虚函数还是子类虛函数?
  • —— 就不确定。

在这里插入图片描述

4.如果虚表拷贝了的话:

在这里插入图片描述

  • 因为虚表如果被拷贝过去的话,ptr此时指向父类,此时ptr去调用Func1的时候,按照多态来说是要调用父类的Func1,但是此时调用的却是子类的Func1,乱套了。
  • 此时就是指向子类调子类,指向父类调用的也是子类了,就不是多态了!!
    在这里插入图片描述
  • 那么这时候有个指针指向父类对象,那么调用的是父类还是子类呢?
  • 指向父类却有可能调用子类也有可能调用父类,此时就分不清了!!
  • 所以不能拷贝虚表!!
  • 所以对象不能实现多态,想实现也不能 – 乱套了
  • 对象无法实现多态 – 对象要实现多态必然要拷贝虚表,一拷贝虚表就乱了
  • 一个父类对象如果被切片过,就指向子类,没切片过就指向父类

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

5.显然编译器也没有拷贝虚表:

在这里插入图片描述

2.2 动态绑定与静态绑定:

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
  • C++中:动态是运行时,静态是编译时
  • 静态库: 链接阶段去链接的
  • 动态库: 程序运行起来才会去加载动态库

动静态多态:

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

3. 探索虚表

3.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窗口看到的虚函数表不一定是真实的,可能被处理过
  • 虚函数的地址不在对象里面,而是在虚表指针指向的虚表里面

通过内存窗口看:

在这里插入图片描述
问题:

  • 我们上文所述,vfptr是一个函数指针数组首元素的地址(指针),那么我们通过该指针就能找到虚表(函数指针数组)中的元素(函数指针)
  • 我们通过内存窗口发现了vfptr指向的空间前两个元素(函数指针)都是可以从监视窗口中的虚表中核对
  • 唯独第三个虚函数也就是图中圈出来的那一串不知道是不是虚函数Func4的指针?

3.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.3 虚表存在哪个区域:

虚表存在哪个区域?

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

我们来验证一下:

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

	return 0;
}
  • 同一个类型都是指向一张表的,同一个类型的不同对象它们的虚表都是一样的。
  • 同一个类型的对象共用一份虚表,没必要搞多个
  • 虚表最好能够永久存储。
    在这里插入图片描述

深入理解:(重点)

按理来说在编译的时候就建好了虚表,对象在构造的时候才初始化虚表,其实不是初始化虚表,而是把这个类型的虚表找到,虚表的地址放在对象的头四个字节上。而是在对象初始化列表的时候挨个给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;
}
  • str在栈,但是str指向的空间在 常量区/代码段
  • p在栈,但是p指向的空间在
  • 函数编译完了是一段指令,第一句指令的地址就可以认为是函数的地址!!

运行结果:
在这里插入图片描述
结合上图几个地址来看,充分的说明了虚表是存在 常量区/代码段 中的:

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

3.4 多继承 - 虚表打印:

多继承代码:

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

我们再来通过监视窗口看一下子类对象:

在这里插入图片描述

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

  • 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了
  • Base2的虚表我们可以通过如下的操作找到其vfptr:
    *
  • 去掉红框框出来的,我们取到的是红色箭头指向的Base1中的vfptr,是我们之前取到头四个字节的办法
  • 而下面我们是先取到d的地址,加一整个Base1对象大小个字节就能指向Base2中的vfptr了
  • 因为&d是Base1的指针,Base1的指针加减是跳过一整个对象大小的字节
  • 我们需要先将&d强转成char* 类型的指针,这样指针加减就是跳过一个字节了

见去下代码:

typedef void(*VFPTR)();

//vTable是指向函数指针数组首元素的指针
void PrintVFTable(VFPTR* a)
{
	printf("vfptr:%p\n", a);

	for (size_t i = 0; a[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, a[i]);

		//有函数的地址调用函数 -- 通过函数打印出结果便于观察
		VFPTR f = a[i];
		f();
	}
	cout << endl;
}

int main()
{
	Derive d;
	PrintVFTable((VFPTR*)(*((int*)&d)));
	PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));

	return 0;
}

在这里插入图片描述
没有重写的虚函数放在第一张虚表,第二张虚表不放。

多继承子类对象模型:
在这里插入图片描述


补充:
在这里插入图片描述

  • 首先这三个指针的值是不一样的
  • 其次这三个指针的意义也是不一样的
  • 这里发生了切片
    在这里插入图片描述
  • ptr1和ptr2之间差了8个字节,正好是一个Base1的大小
  • ptr1和ptr3指向同一个位置并且大小一样,但是意义不一样
  • ptr1向后“看”的是Base1,ptr3向后看的是Derive

我们此时发现一个问题:
在这里插入图片描述

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

我们直接将Derive中虚函数func1的地址打印出来:

在这里插入图片描述

  • 我们将地址打印出来之后
  • 惊奇的发现和两个虚表中的函数地址,没一个一样的

原因:

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

3.5 通过汇编深度探索底层调用

我们通过汇编逐步探索整个过程:

Base1虚表调用Derive::func1:

在这里插入图片描述

Base2虚表调用Derive::func1:

在这里插入图片描述
通过汇编逐层调用的结果来看:

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

我们上述过程是直接取到虚表中的内容直接通过虚表中存储的函数指针去调用(函数名就是函数地址),我们直接搞一个多态调用如下图:
在这里插入图片描述
经过本人验证,和上面两个图的过程一样!!

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

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

修正图:
在这里插入图片描述

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

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

相关文章

Allegro基本规则设置指导书之Analysis Modes

Allegro基本规则设置指导书之Analysis Modes 下面介绍基本规则设置指导书之Analysis Modes 点击set-up-constrains-Modes 调出Analysis Modes,这个是所有DRC的总开关 下面介绍常用的一些开关设置 Design Options (Soldermask) 从上往下 阻焊到阻焊的间距 阻焊到pad和走线…

EasyCVR及智能分析网关在校园视频融合及明厨亮灶项目中的应用方案设计

随着校园智能化需求的不断增长&#xff0c;越来越多的校园逐渐开始升级校园监控视频平台&#xff0c;将原先传统的视频监控系统&#xff0c;逐渐升级转变为灵活性强、视频能力丰富、具备AI检测能力、并能支持视频汇聚与统一管理的智能化校园综合管理平台。 在某学校的视频监控…

MapReduce概述

MapReduce概述 MapReduce是一个分布式运算程序的编程框架&#xff0c;是用户开发“基于Hadoop的数据分析应用”的核心框架。 MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序&#xff0c;并发运行在一个Hadoop集群上。 MapReduce…

【优化算法】鹈鹕优化算法(POA)(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑…

ARMv7/ARMv8/ARMv9架构你不知道的那些事

快速链接: . &#x1f449;&#x1f449;&#x1f449; 个人博客笔记导读目录(全部) &#x1f448;&#x1f448;&#x1f448; 付费专栏-付费课程 【购买须知】: 【精选】ARMv8/ARMv9架构入门到精通-[目录] &#x1f448;&#x1f448;&#x1f448; 以下仅代表个人观点&…

七大排序算法——快速排序

AcWing算法专题——快速排序 文章目录AcWing算法专题——快速排序前言一、快速排序的核心二、算法图示三、算法代码四、算法详解1.递归停止的条件2.下标移动的实现3.下标初始值的设定4.边界情况分析4.1区间划分4.2 do while循环条件五、思考题总结前言 现在我们开始进入算法模块…

【第一章 虚拟机】

第一章 虚拟机 1. 虚拟机VM ①虚拟机就是一台虚拟的计算机&#xff0c;它是一款软件&#xff0c;用来执行一系列虚拟计算机指令。 ②虚拟机分为系统虚拟机和程序虚拟机。系统虚拟机&#xff08;比如VMware&#xff09;,它们完全是对物理计算机的仿真&#xff0c;提供了一个可运…

瑞芯微rk3568移植openbmc(三)

2022.11.04 更新 1、关于h264 novnc openbmc中使用的ipkvm其server端调用的是libvncserver库&#xff0c;而其web client端调用的则是novnc的库&#xff0c;既上篇研究修改了libvncserver后&#xff0c;再次继续研究了一下novnc。 Github搜索一圈以后&#xff0c;发现https://…

Java设计模式之单例模式详细讲解

设计模式与单例模式 1、什么是单例模式 ​ 单例模式是指保证某个类在整个软件系统中只有一个对象实例&#xff0c;并且该类仅提供一个返回其对象实例的方法&#xff08;通常为静态方法&#xff09; 2、单例模式的种类 ​ 经典的单例模式实现方式一般有五种 2.1 饿汉式 //…

【SpringBoot】SpringBoot整合SpringSecurity+thymeleaf实现认证授权(配置对象版)

一.概述 1.框架概述 Spring Security 是 Spring 家族中的一个安全管理框架&#xff0c;Spring Security 的两大核心功能就是认证&#xff08;authentication&#xff09;和授权&#xff08;authorization&#xff09;。 认证 &#xff1a;你是什么人。授权 &#xff1a;你能…

RHCE学习 --- 第一次作业

RHCE学习 — 第一次作业 首先我们先设置网卡开机自动启动 vi /etc/sysconfig/network-scripts/ifcfg-ens160 最下面修改ONBOOTyes 然后安装好chrony&#xff0c;配置/etc/chrony.conf文件&#xff0c;添加题目要求的时间服务器 然后设置一个例行性工作&#xff0c;每天早上九…

美国FBA海运专线究竟是什么?美国fba海运专线都有那些?

美国FBA海运专线究竟是什么?美国FBA特别航运线&#xff0c;顾名思义就是海运发送的特别货运线&#xff0c;那么它的收费标准是什么呢?一、美国FBA海运专线究竟是什么 美国FBA特别航运线&#xff0c;顾名思义就是海运发送的特别货运线&#xff0c;那么它的收费标准是什么呢? …

猿创征文|『编程与创作』10款颜值颇高的宝藏工具

&#x1f31f;个人主页&#xff1a;Mymel_晗&#xff0c;一名喜欢鼓捣 Java 的在校学生。 &#x1f31f;撸代码本来是一件枯燥的事情&#xff0c;而一款高颜值工具加持可能会让你事半功倍&#xff0c;今天就给大家推荐一下我在大学学习中发现的几款颜值工具~ 从写代码&#xff…

录音m4a怎么转换成mp3

有小伙伴问手机录音文件电脑上播放不了怎么办&#xff1f;这是很多小伙伴在用手机录完音后遇到最多的问题&#xff0c;尤其是那些第一次遇到这个问题的人&#xff0c;根本不知道这是什么原因导致的&#xff0c;还总以为自己的录音文件出了问题&#xff0c;回去检查发现手机上还…

03-Nginx性能调优与零拷贝

目录 Nginx 性能调优 零拷贝&#xff08;Zero Copy&#xff09; 零拷贝基础 A、 实现细节 B、 总结 零拷贝方式 A、 实现细节 B、 总结 A、 实现细节 B、 总结 A、 实现细节 B、 总结 Nginx 性能调优 在 Nginx 性能调优中&#xff0c;有两个非常重要的理论点&#xff08;面试点…

【C++红黑树】带图详细解答红黑树的插入,测试自己的红黑树是否正确的代码

目录 1.红黑树的概念 1.1红黑树的特性&#xff08;41&#xff09; 2.红黑树的框架 3.红黑树的插入 3.1parent在grandfather的左边 3.1parent在grandfather的右边 4.测试自己的红黑树是不是平衡的 1.红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个…

米联客FDMA3.1数据缓存方案全网最细讲解,自创升级版,送3套视频和音频缓存工程源码

米联客的FDMA数据缓存方案发布也有五六年了&#xff0c;但真正能熟练使用的兄弟却很少&#xff0c;其实还是没有好的例程作为参考和同熟易懂的讲解&#xff0c;这里我做如下解析&#xff1a; FDMA部分&#xff1a;这部分是米联客封装了用户接口的AXI4-FULL协议代码&#xff0c;…

xhs-web校验流程分析

经测试&#xff0c;cookie中需携带gid和timestamp2。参数整理有点乱&#xff0c;仅供参考。 xhsFingerprintV3&#xff0c;VERSION: ‘2.1.2’ 文章目录流程概述timestamp2滑块验证参数Params轨迹FNcaptcha deviceIdProfileDatax-s-commonx5生成x8生成x9生成smidV2a1x-b3-trac…

Jenkins配置linux节点

之前在Windows下安装Jenkins 但是通过windows节点进行构建有诸多的不便&#xff0c;于是想到通过Jenkins里添加linux节点&#xff0c;让构建的时候&#xff0c;使用远程的linux服务器构建 目录一、配置凭据二、配置节点一、配置凭据 Manage Jenkins → Manage Credentials→Je…

Cadence Allegro PCB设计88问解析(十三) 之 Allegro中artwork层的建立

一个学习信号完整性的layout工程师 作为layout工程师&#xff0c;我们经常接触到的是PCB文件&#xff0c;用Cadence设计的是.brd文件。但是我们发给板厂的都是gerber文件。这就涉及到在我们设计好PCB文件之后&#xff0c;怎么把这些文件给到板厂。也就是我们Allegro中的artwork…