C++ - 多态的实现原理

news2024/11/26 1:42:50

前言

本博客主要介绍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.coloradmin.cn/o/980276.html

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

相关文章

py脚本解决ArcGIS Server服务内存过大的问题

在一台服务器上&#xff0c;使用ArcGIS Server发布地图服务&#xff0c;但是地图服务较多&#xff0c;在发布之后&#xff0c;服务器的内存持续处在95%上下的高位状态&#xff0c;导致服务器运行状态不稳定&#xff0c;经常需要重新启动。重新启动后重新进入这种内存高位的陷阱…

并行和并发的区别

从操作系统的角度来看&#xff0c;线程是CPU分配的最小单位。 并行就是同一时刻&#xff0c;两个线程都在执行。这就要求有两个CPU去分别执行两个线程。并发就是同一时刻&#xff0c;只有一个执行&#xff0c;但是一个时间段内&#xff0c;两个线程都执行了。并发的实现依赖于…

立晶半导体Cubic Lattice Inc 专攻音频ADC,音频DAC,音频CODEC,音频CLASS D等CL7016

概述&#xff1a; CL7016是一款高保真USB Type-C兼容音频编解码芯片。可以录制和回放有24比特音乐和声音。内置回放通路信号动态压缩&#xff0c; 最大42db录音通路增益&#xff0c;PDM数字麦克风&#xff0c;和立体声无需电容耳机驱动放大器。 5V单电源供电。兼容USB 2.0全速工…

三分法,伟大无比的二分法扩展,本节带部分数论问题。

一&#xff0c;引导简介 简单的来看三分法实际就是二分法的另一种扩展&#xff0c;可以完全的看成二分法&#xff0c;我们介绍几个特殊的点&#xff0c;才能使用这个解法来进行相关的算法求解&#xff1a;求解单调性改变的点&#xff0c;在本个区间中只有一个导数为 0 的点&…

基于qt软件的网上聊天室软件

1.服务器: 1).功能: 用于创建一个客户端&#xff0c;通过文本编辑器来获得端口号&#xff0c;根据获得的端口号创建服务器&#xff0c;等待客户端连接 创建成功会提示服务器创建成功 在收到客户端发送的信息时&#xff0c;把这条信息发送给其他所有客户端&#xff0c;实现群…

力扣(LeetCode)算法_C++——有效的数独

请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08;请参考示例图&#xff09; …

vue3集成jsoneditor

一、背景 之前在做录制回放平台的时候&#xff0c;需要前端展示子调用信息&#xff0c;子调用是一个请求列表数组结构&#xff0c;jsoneditor对数组的默认展示结构是[0].[1].[2]..的方式&#xff0c;为了达到如下的效果&#xff0c;必须用到 onNodeName的钩子函数&#xff0c;…

使用Minifilter过滤驱动保护文件

代码如下: 可以保护拓展名.com文件不被删除、重命名、读写、可执行。#include <ntifs.h> #include <ntstrsafe.h> #include <fltKernel.h> static UNICODE_STRING ProtectedExtention RTL_CONSTANT_STRING(L"com"); //卸载回调 PFLT_FILTER gFile…

如何写一个可以找到工作的简历不至于太烂

简历是自己的一个很重要的标签&#xff0c;是获得面试的敲门砖&#xff0c;简历是要时常更新的&#xff0c;否则会错过一些机会。简历也是给自己的正反馈。 方法 ● 模仿&#xff0c;例如Boss&#xff0c;拉钩下面都给你一个案例模板供你参考&#xff0c;但是我觉得其实参考性…

基础算法--二分查找

二分查找 算法原理 1. 简介 故事分享&#x1f3ec;&#xff1a; 有一天小明到图书馆借了 N 本书&#xff0c;出图书馆的时候&#xff0c;警报响了&#xff0c;于是保安把小明拦下&#xff0c;要检查一下哪本书没有登记出借。小明正准备把每一本书在报警器下过一下&#xff0…

C语言基本知识

基础 第一个函数 argc代表参数个数argument count。argv代表参数value&#xff0c;第一个为放的是文件名&#xff0c;后面是传入的参数。 编译过程 预处&#xff1a;gcc -E hello.c -o hello.i编译&#xff1a;gcc -S hello.c(.i) -o hello.s汇编&#xff1a;gcc -c hello.c…

2023年09月数据库流行度最新排名

点击查看最新数据库流行度最新排名&#xff08;每月更新&#xff09; 2023年09月数据库流行度最新排名 TOP DB顶级数据库索引是通过分析在谷歌上搜索数据库名称的频率来创建的 一个数据库被搜索的次数越多&#xff0c;这个数据库就被认为越受欢迎。这是一个领先指标。原始数…

C语言“牵手”1688商品详情数据方法,1688商品详情API接口,1688API申请指南

1688是中国最大的自营式电商企业&#xff0c;在线销售计算机、手机及其它数码产品、家电、汽车配件、服装与鞋类、奢侈品、家居与家庭用品、化妆品与其它个人护理用品、食品与营养品、书籍与其它媒体产品、母婴用品与玩具、体育与健身器材以及虚拟商品等。 1688平台的商品详情…

route命令小结

Destination: 如果不满足该列的任何一个ip,则走默认的default Gataway: *是 不指定gateway.有的系统是0.0.0.0,与*意义相同 Genmask: 0.0.0.0是不指定掩码, 255.255.0.0掩码了16位,172.17 开头的ip,会走这个网关 255.255.255.0掩码了16位,192.168.0 开头的ip都会走这个网关 当是…

基于Alexnet深度学习网络的人员口罩识别算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 file_path1 test\mask\;% 图像文件夹路径 %获取测试图像文件夹下所有jpg格式的图像文件…

网工内推 | 国企专场,网络运维工程师,华为/思科认证优先

01 中百集团 招聘岗位&#xff1a;运维工程师 职责描述&#xff1a; 1、对集团内使用云计算架构&#xff08;Kubernetes&#xff09;的系统进行规划、运维及管理相关工作。 2、对集团数据中心系统的大数据基础架构&#xff08;Cloudera Distribution Hadoop&#xff09;的规划…

vue3项目修改浏览器的项目icon小图标

修改vue3项目的浏览器的图标 vue2修改图标

微信的标签怎样管理?怎样标签群发更高效?(建议收藏)

01 管理标签的意义 随着时间的递增&#xff0c;微信好友越来越多&#xff0c;很容易就超过了上千人&#xff0c;如果这时候&#xff0c;还没有具备管理标签的意识&#xff0c;那就必须得提上日程了。 管理标签有如下几点意义&#xff1a; 1、方便找到对方 2、分组备注&am…

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

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

分享一下在微信上有哪些微信活动可以做

微信营销活动是吸引更多用户和提高品牌知名度的有效策略。下面是一些微信营销活动的做法&#xff1a; 抽奖活动&#xff1a;通过设置奖品和参与条件&#xff0c;吸引用户参与抽奖活动。例如&#xff0c;可以设置关注公众号、转发活动页面等条件&#xff0c;吸引更多用户参与抽奖…