C++ - 多态的实现原理

前言

本博客主要介绍C++ 当中 多态语法的实现原理,如果有对 多态语法 有疑问的,请看下面这篇博客:

 探究,为什么多态的条件是那样的(虚函数表)

 首先,调用虚函数必须是 父类的 指针或 引用,不能是子类的。这是因为,子类当中有父类部分,而父类当中只有自己。我们使用父类指针,当指向对象是父类的时候,指针类型也是相匹配的,就会调用父类的函数;如果指向那个对象是子类的,但是子类当中构造了父类,就会发生切片,因为指针类型是父类的,指针只会访问父类的那一部分。

但是如果指针类型是子类的,就不行了,因为子类虚函数表当中只有自己虚函数的地址。父类指针可以指向子类和父类,但是子类指针只能指向子类。


为什么不能是父类的对象,而必须是指针或引用呢
因为 ,指针的切片和 对象的切片做造成的结果是不一样的

如果是指针切片,在创建派生类的时候,子类当中父类的虚函数表,是先从父类当中拷贝一份到在子类当中父类的虚函数表,然后,如果子类当中重写了虚函数,再把子类当中重写虚函数地址直接覆盖在虚函数表中之前父类中对应的虚函数地址位置

 拷贝之后如下所示:

 所以,才会有指向父类调用父类的虚函数;指向子类调用子类的虚函数;只不过在指针看来,看到的都是对象对象。一个是之间看到父类对象,一个是切片之后看到了子类当中父类对象。所以说,指针的切片不考虑拷贝的问题,就可以理解为他只是把原本就有的部分切片出来给指针看到。

 而对象的切片,像上述的例子, ps = st,相当于是把子类对象拷贝到 ps 当中;因为ps 是父类的指针, 而 st 是子类的指针,这时候就会发生切片。把切片出来的子类当中的父类拷贝到 ps 当中。

在子类的父类当中有两个部分,首先 _a 肯定是会拷贝到 ps  当中,但是虚函数表会不会拷贝呢

我们先来看,拷贝之后会发生什么。如果我们把子类当中的虚函数表拷贝到父类当中的虚函数表当中,那么当指针指向父类的时候,此时应该调用父类的函数,但是此时父类当中的虚函数表存储的是子类的虚函数地址(因为刚刚假设是直接拷贝),那么此时就会去调用子类的虚函数,这部乱套了吗?这肯定不会是我们所期望的,我们肯定期望父类指针指向父类对象 ,就去调用父类的虚函数。

所以,此时肯定不能 把 子类当中的 虚函数表拷贝到 父类当中的虚函数表当中。在实际当中,子类拷贝给父类,编译器也没有拷贝虚函数表,和我们刚刚所想是一样的。

