【C++】多态的实现及其底层原理

在这里插入图片描述
在这里插入图片描述

个人主页:🍝在肯德基吃麻辣烫

我的gitee:gitee仓库
分享一句喜欢的话:热烈的火焰,冰封在最沉默的火山深处。


文章目录

  • 前言
  • 一、什么是多态?
  • 二、多态的构成条件
    • 2.1什么是虚函数?
    • 2.2虚函数的重写
    • 2.3 什么是虚函数表?
      • 有虚函数的对象的大小
    • 2.4普通对象调用和实现多态后的对象调用
  • 三、多态的原理
      • 经典题
      • 回到多态的两个条件
    • 多态条件的两个特例:(这一点是C++的大坑)
    • 3.1动态绑定和静态绑定
  • 四、默认成员函数和虚函数的关系
    • 4.1构造函数可以设置成虚函数吗?
    • 4.2 析构函数可以设置成虚函数吗?
    • 一道经典面试题
  • 五、单继承和多继承关系中的虚函数表
    • 5.1单继承关系中的虚函数表
    • 5.2多继承关系中的虚函数表
  • 六、抽象类
  • 写在最后的面试题
  • 总结


前言

本文继C++继承之后讲解C++多态。


一、什么是多态?

单单从概念入手不好理解,应该深入理解多态的实现后再回过头来讲解。
现在简单举个例子:我们在购买高铁票时,往往会有成人票全价,学生票半价的优惠,针对不同的人群给予不同的优惠,这个就是多态(多种形态)。

二、多态的构成条件

多态的两个构成条件为:

1.基类的指针或引用
2.满足虚函数的重写

2.1什么是虚函数?

virtual关键字修饰的类成员函数就是虚函数。

在继承中,子类要想重写父类,父类的成员函数必须是虚函数,而子类的成员函数可以不加virtual,但一般建议加上比较合适。

2.2虚函数的重写

虚函数的重写是:对父类的虚函数的实现进行覆盖,覆盖的内容是子类虚函数的实现。

满足重写的条件:三同。

函数名,参数类型,返回值相同的虚函数,就能满足重写的条件。

2.3 什么是虚函数表?

虚函数表是继承体系中,如果一个函数是虚函数,则该函数的地址会存储在一张虚函数表中,而不是存储在对象中,该虚函数表的地址才存储在对象中。通过虚函数表可以找到对应的虚函数,从而能够进一步实现多态。
在这里插入图片描述

有虚函数的对象的大小

class A
{
public:
	virtual void func1()
	{}

	virtual void func2()
	{}

protected:
	int _a;
};

int main()
{
	cout << sizeof(A) << endl;
	return 0;
}

请计算上面的代码中,A这个类的大小。
A有两个虚函数,一个成员,实际上大小为8

原因:
虚函数在内存中是存储在虚函数表中,而不是存在类对象中,类对象在内存中存储的是一个虚函数表指针和成员变量。
指针大小是4字节,成员是int类型,则共为8字节。
在这里插入图片描述

虚函数表本质上是一个函数指针数组。
一般建议:如果不实现多态,就不要设置成虚函数

2.4普通对象调用和实现多态后的对象调用

普通对象调用成员函数是在编译期间就确定了地址,而实现多态后的函数是在运行期间才确定地址。因为子类继承父类,子类先拷贝父类的虚函数表的地址,如果某个函数是虚函数,则会在子类的虚函数表中重写改函数的地址。

所以编译器在遇到父类的指针或引用调用子类的函数时,编译器在编译期间无法确定到底调用谁的函数,只能运行起来去子类的虚函数表中查找,是谁的地址就调用谁。

三、多态的原理

我们通过以下例子来看待多态。

class Person 
{
public:
	virtual void BuyTicket() 
	{ 
		cout << "买票-全价" << endl; 
	}
};

class Student : public Person 
{
public:
	virtual void BuyTicket() 
	{ 
		cout << "买票-半价" << endl; 
	}
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);
	return 0;
}

首先:子类继承了父类,父类在内存中有一张虚表,存的是虚函数的地址。当子类继承了父类后,子类中也同样有一张虚表,只不过是这张虚表上的虚函数的地址是拷贝父类的,如果子类中有虚函数,且满足重写的条件,则子类的虚函数表的虚函数的地址会被重写覆盖成子类的虚函数的地址。

在这里插入图片描述