所以说,上述就是我们不能使用父类对象调用虚函数的原因(如果使用对象调用,就要进行赋值拷贝开空间,而新开出来的父类就需要重新构建虚函数表,而又不能直接拷贝原本的虚函数表,原本子类和父类构建的重写关系可能会乱套

 这里提一嘴,我们普通继承(比如 A类 继承 B类),这种称之为 实现继承。而 上述的多态继承称之为 接口继承

至于为什么需要虚函数的重写,上述给出的过程也可以证明了,因为只有是虚函数重写,在子类当中的虚函数表当中才会 有 子类新重写的虚函数地址。而调用虚函数的 父类指针 或 引用,只需要“无脑的” 从 父类当中的虚函数表 ,找到这个虚函数地址,调用这个虚函数就行了。 


 关于虚函数表的一些问题

  • 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  •  基类b对象和派生类d对象虚表是不一样的,这里我们发现BuyTicket完成了重写,所以Student的虚表中存的是重写的Student::BuyTicket,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法.
  • 虚函数表当中只放虚函数的地址,如果不是虚函数,该函数的地址是不会放到虚函数表当中的;
  • 派生类对于父类当中的虚函数表是先拷贝,然后在把派生类重写的虚函数地址进行直接覆盖,如果在派生类当中自己有虚函数,就按照派生类的声明次序写在虚函数表当中(注:VS的调试窗口下不会显示派生类当中的虚函数(不是重写的),但是我们打开内存窗口可以看到派生类虚函数的地址)。也就是说,在派生类中父类的虚函数表当中有三个部分:从父类拷贝下来父类的虚函数;子类重写之后覆盖的虚函数;子类当中的虚函数;
  • 虚函数表的本质是一个 存放 函数指针的指针数组,一般情况下,这个数组在最后放了一个 nullptr(0),但是这个看不同的编译器,在VS下就给了,但是在 g++ 当中没给。
  • 虚函数表是存储在 代码段,也就是常量区当中的。

虚函数表存储在代码段的验证:

  • C++当中数据存储位置大概有以下几个地方:栈  堆   数据段(静态区)   代码段(常量区)。首先排除的是 堆 ,因为堆是拿给我们动态开辟空间的,而虚函数表是由编译器生成的,所以不可能是编译器 什么 malloc new出来的。
  • 栈也不可能,因为同类型的对象共用一个虚函数表(一表多用)(比如 Person ps1 和 Person ps2这两个对象是共用一个虚函数表的,不管这两个对象分别构造在任意位置),而栈上的空间一般是跟着栈帧走的,不能单独开空间。而且如果存储在栈上也有一个问题,就是函数执行结束,战争销毁,存储在这个栈帧当中的虚函数表也要进行销毁,那么当下一个同类型的对象构造的时候,虚函数表难道要重新进行构造吗?肯定是不行的。(除非是 main 函数栈帧)
  • 我们可以来验证一下,我们用 分别存储在上述四个存储位置的  四个数据,分别打印他们的地址,这样我们可以大概的看出这个四个存储位置的地址区间,在打印对象当中虚函数表的地址(用强转类型,int* ,这样解引用的话只会访问 4 个字节的内容),因为这个例子的数据量很小,地址最接近的我们可以认为虚函数表就存储在那个 存储位置:

 我们发现,虚函数表 和 常量区 存储位置地址最接近。

VS当中 虚函数表最后的 nullptr(有时候编译器在你调试的时候修改一些代码,编译器可能不会给nullptr,但是清理一下,重新生成解决方案之后就会有了);

 验证,派生类的虚函数表当中,VS调试窗口看不到的,派生类的虚函数地址:
 

class Person
{
public:virtual void Func1(){cout << "Person::Func1()" << endl;}virtual void Func2(){cout << "Person::Func2()" << endl;}};class Student : public Person
{
public:virtual void Func3(){cout << "Student::Func3()" << endl;}
};

如上例子所示,在子类 Student 中的虚函数表,除了存储父类 Person 的两个虚函数之外,还要存储 子类 当中的 虚函数--func3()的地址,但是 这个 func3 ()函数的地址,在VS的调试窗口上是不会显示的,但是在内存当中,除了 有父类 当中两个虚函数的地址,还多出来一个地址,我们怀疑这个地址的空间存储的就是 func3()函数的地址:
 

调试窗口子类虚函数表没有func3()地址:

 内存窗口当中多出一个地址:

 上述说过,虚函数表其实就是函数指针数组,这个数组当中存储的是每一个虚函数的指针,所以我们可以利用C 当中的函数指针来帮助我们验证这个地址是不是 func3()函数的指针。

我们可以在虚函数表数组当中找到这个地址,然后用这个地址调用这个地址的函数看是不是func3()。

函数指针语法(转自博客:c++ 函数指针_c++指针函数_Alpha205的博客-CSDN博客):

double (*pf)(int);   // 指针pf指向的函数, 输入参数为int,返回值为double

这样不太好看,我们可以typedef一下:

typedef void(*FUNC_PTR) ();

在数组当中找到 这个地址,然后调用这个地址上的 函数:

typedef void(*FUNC_PTR) ();void PrintVFT(FUNC_PTR* table)
{for (size_t i = 0; table[i] != nullptr; i++){printf("[%d]:%p\n", i, table[i]);}printf("\n");
}int main()
{Person ps;Student st;int vft1 = *((int*)&ps);PrintVFT((FUNC_PTR*)vft1);int vft2 = *((int*)&st);PrintVFT((FUNC_PTR*)vft2);return 0;
}

我们拿输出结果看和内存对比,地址是否相同,来验证我们当前取出来的地址是否正确:
 

 我们发现是完全吻合的。

然后我们在把 疑似 func3()函数的函数指针拿出来调用,看这个地址是不是 func3()函数的地址:
 

//打印虚函数表当中 虚函数地址的函数
void PrintVFT(FUNC_PTR* table)
{for (size_t i = 0; table[i] != nullptr; i++){printf("[%d]:%p\n", i, table[i]);FUNC_PTR func = table[i];func();}printf("\n");
}

输出:

[0]:00FC1357
Person::Func1()
[1]:00FC12AD
Person::Func2()[0]:00FC1357
Person::Func1()
[1]:00FC12AD
Person::Func2()
[2]:00FC12D0
Student::Func3()

此时我们就验证了,那个多出来的地址就是 Func3()函数的地址。

 以上验证方式需要注意的点:

  •  首先,这个程序是在 X86 也就是在 32位环境下执行的,也就是说,我们在寻找虚函数表的首地址的时候,是寻找对象前4字节的存储的数据,这个数据就是虚函数表的地址。如果是 64 位环境的话,应该是取 对象的前8个字节。
  • 上述写的PrintVFT()这个函数中的循环,是以 nullptr(0)作为循环的终止条件的,因为在VS当中的虚函数表,在最后会以 nullptr 来结尾。但是有时候我们不注意在调试时候修改代码,可能就不会以 nullptr 来结尾了,这时候我们需要重新生成解决方案。
  • 在Linux中,也就是在g++环境下,虚函数表不是以 nullptr 结尾的,这时候的循环只能写死了。

 看到上述的验证,我们应该注意了,VS当中的监视窗口有时候可能不靠谱,而内存当中是绝对靠谱的。

函数指针(函数地址)在使用的时候需要注意,如果你知道函数的地址,不管这个函数受哪一个权限修饰符修饰,就算是使用 private 修饰,照样可以访问。

因为,此时你都已经知道了这函数的地址,使用函数指针来调用函数,是直接在代码段当中找到这个函数,然后调用。

还有一个原因是,权限的限定只是在语法层次来限定,不是在运行层当中进行限制的。这里的函数指针直接跳过了语法层次,直接在语法层次来进行寻找函数调用。

 动态绑定和静态绑定(动态多态和静态多态)

 其实多态这一现象不止发生在对象当中,在函数的当中时常发生。如下例子:
 

int a = 1;
double b = 1;cout << a << endl;
cout << b << endl;

 库函数当中的 cout 流插入之所以实现自动判别类型,其实底层实现就是使用 函数重载。当我们传入不同类型的参数的时候,编译器就会自动的去寻找参数列表对应的函数来调用。

上述这种用函数重载来实现的多态,就叫做静态多态

而我们上述的多态,也就是使用继承,虚函数来实现的多态,就是动态多态

 多继承当中的虚函数表

 

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

我们先来计算 Deribve d ,这个对象的大小是多少,sizeof(d)= 20;Base1的虚表指针 + b1 + Base2的虚表指针 + b2 + d1 = 20。

派生类是没有自己的虚表的,因为派生类在构造之前需要先构造其父类,它的父类当中就有虚表,而且这个虚表还是子类修改过的(重写就覆盖地址)。

多继承当中,派生类当中创建的多个父类当中都有虚表,我们可以认为,这些虚表都是属于这个派生类的,因为这些虚表当中,如果派生类对其中的虚函数进行了重写,那么都是对这个虚表进行了修改的,就算这个表当中没有,也不影响子类调用函数。 

那么在子类当中func3()这个函数,没有重写,但是是虚函数,那么也要放进虚表当中,但是紫烈继承了两个父类,此时有两个虚表,究竟是放到哪一个虚表当中的呢?

要得到上述问题的答案,我们还是要进行虚表当中虚函数的打印,打印过程和上述一样,唯一不一样的是,base2父类不在 d 对象当中的第一位置,Base1当中的虚表好弄,因为是Base1是在第一位置。所以,此时我们要像去Base1一样先取出第一位置的地址,然后加上 sizeof(base2),因为 所用的指针是 d 类型指针,所以此时还需要把 d 指针强转为 char* ,使得我们加上 sizeof(base2)是一个字节一个字节加的。具体代码如下所示:
 

Derive d;
int vft2 = *((int*)( (char*)&d + sizeof(Base1)));

这样就可以取出d对象当中 Base2 父类当中的虚函数表的地址了。

还有一个更好的方法:使用 Base2 类型的指针,指向子类对象(d),这样就会发生切片,Base2类型的指针直接指向 d 对象当中的 Base2 首地址。虚函数就在首地址处,直接按照4个字节大小取出就好:

Derive d;
Base2* ptr = &d;
int vft2 = *((int*)ptr);
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;int vTableb1 = (*(int*)&d);PrintVTable((VFPTR*)vTableb1);int vTableb2 = (*(int*)((char*)&d + sizeof(Base1)));PrintVTable((VFPTR*)vTableb2);return 0;
}