当我们用父类的指针或引用调用子类的虚函数时,会去访问子类的虚函数表,当我们调用父类的虚函数时,会访问父类的虚函数表,这样就实现了父类指针指向父类对象就调用父类的虚函数表,指向子类对象就调用子类的虚函数表。在这里插入图片描述

所以多态就是:我们想调用父类的函数就传递父类对象给父类指针/引用,想调用子类的函数就传递子类对象给父类指针/引用。

经典题

这里有一道非常经典的坑人题目:

class A
{
public:
   	virtual void func(int val = 1)
   	{ 
   		std::cout<<"A->"<< val <<std::endl;
   	}
   	
   	virtual void test()
   	{ 
   		func();
   	}
 };
 
class B : public A
{
public:
	void func(int val=0)
	{ 
	    std::cout<<"B->"<< val <<std::endl;
	}
 };
 
 int main(int argc ,char* argv[])
 {
   B*p = new B;
   p->test();
   return 0;
 }

问:结果输出什么?
对于一个子类指针p指向子类对象,通过这个指针调用函数test,因为子类继承了父类,父类就成了子类的成员,通过父类的虚表找到test函数的地址调用,在test函数中又调用func函数,因为这是B的指针,调用func函数时会去B对象的虚表中找到func的地址,这个地址不再是从父类那里拷贝的func的地址,这个地址已经被重写成子类的func的地址,所以调用的是子类的func函数。
然而,虚函数的重写只是重写函数的实现,函数的接口仍然继承父类的,这个叫做接口继承,所以参数的缺省值仍然是父类的。
结果输出B->1

回到多态的两个条件

多态的两个条件:

1.父类的指针或引用
2.虚函数的重写

(1)为什么不能是子类的对象赋值给父类的对象,而是子类对象赋值给父类的指针/引用?
因为子类对象赋值给父类对象,切片过程中不会拷贝虚表。所以父类对象只能调用父类的虚表,子类对象才能调用子类的虚表,满足不了多态。

(2)为什么子类的对象赋值给父类的对象不会拷贝虚表?
因为如果拷贝虚表,使用子类对象会调用子类的虚函数,使用父类对象也会拷贝子类的虚函数,就乱套了。

(3)为什么不能是子类的指针或引用?因为父类是子类的一部分,父类赋值给子类指针不会切片,就不能获取父类的虚函数表。

需要注意的以下几点:

(1)虚函数存在哪?虚函数表存在哪?
注意:虚函数和普通函数一样,都是存在代码段的,虚函数表也是存在代码段的。
是虚函数地址存在虚表中,虚函数表的地址存在对象中。
我们可以通过打印地址的方式验证一下虚函数表存在代码段:

void test_where()
{
	Person p;
	Student s;
	//栈
	int a;
	printf("栈->[%p]\n", &a);

	//堆区
	int* ptr = new int;
	printf("堆->[%p]\n", ptr);

	//数据段(静态区)
	static int b;
	printf("静态->[%p]\n", &b);

	//代码段(常量区)
	const char* str = "Hello World";
	printf("常量区->[%p]\n", str);

	printf("虚函数表1->[%p]\n", *((int*)&p));
	printf("虚函数表2->[%p]\n", *((int*)&s));

}

int main()
{
	test_where();
	return 0;
}

思路:获取父类或子类对象的虚函数表指针,也就是取出父类/子类在内存中的前4个字节,将该地址与内存中的栈区,堆区,静态区(数据段),常量区(代码段)的地址进行对比,跟谁的地址比较近,就大致在哪个区域。

(2)子类继承父类后,如果子类也有自己的虚函数,则这些虚函数的地址是按照他们声明的顺序依次存在虚表的最后,因为前面先存父类的虚函数的地址。

多态条件的两个特例:(这一点是C++的大坑)

(1)协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
(这一点是C++的大坑)