输出:

 虚表地址>00A69B94第0个虚函数地址 :0Xa61244,->Derive::func1第1个虚函数地址 :0Xa612e9,->Base1::func2第2个虚函数地址 :0Xa61230,->Derive::func3虚表地址>00A69BA8第0个虚函数地址 :0Xa61357,->Derive::func1第1个虚函数地址 :0Xa610b9,->Base2::func2

通过上述输出结果发现,func3()函数的地址是存在 第一个 父类对象的虚函数表当中的。

 


我们还看到,上述在 Base1 当中的 func1()函数的地址 和 在 Base1 当中的 func1()函数的地址是不一样的

我们先来模拟,用两个分别指向 Base1 和 Base2 的指针来调用func1()函数,来看看:
 

int main()
{Derive d;Base1* pb1 = &d;pb1->func1();Base2* pb2 = &d;pb2->func1();return 0;
}

输出结果是一样的:

Derive::func1
Derive::func1

虽然两次函数调用的结果是一样的,但是两次调用函数的地址是不一样的。

我们来查看反汇编,来看看编译器在这里究竟干了些什么,为什么要这样做?

 上述就是我们在main函数的当中写的代码所转化的反汇编截图。

过程描述:

首先是 Base1 指针调用 func1(),call指令调用函数,call指令当中 eax寄存器 存储的地址是 jmp的地址,因为在VS当中,调用函数之前要先走一趟jmp,而jmp跳跃到的地址才是调用函数真真的地址,我们发现,Base1 指针调用 func1()函数就是直接跳到子类重写的 func1()函数地址来进行调用的

 然后我们来看 Base2 指针调用func1()的过程,同样在call指令开始查看,发现此时 eax寄存器当中存储的地址和 Base1当中 eax存储的地址不一样了

 也就是说,此时call之后执行的 jmp 指令也不会是之前的那个指令了:

 此时的jmp 跳到了另一个指令位置当中,此时就只有 sub 和 jmp这两个指令,首先执行 sub 这个指令,这个指令是 减 的意思,意思是 寄存器 ecx 当中的值 减 8。而 ecx 当中存储的值是 this 指针的值,也就是说,sub 指令是让 this 指针 减8。