(2)析构函数的重写(父类和子类的析构函数名不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

3.1动态绑定和静态绑定

多态可以分为动态多态和静态多态,动态多态也叫做动态绑定,静态多态也叫做静态绑定。

静态绑定有:函数重载
函数重载在编译时进行匹配,cout的自动识别类型的底层也是使用函数重载实现的。
函数重载原理:函数名修饰规则
动态绑定:多态。
编译阶段编译器不知道到底调用子类的虚函数还是父类的虚函数,所以编译器只能通过给定的对象,在运行期间,通过调用虚函数表的地址来进行查看。这个过程叫做动态绑定。
多态原理:虚函数表

四、默认成员函数和虚函数的关系

4.1构造函数可以设置成虚函数吗?

构造函数不能是虚函数,因为虚函数表是在构造函数的初始化列表初始化之后才会生成的。如果构造函数是虚函数,那么调用构造函数的时候要去虚函数表里面找到构造函数的地址来进行调用。那么如何找到虚表呢?这就是一个典型的现有鸡还是先有蛋的问题了。

4.2 析构函数可以设置成虚函数吗?

析构函数可以设置成虚函数,并且强烈建议设置成虚函数。

(1)析构函数加virtual,是不是虚函数?
是,因为在编译器编译阶段,会通过函数名修饰规则,统一将子类的析构函数和父类的析构函数改成:destructor。
(2)为什么要这么处理?

因为在子类析构函数调用完成后必须要调用父类的析构函数对父类的资源释放。设置成虚函数可以完成子类对父类的析构函数的重写。
(3)为什么要让他们构成重写?
因为在下面的场景中,构成重写可以实现父类指针调用子类对象会调用子类的析构,子类析构调用结束会自动调用父类的析构,父类指针调用父类对象会调用父类的析构。从而实现不同对象传递给父类指针/引用,会调用不同对象的函数,实现多态。

如果不设置成虚函数,假如有以下的场景,会出问题:

class Person
{
public:
	~Person()
	{
		cout << "father:~Person" << endl;
	}
};

class Student : public Person
{
public:
	~Student()
	{
		cout << "son:~Student" << endl;
		delete [] ptr;
	}

protected:
	int* ptr = new int[10];
	//new ==> 构造 + operator new()
};

int main()
{
	Person* p = new Person;
	delete p;

	p = new Student;
	delete p;
	return 0;
}

delete p 会处理成以下方式:
p->destructor() + operator delete( p )

普通对象,看当前者的类型。
多态对象,看指向对象的类型。

p是父类对象,不管指向的对象是父类还是子类,都会调用父类的析构函数,在子类申请的空间就得不到释放,会造成内存泄露问题。
如果设置成虚函数,就能够实现虚函数的重写,从而实现多态。

一道经典面试题

设计一个不想被继承的类,如何创建?
(1)构造函数设置成私有

class A
{
private:
	A()
	{}

};

class B : public A
{
public:
	B()
	{}

};

原理:在继承体系中,子类的构造函数必须先去调用父类的构造函数,这里父类构造设置成私有子类就无法调用了。

不过这里出现一个问题,子类无法调用父类的构造,父类也无法调用自己的构造了。
我们可以写一个函数,在函数里面创建一个父类。

class A
{
private:
	A Createobj()
	{
		return A();
	}

	A()
	{}

};

但是又有一个问题:如何调用这个函数呢?因为调用该函数创建对象,而创建对象又需要在函数里面创建。
这里我们可以加一个static解决

class A
{
private:
	static A Createobj()
	{
		return A();
	}

	A()
	{}

};

int main()
{
    A a = A::Createobj();
}

这样就可以通过指定类域访问该函数解决。

五、单继承和多继承关系中的虚函数表

5.1单继承关系中的虚函数表

单继承关系中,子类会拷贝父类的虚函数表,如果子类还有自己的虚函数,则该虚函数的地址会放在虚函数表的最后。

我们通过调试窗口无法看到子类的虚函数。在这里插入图片描述
不过我们可以通过获取虚表的地址来打印虚表的各个虚函数的地址。

typedef void(*VFPTR)();

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a = 1;
};

class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b = 2;
};

void PrintVfptr(VFPTR* arr)
{
	printf("虚表地址是:[%p]\n", arr);
	for (int i = 0; arr[i] != nullptr; i++)
	{
		printf("第%d个虚函数地址是:[%p]\n", i, arr[i]);
	}
	printf("\n");

}

int main()
{
	Base b;

	Derive d;
	int vfptrb = *((int*)&b);
	int vfptrd = *((int*)&d);
	PrintVfptr((VFPTR*)vfptrb);
	PrintVfptr((VFPTR*)vfptrd);

	return 0;
}


  • 思路:1.先获取虚表的地址,取子类对象的地址,强转成int*,再进行解引用,就取到了子类对象的前4个字节,也就是虚表指针。
  • 2.再强转成(VFPTR*)通过打印该指针指向的内容,即可打印虚表的内容。

5.2多继承关系中的虚函数表