然后 接下来执行的 jmp指令 就和 Base1 当中的 jmp 指令地址一样的了,也就是执行的是一个指令,此时就跳到了 func1()子类重写的地址处进行执行。我们发现,Base2也是跳到 子类重写的函数当中进行调用 func1()函数的。

 既然,两处最后都是跳到 子类当中对 func1()函数重写位置进行 调用的,那么 Base2 指针调用的 func1()函数为什么要多执行这几步绕一圈在执行呢?

其实不难发现,Base2 指针调用的 func1()函数过程,多执行的几步当中,jmp指令都不重要,重要的是执行的 那个 sub 指令。这个指令对当时的 this 指针进行了修改,那么为什么要对当中的 this 指针进行修改呢?

 首先我们要知道,此时的this指针指向的是什么。此时的this指针,谁调用的,谁就是 this 指针,很明显,此时的this指针是 pb2。而此时的pb2 指向的是 d 子类对象当中的 Base2 这个父类对象,也就是说,此时的this 指针指向的是 Base2 这个对象。

但是,我们此时调用的 func1()函数进行了重写,所以 func1 ()函数的实现是在 子类当中,而不在Base2 当中,当 func1()当中调用了 子类当中的成员函数或成员变量,我们知道,调用成员是需要 this 指针来调用的,如果此时this 指针还是指向 Base2,就出大问题了!! 

 所以,此时编译器就对this指针进行了修改:

 ecx 存储的是 this指针,在最开始是从 ptr2 拷贝过来的this指针的值,ptr2 存储的是 Base2 对象的指针,这肯定是不对的,所以此时编译器才饶了一圈来修改this指针指向的位置。

 那为什么 Base1 类型的指针(ptr1)调用 func1()函数就没有这样饶圈,而是直接跳到 func1()函数实现位置调用呢?

其实是也 ptr1 指针指向位置很特殊,他就是 子类对象 d 的首地址,就是 d 对象的 this指针应该指向的地方,所以此时编译器不需要对this 指针进行修改。

 当然,上述是基于 VS 当中的编译器做到事情,其他编译器不好说。

除了上述方法,我们还可以让两个虚表当中的 func1()函数的地址是一样的,而且同样的可以修改this 指针。

就是不要 那 ptr2 来赋值 ecx ,就算要拿 ptr2 来赋值给 ecx ,在下一行指令就把 ecx 当中的值向上述一样 减8 就行了。

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

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

相关文章

KT142C-sop16语音芯片ic的功能介绍 支持pwm和dac输出 usb直接更新内置空间

1.1 简介 KT142C是一个提供串口的SOP16语音芯片&#xff0c;完美的集成了MP3的硬解码。内置330KByte的空间&#xff0c;最大支持330秒的语音长度&#xff0c;支持多段语音&#xff0c;支持直驱0.5W的扬声器无需外置功放 软件支持串口通信协议&#xff0c;默认波特率9600.同时…

opencv旋转图像

0 、使用旋转矩阵旋转 import cv2img cv2.imread(img.jpg, 1) (h, w) img.shape[:2] # 获取图像的宽和高# 定义旋转中心坐标 center (w / 2, h / 2)# 定义旋转角度 angle 90# 定义缩放比例 scale 1# 获得旋转矩阵 M cv2.getRotationMatrix2D(center, angle, scale)# 进行…

比亚迪海豹:特斯拉强劲对手,瑞银拆解成本比同级车型低15%~35%

瑞银证券日前对中国电动车产品比亚迪海豹进行了拆解&#xff0c;发现海豹具有强大的成本优势&#xff0c;而这个优势主要来自于中国本土生产和国内完善的电动车供应链以及比亚迪的垂直整合体系和零部件高度集成性。比亚迪的整车成本比同级别竞争车型分别低15%至35%。 瑞银预测&…

【100天精通Python】Day55:Python 数据分析_Pandas数据选取和常用操作

目录 Pandas数据选择和操作 1 选择列和行 2 过滤数据 3 添加、删除和修改数据 4 数据排序 Pandas数据选择和操作 Pandas是一个Python库&#xff0c;用于数据分析和操作&#xff0c;提供了丰富的功能来选择、过滤、添加、删除和修改数据。 1 选择列和行 Pandas 提供了多种…

学习Bootstrap 5的第六天

目录 信息警告框 警告框 实例 警告框链接 实例 关闭警告框 实例 警告框动画 实例 按钮 按钮样式 实例 按钮轮廓 实例 ​编辑按钮尺寸 实例 块级按钮 实例 实例 活动/禁用按钮 实例 加载器按钮 实例 扩展小知识 信息警告框 警告框 警告框是使用 .aler…

ETCD详解

一、etcd概念 ETCD 是一个高可用的分布式键值key-value数据库&#xff0c;可用于服务发现。 ETCD 采用raft 一致性算法&#xff0c;基于 Go语言实现。 etcd作为一个高可用键值存储系统&#xff0c;天生就是为集群化而设计的。由于Raft算法在做决策时需要多数节点的投票&…

【算法】归并排序 详解

归并排序 详解 归并排序代码实现1. 递归版本2. 非递归版本 排序&#xff1a; 排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 稳定性&#xff1a; 假定在待排序的记录序列中&#xff0c;存在多个具有相…

eclipse进入断点之后,一直卡死,线程一直在运行【记录一种情况】

问题描述: 一直卡死在某个断点处&#xff0c;取消断点也是卡死在这边的进程处。 解决方式&#xff1a; 将JDK的使用内存进行了修改 ① 打开eclipse&#xff0c;window->preference->Java->Installed JREs&#xff0c;选中使用的jdk然后点击右侧的edit&#xff0c;在…

【算法】插入排序

插入排序 插入排序代码实现代码优化 排序&#xff1a; 排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 稳定性&#xff1a; 假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&…

npm/yarn link 测试包时报错 Warning: Invalid hook call. Hooks can only be called ...

使用 dumi 开发 React 组件库时&#xff0c;为避免每次修改都发布到 npm&#xff0c;需要在本地的测试项目中使用 npm link 为组件库建立软连接&#xff0c;方便本地调试。 结果在本地测试项目使用 $ npm link 组件库 后&#xff0c;使用内部组件确报错&#xff1a; react.dev…