这里有几种猜测,子类的未重写的虚函数会放在第一个继承的父类的虚表中,或者放在其他的父类的虚表中。

我们也可以通过打印地址的方式确定。

typedef void(*VFPTR)();

class Base1 
{
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }

private:
	int b1 = 1;
};

class Base2 
{
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Derive::func2" << endl; }
private:
	int b2 = 2;
};

class Derive1 :public Base1, public Base2
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d = 2;
};

void PrintVfptr(VFPTR* arr)
{
	printf("虚表地址是:[%p]\n", arr);
	for (int i = 0; arr[i] != nullptr; i++)
	{
		printf("第%d个虚函数地址是:[%p]\n", i, arr[i]);
	}
	printf("\n");

}

int main()
{
	Derive1 d;

	int vfptrb1 = *((int*)&d);
	
	Base2* p2 = &d;
	//自动切片,p2就指向Base2对象的首地址
	int vfptrb2 = *(int*)p2;
	//int vfptrb2 = *((int*)((char*)&d + sizeof(Base1)));

	PrintVfptr((VFPTR*)vfptrb1);
	PrintVfptr((VFPTR*)vfptrb2);

	return 0;
}

在这里插入图片描述

通过打印可以看到,子类中未重写的虚函数放在第一个继承的父类的虚表中。

但是这里有一个问题:为什么重写了func1,在Base1的func1的地址和Base2的func1的地址不一样?
在这里插入图片描述

在ptr1和ptr2调用func1的过程中,调用的是Derive的func1函数,因为func1已经被重写了。

而在内存中,ptr1和ptr2指向的地址如下:

在这里插入图片描述

因为ptr1指向的地址刚好是d对象的首地址,ptr1和this指针是重叠的,无需偏移,而ptr2需要偏移Base1字节才与this指针重叠。
这就导致在调用func1函数前,ptr2需要先偏移,才能调用。我们看到的是在偏移之前的ptr2的地址,这就是为什么看到的func1的地址不同,调用的确实同一个函数的原因,实际上ptr2会偏移。

六、抽象类

在虚函数的后面写上 =0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class
{
public:
	virtual void func1() = 0
	{}	
};

比如上面这个就是抽象类。
抽象类的特性:

  • 1.不能实例化出对象,子类继承后也不能实例化出对象,只有重写虚函数才能实例化出对象。
  • 2.可以定义指针或引用.

普通的对象是实现继承,而实现多态的对象是接口继承。

抽象类的作用:间接强制子类虚函数必须重写,否则无法实例化对象。

写在最后的面试题

  1. 什么是多态?
  2. 什么是重载、重写(覆盖)、重定义(隐藏)?
  3. 多态的实现原理?
  4. inline函数可以是虚函数吗?
  5. 静态成员可以是虚函数吗?
  6. 构造函数可以是虚函数吗?
  7. 析构函数可以是虚函数吗?
  8. 对象访问普通函数快还是虚函数更快?
  9. 虚函数表是在什么阶段生成的,存在哪的?
  10. C++菱形继承的问题?虚继承的原理?
  11. 什么是抽象类?抽象类的作用?

1.多态分为静态多态和动态多态。
静态多态是函数重载,本质上是函数名修饰规则来完成。
动态多态:继承中的虚函数重写 + 父类指针或引用两个条件完成。
动态多态的本质上是由虚函数表的实现来完成。
2.重载:两个函数必须在同一作用域,函数名和参数类型必须相同。
重写(覆盖):两个函数在父类和子类的作用域,且要求满足函数名,参数类型,返回值必须相同(协变例外),两个函数必须是虚函数。
重定义(隐藏)两个函数在父类和子类的作用域,且要求满足函数名相同,两个父类和子类的重名函数不构成重写就一定是重定义。
3.多态实现的原理:
静态多态:函数名修饰规则
动态多态:虚函数表
4.不可以,因为一个函数如果设置成内联,就是一段代码,没有产生地址,无法将地址放进虚表,就不能是虚函数了。(声明:如果一个内联函数被virtual修饰,那么该函数就会自动忽略inline的属性,而成为一个虚函数。
5. 静态成员函数不能是虚函数,因为静态成员函数没有this指针,使用类型::成员函数的方式来调用该函数,但这种方式无法访问虚函数表,所以静态成员函数无法放进虚表里,无法实现多态,就没有意义。
6.构造函数不能是虚函数,构造函数不能是虚函数,因为虚函数表是在构造函数的初始化列表中生成的,如果构造函数是虚函数,又得取到虚函数表里面找到构造函数的地址,所以是现有构造函数还是先有虚表呢?
7.析构函数可以是虚函数,并且强烈建议析构函数设置成虚函数。因为父类和子类的析构函数会被编译器统一编译成destructor,有些特殊场景必须实现多态才能解决,请参考本文内容。
8.对象访问普通函数和虚函数是一样快的,因为它们都是在编译期间就确定。但如果是指针对象或引用对象调用,是普通函数快,因为调用虚函数会去到虚表中查找地址,速度会慢一些。
9.虚函数表在编译阶段生成的,存在代码段,具体参考本文章的案例。
10.菱形继承的问题:数据冗余和二义性。菱形虚拟继承原理:生成一张虚基表,存放的是该对象相对于父类对象的偏移量,通过偏移量可以访问父类的成员,不再需要在每个子类中存一份父类的成员。
11.抽象类:具有纯虚函数的类叫做抽象类。而在虚函数后面加上 = 0,就是纯虚函数。抽象类的意义:强制子类进行虚函数的重写,并且抽象类体现了接口继承关系。

总结

多态内容就讲到这里。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ICASSP 2023 | Cough Detection Using Millimeter-Wave FMCW Radar

原文链接&#xff1a;https://mp.weixin.qq.com/s?__bizMzg4MjgxMjgyMg&mid2247486540&idx1&sn6ebd9f58e9f08a369904f9c48e12d136&chksmcf51beb5f82637a3c65cf6fa53e8aa136021e35f63a58fdd7154fc486a285ecde8b8521fa499#rd ICASSP 2023 | Cough Detection Usi…

Qt中postevent造成内存泄漏问题的通用解决方案

在Qt中由QCoreApplication统一管理Qt事件的收发和销毁,其中sendEvent为阻塞式发送,用于单线程的事件发送;postevent为非阻塞式发送,构造事件的线程和接受事件的线程可以为两个线程。 最近在做一个个人项目ShaderLab 需要绘制OpenGL实时渲染的图像,由于OpenGL渲染基本都放…

论文笔记——Influence Maximization in Undirected Networks

Influence Maximization in Undirected Networks ContributionMotivationPreliminariesNotations Main resultsReduction to Balanced Optimal InstancesProving Theorem 3.1 for Balanced Optimal Instances Contribution 好久没发paper笔记了&#xff0c;这篇比较偏理论&…

python+django+mysql项目实践一(环境准备)

python项目实践 环境说明: Pycharm 开发环境 Django 前端 MySQL 数据库 Navicat 数据库管理 创建Pycharm项目 安装Django 在pycharm文件—设置进行安装 新建Django项目 注意项目创建目录 项目默认目录文件说明: __init__.py asgi.py 【异步接受网络…

深度学习实践——模型推理优化练习

系列实验 深度学习实践——卷积神经网络实践&#xff1a;裂缝识别 深度学习实践——循环神经网络实践 深度学习实践——模型部署优化实践 深度学习实践——模型推理优化练习 深度学习实践——模型推理优化练习 模型推理优化练习架构设计练习知识蒸馏练习模型剪枝练习参数量化练…

如何高效实现文件传输:小文件采用零拷贝、大文件采用异步io+直接io

一般会如何实现文件传输&#xff1f; 服务器提供文件传输功能&#xff0c;需要将磁盘上的文件读取出来&#xff0c;通过网络协议发送到客户端。如果需要你自己编码实现这个文件传输功能&#xff0c;你会怎么实现呢&#xff1f; 通常&#xff0c;你会选择最直接的方法&#xf…

QT基于TCP协议实现数据传输以及波形绘制——安卓APP及Windows程序双版本

文章代码有非常非常之详细的解析&#xff01;&#xff01;&#xff01;诸位可放心食用 这个玩意我做了两个&#xff0c;一个是安卓app&#xff0c;一个是Windows程序。代码并非全部都是由我从无到有实现&#xff0c;只是实现了我想要的功能。多亏了巨人的肩膀&#xff0c;开源…

小研究 - 一种复杂微服务系统异常行为分析与定位算法(一)

针对极端学生化偏差&#xff08;&#xff25;&#xff58;&#xff54;&#xff52;&#xff45;&#xff4d;&#xff45; &#xff33;&#xff54;&#xff55;&#xff44;&#xff45;&#xff4e;&#xff54;&#xff49;&#xff5a;&#xff45;&#xff44; &#…

Java中的生产者/消费者模型

一、什么是生产者/消费者模型 生产者-消费者模型&#xff08;Producer-Consumer problem&#xff09;是一个非常经典的多线程并发协作的模型。 比如某个模块负责生产数据&#xff0c;而另一个模块负责处理数据。产生数据的模块就形象地被称为生产者&#xff1b;而处理数据的模…

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

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

39.手机导航

手机导航 html部分 <div class"phone"><div class"content"><img class"active" src"./static/20180529205331_yhGyf.jpeg" alt"" srcset""><img src"./static/20190214214253_hsjqw…

DHCP部署与安全详解

文章目录 一、DHCP是什么&#xff1f;二、DHCP相关概念三、DHCP优点四、DHCP原理1. 客户机发送DHCP Discovery广播包&#xff08;发现谁是DHCP服务器&#xff09;2. 服务器响应DHCP Offer广播包3. 客户机发送DHCP Request广播包4. 服务器发送DHCP ACK广播包 五、DHCP续约六、部…

ElasticSearch基本使用--ElasticSearch文章一

文章目录 官网学习必要性elasticsearch/kibana安装版本数据结构说明7.x版本说明ElasticSearch kibana工具测试后续我们会一起分析 官网 https://www.elastic.co/cn/ 学习必要性 1、在当前软件行业中&#xff0c;搜索是一个软件系统或平台的基本功能&#xff0c; 学习Elastic…

使用 OpenCV 进行图像模糊度检测(拉普拉斯方差方法)

写在前面 工作中遇到&#xff0c;简单整理人脸识别中&#xff0c;对于模糊程度较高的图像数据&#xff0c;识别率低&#xff0c;错误率高。虽然使用 AdaFace 模型&#xff0c;对低质量人脸表现尤为突出。但是还是需要对 模糊程度高的图像进行丢弃处理当前通过阈值分类&#xff…

Java开发中的分层开发和整洁架构

分层开发(横向拆分) 分层开发的概念: maven多模块开发项目管理.可以利用这种管理功能,实现一个项目的多层次模块开发–分层开发. 比如,当前项目HelloController依赖HelloService 这样做目的: 复杂开发过程.解耦(不调整依赖关系,无法解耦).分层开发(横向拆分)和纵向拆分的区别…

c# 此程序集中已使用了资源标识符

严重性 代码 说明 项目 文件 行 禁止显示状态 错误 CS1508 此程序集中已使用了资源标识符“BMap.NET.WindowsForm.BMapControl.resources” BMap.NET.WindowsForm D:\MySource\Decompile\BMap.NET.WindowsForm\CSC 1 活动 运行程序时&a…

【机器学习】Feature Engineering and Polynomial Regression

Feature Engineering and Polynomial Regression 1. 多项式特征2. 选择特征3. 缩放特征4. 复杂函数附录 首先&#xff0c;导入所需的库&#xff1a; import numpy as np import matplotlib.pyplot as plt from lab_utils_multi import zscore_normalize_features, run_gradien…

session反序列化+SoapClientSSRF+CRLF

文章目录 session反序列化SoapClientSSRFCRLF前言bestphps revengecall_user_func()方法的特性SSRFCRLF组合拳session反序列化 解题步骤总结 session反序列化SoapClientSSRFCRLF 前言 从一道题分析通过session反序列化出发SoapClientSSRF利用CRLF解题 bestphp’s revenge 首…

超详细的74HC595应用指南(以stm32控制点阵屏为例子)

74HC595是一款常用的串行输入/并行输出&#xff08;Serial-in/Parallel-out&#xff09;移位寄存器芯片&#xff0c;在数字电子领域有着广泛的应用。它具有简单的接口和高效的扩展能力&#xff0c;成为了许多电子爱好者和工程师们的首选之一。本文将深入介绍74HC595芯片的功能、…

UE5、CesiumForUnreal加载无高度地形

文章目录 1.实现目标2.实现过程3.参考资料1.实现目标 在UE5中,CesiumForUnreal插件默认的地形都是带高度的,这里加载没有高度的地形,即大地高程为0,GIF动图如下: 2.实现过程 参考官方的教程,下载无高度的DEM,再切片加载到UE中。 (1)下载无高度地形DEM0。 在官方帖子…