“安全即服务”为网络安全推开一道门

8月30日&#xff0c;三六零&#xff08;下称“360”&#xff09;集团发布了2023年半年报&#xff0c;其中安全业务第二季度收入6.54亿元&#xff0c;同比增长98.76%&#xff0c;环比增长157.16%&#xff0c;安全第二增长曲线已完全成型&#xff01;特别值得一提的是&#xff0c…

高速路自动驾驶功能HWP功能定义

一、功能定义 高速路自动驾驶功能HWP是指在一般畅通高速公路或城市快速路上驾驶员可以放开双手双脚&#xff0c;同时注意力可在较长时间内从驾驶环境中转移&#xff0c;做一些诸如看手机、接电话、看风景等活动&#xff0c;该系统最低工作速度为60kph。 如上两种不同环境和速度…

Vue+NodeJS+MongoDB实现邮箱验证注册、登录

一.主要内容 邮件发送用户注册用户信息存储到数据库用户登录密码加密JWT生成tokenCookie实现快速登录 在用户注册时,先发送邮件得到验证码.后端将验证进行缓存比对,如果验证码到期,比对不正确,拒绝登录;如果比对正确,将用户的信息进行加密存储到数据库. 用户登录时,先通过用…

LRTimelapse 6 for Mac(延时摄影视频制作软件)

LRTimelapse 是一款适用于macOS 系统的延时摄影视频制作软件&#xff0c;可以帮助用户创建高质量的延时摄影视频。该软件提供了直观的界面和丰富的功能&#xff0c;支持多种时间轴摄影工具和文件格式&#xff0c;并具有高度的可定制性和扩展性。 LRTimelapse 的主要特点如下&am…

Leetcode刷题笔记--Hot41-50

1--二叉树的层序遍历&#xff08;102&#xff09; 主要思路&#xff1a; 经典广度优先搜索&#xff0c;基于队列&#xff1b; 对于本题需要将同一层的节点放在一个数组中&#xff0c;因此遍历的时候需要用一个变量 nums 来记录当前层的节点数&#xff0c;即 nums 等于队列元素的…

全网独家:编译CentOS6.10系统的openssl-1.1.1多版本并存的rpm安装包

CentOS6.10系统原生的openssl版本太老&#xff0c;1.0.1e&#xff0c;不能满足一些新版本应用软件的要求&#xff0c;但是它又被wget、mysql-libs、python-2.6.6、yum等一众系统包所依赖&#xff0c;不能再做升级。故需考虑在不影响系统原生openssl的情况下&#xff0c;安装较新…

HarmonyOS/OpenHarmony(Stage模型)应用开发单一手势(三)

五、旋转手势&#xff08;RotationGesture&#xff09; RotationGesture(value?:{fingers?:number; angle?:number}) 旋转手势用于触发旋转手势事件&#xff0c;触发旋转手势的最少手指数量为2指&#xff0c;最大为5指&#xff0c;最小改变度数为1度&#xff0c;拥有两个可…

mac安装adobe需要注意的tips(含win+mac all安装包)

M2芯片只能安装2022年以后的&#xff08;包含2022年的&#xff09; 1、必须操作的开启“任何来源” “任何来源“设置&#xff0c;这是为了系统安全性&#xff0c;苹果希望所有的软件都从商店或是能验证的官方下载&#xff0c;导致默认不允许从第三方下载应用程序。macOS sie…

力扣(LeetCode)算法_C++——寻找重复的子树

给你一棵二叉树的根节点 root &#xff0c;返回所有 重复的子树 。 对于同一类的重复子树&#xff0c;你只需要返回其中任意 一棵 的根结点即可。 如果两棵树具有 相同的结构 和 相同的结点值 &#xff0c;则认为二者是 重复 的。 示例 1&#xff1a; 输入&#xff1a;root…

智能合约安全分析,Vyper 重入锁漏洞全路径分析

智能合约安全分析&#xff0c;Vyper 重入锁漏洞全路径分析 事件背景 7 月 30 日 21:10 至 7 月 31 日 06:00 链上发生大规模攻击事件&#xff0c;导致多个 Curve 池的资金损失。漏洞的根源都是由于特定版本的 Vyper 中出现的重入锁故障。 攻击分析 通过对链上交易数据初步分…