探索多态的本质【C++】

news2024/12/25 8:52:58

文章目录

  • 多态的构成条件
    • 虚函数
    • 虚函数的重写(覆盖)
  • 虚函数重写的两个例外
  • C++11 override和final
  • 区分重载、覆盖(重写)、隐藏(重定义)
  • 抽象类
  • 接口继承和实现继承
  • 多态的原理
    • 虚函数表
  • 动态绑定和静态绑定
    • 动态绑定
    • 静态绑定
  • 单继承中的虚函数表
  • 多继承中的虚函数表
  • 菱形继承、菱形虚拟继承

多态的构成条件

在继承中要构成多态还有两个条件:
1、必须通过基类的指针或者引用调用虚函数。

2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

//父类
class Person
{
public:
	//父类的虚函数
	virtual void BuyTicket() const
	{
		cout << "买票-全价" << endl;
	}
};

//子类
class Student : public Person
{
public:
	//派生类的虚函数重写了父类的虚函数
	virtual void BuyTicket() const
	{
		cout << "买票-半价" << endl;
	}
};

引用
//void func(const Person &  p)
//{
//	p.BuyTicket();
//}

//指针
void func(const Person *  p)
{
	p->BuyTicket();
}



int main()
{    
	//多态条件
	// 1、调用函数必须是重写的虚函数
	//基类必须是指针或者引用


	//多态,不同对象传递过去,调用不同参数
	//多态调用看指向的对象
	//普通对象,看当前类型

	//引用
	/*func(Person());
	func(Student());*/


	//指针
	Person pp;
	func(&pp);
	Student st;
	func(&st);

	return 0;
}

虚函数

被virtual修饰的类成员函数被称为虚函数。

class A
{
public:
	virtual void func()
	{
		cout << "virtual void func() " << endl;
	}
};

注意:
只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual

虚函数这里的virtual和虚继承中的virtual虽然是同一个关键字,但是它们之间没有任何关系。

虚函数的virtual是为了实现多态
虚继承的virtual是为了解决菱形继承的数据冗余和二义性。

虚函数的重写(覆盖)

如果派生类中有一个和基类完全相同的虚函数(返回值类型相同、函数名相同以及参数列表完全相同,这里所说的参数列表是指参数类型要相同)
此时我们称该派生类的虚函数重写了基类的虚函数。

//父类
class Person
{
public:
	//父类的虚函数
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
//子类
class Student : public Person
{
public:
	//派生类的虚函数重写了父类的虚函数
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
//子类
class Soldier : public Person
{
public:
	//派生类的虚函数重写了父类的虚函数
	virtual void BuyTicket()
	{
		cout << "优先-买票" << endl;
	}
};

通过父类Person的指针或者引用调用虚函数BuyTicket,此时不同类型的对象,调用的就是不同的函数,产生的也是不同的结果,进而实现了函数调用的多种形态。

void Func(Person& p)
{
	//通过父类的引用调用虚函数
	p.BuyTicket();
}
void Func(Person* p)
{
	//通过父类的指针调用虚函数
	p->BuyTicket();
}
int main()
{
	Person p;   //普通人
	Student st; //学生
	Soldier sd; //军人

	Func(p);  //买票-全价
	Func(st); //买票-半价
	Func(sd); //优先买票

	Func(&p);  //买票-全价
	Func(&st); //买票-半价
	Func(&sd); //优先买票
	return 0;
}

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用)

虚函数重写的两个例外

协变(基类与派生类虚函数的返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。
即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。

//基类
class A
{

};
//派生类
class B : public A
{

};

//基类
class Person
{
public:
	//虚函数
	virtual  A* fun()
	{
		cout << "A* Person::f()" << endl;
		return new A;
	}
};
//派生类
class Student : public Person
{
public:
	// 虚函数
		virtual  B * fun()
	{
		cout << "B* Person::f()" << endl;
		return new B;
	}

};
int main()
{
	Person p;
	Student st;
	//基类指针指向基类对象
	Person* ptr1 = &p;
	//基类指针指向子类对象
	Person* ptr2 = &st; //切片
	
	//父类指针ptr1指向的p是父类对象,调用父类的虚函数
	ptr1->fun();

	//父类指针ptr2指向的st是子类对象,调用子类的虚函数
	ptr2->fun();
	return 0; 
}

析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。

//析构函数加上virtual ,是虚函数重写,为什么?
// 因为析构函数都被处理成了destructor这个统一的名字,为什么统一处理成destructor?
//因为统一处理成destructor,是要将派生类和基类的析构函数构成重写,而重写是构成多态的一个重要条件
class Person
{
public:
	virtual  ~Person()
	{
		cout << "~Person()" << endl;
	}
};
//子类
class Student : public Person
{
public:
	virtual  ~Student()
	{
		cout << "~Student()" << endl;
		delete[]ptr;
	}
protected :	
	int* ptr = new int[10];
};

int main()
{
	//Person p;
	//Student s;//析构顺序:先子后父

	Person* p = new Person;
	delete p;

	p = new Student;
	delete p;//p->destuctor() +  operator delete (p)
	   //这里我们希望p->destuctor()是一个多态调用 ,而不是普通调用

	return 0;
}

在继承当中,子类和的析构函数和父类的析构函数构成隐藏的原因就在这里,这里表面上看子类的析构函数和父类的析构函数的函数名不同,但是为了构成重写,编译后析构函数的名字会被统一处理成destructor();

C++11 override和final

C++11提供了override和final两个关键字,可以帮
助用户检测是否重写。

final:修饰虚函数,表示该虚函数不能再被重写。

//基类
class Person
{
public:
	//虚函数
	//final:修饰虚函数,表示该虚函数不能再被重写
	 virtual void BuyTicket() final
	{
		cout << "买票-全价" << endl;
	}
};

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

override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错

//基类
class Person
{
public:
	//虚函数
	virtual void BuyTicket() 
	{
		cout << "买票-全价" << endl;
	}
};

//派生类
class Student : public Person
{
public:
	//override,派生类完成基类的重写,就不报错
	virtual  void BuyTicket() override
	{
		cout << "买票-半价" << endl;
	}
};

区分重载、覆盖(重写)、隐藏(重定义)

在这里插入图片描述

抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};
int main()
{
	Car c; //err
	return 0; 
}

派生类继承抽象类后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

//抽象类

class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};

//派生类继承抽象类

class Benz : public Car
{
public:
	//重写纯虚函数
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};

int main()
{
	//派生类重写了纯虚函数,可以实例化对象
	Benz b1;
	Car* p1 = &b1;
	p1->Drive();
	return 0;
}

抽象类不能实例化出对象,那抽象类存在的意义是什么?

抽象类体现了虚函数的继承是一种接口继承,强制子类去重写纯虚函数,因为子类若是不重写从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。

接口继承和实现继承

实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数的实现,可以使用该函数。

接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。

建议: 所以如果不实现多态,就不要把函数定义成虚函数。

多态的原理

虚函数表

看下面的代码

class Base
{
public:

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

	int _b = 1;
};

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

b对象当中除了_b成员外,实际上还有一个_vfptr(虚函数表指针简称虚表指针)放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关)。虚函数
的地址要被放到虚函数表中,虚函数表也简称虚表
在这里插入图片描述

#include <iostream>
using namespace std;
//父类
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:
	//重写虚函数Func1
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

通过观察,我们发现:
基类对象b和基类对象d当中除了自己的成员变量之外,基类和派生类对象都有一个虚表指针,分别指向属于自己的虚表。
在这里插入图片描述
实际上虚表当中存储的就是虚函数的地址

派生类虽然继承了基类的虚函数Func1和Func2,但是派生类对基类的虚函数Func1进行了重写。所以,派生类对象d的虚表当中存储的是基类的虚函数Func2的地址和重写的Func1的地址。这就是为什么虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数地址的覆盖,重写是语法的叫法,覆盖是原理层的叫法。

注意:Func2是虚函数,所以继承下来后放进了子类的虚表,而Func3是普通成员函数,继承下来后不会放进子类的虚表。并且,虚函数表本质是一个存虚函数指针的指针数组,一般情况下会在这个数组最后放一个nullptr。

总结:派生类的虚表生成步骤如下
1、先将基类中的虚表内容拷贝一份到派生类的虚表

2、如果派生类重写了基类中的某个虚函数,则用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址

3、派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

虚表是什么阶段初始化的?虚函数存在哪里?
虚表实际上是在构造函数初始化列表阶段进行初始化的,注意虚表当中存的是虚函数的地址不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中。另外,对象中存的不是虚表而是指向虚表的指针。

那虚表是存在哪里的?

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
	virtual void Func1() { }
	virtual void Func2() { }
//protected:
	int _a = 0;
};
class Student : public Person
{
public:
	//派生类重写基类虚函数
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
	virtual void Func3() { }
protected:
	int _b = 1;
};
int main()
{
	Person ps;
	Student st; 
	//栈
	int a = 0;
	printf("栈:%p\n", &a);

	//静态区
	static  int b = 0;
	printf("静态区:%p\n", &b);

	//堆
	int* p = new int;
	printf("堆:%p\n", p);

	const char* str = "hello world";
	printf("常量区(代码段):%p\n", str);

	printf("虚表1:%p\n",*(   (int*)&ps ) );
	printf("虚表2:%p\n", *( (int*) &st ) );

	return  0; 
}

从上述代码可以发现虚表地址与代码段的地址非常接近,由此可以得出虚表是存在代码段的

详细分析下面的代码
为什么当父类Person指针指向的是父类对象Mike时,调用的就是父类的BuyTicket?
当父类Person指针指向的是子类对象Johnson时,调用的就是子类的BuyTicket?

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

	int _a = 1;
};

class Student : public Person 
{
public:
	//派生类重写基类虚函数
	virtual void BuyTicket() 
	{ 
		cout << "买票-半价" << endl;
	}
	int _b = 1;
};

int main()
{
	Person Mike;
	Student Johnson;
	
	Johnson._b = 3; //以便观察是否完成切片
	Person* p1 = &Mike;
	Person* p2 = &Johnson;
	p1->BuyTicket(); //买票-全价
	p2->BuyTicket(); //买票-半价
	return 0;
}

对象Mike中包含一个成员变量_a和一个虚表指针,对象Johnson中包含两个成员变量_a和_b以及一个虚表指针,这两个对象当中的虚表指针分别指向自己的虚表。

在这里插入图片描述

通过上图可分析:
1、父类指针p1指向Mike对象,p1->BuyTicket在Mike的虚表中找到的虚函数就是Person::BuyTicket。

2、父类指针p2指向Johnson对象,p2>BuyTicket在Johnson的虚表中找到的虚函数就是Student::BuyTicket。

这样就实现出了不同对象去完成同一行为时,展现出不同的形态,即多态

多态构成的两个条件,
1、完成虚函数的重写,
2、必须使用父类的指针或者引用去调用虚函数。
完成虚函数的重写是因为需要完成子类虚表当中虚函数地址的覆盖,这样才能做到指针指向父类,调用父类对象,指针指向子类,调用子类对象

为什么多态的设计必须使用父类的指针或者引用,不使用父类的对象?
指针和引用的切片不存在拷贝问题 ,但是对象的切片需要拷贝
子类赋值给父类对象切片,不会拷贝虚表,如果拷贝虚表,那么父类对象虚表中是父类虚函数还是子类虚函数就不确定了

在这里插入图片描述

使用父类的指针或者引用时,实际上是一种切片行为,切片时只会让父类指针或者引用得到父类对象或子类对象中切出来的那一部分。

在这里插入图片描述
用p1和p2调用虚函数时,p1和p2通过虚表指针找到的虚表是不一样的,最终调用的函数也是不一样的。

Person p1 = Mike;
Person p2 = Johnson;

使用父类对象时,切片得到部分成员变量后,会调用父类的拷贝构造函数对那部分成员变量进行拷贝构造,而拷贝构造出来的父类对象p1和p2当中的虚表指针指向的都是父类对象的虚表。因为同类型的对象共享一张虚表,他们的虚表指针指向的虚表是一样的。

总结:

对象的调用:
如果是普通对象的调用(不符合多态),看调用者的类型,普通对象的调用在编译时就确定好了地址
如果调用符合多态,看指向的对象,在运行时到指向对象的虚函数表中找调用函数的地址从而完成调用

动态绑定和静态绑定

动态绑定

动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载

普通对象的调用

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

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

int main()
{
	Student st;
	Person p = st;
	//不构成多态,函数的调用是在编译时就确定的

	p.BuyTicket();
	return 0;
}

在这里插入图片描述
将调用函数的那句代码翻译成汇编就只有以上两条汇编指令,也就是直接调用的函数。

使用多态调用

//基类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
protected:
	int _a = 0;
};

//派生类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
protected:
	int _b = 1;
};

int main()
{
	Student st;
	Person & p = st;
	//构成多态,看指向的对象

	p.BuyTicket();
	return 0;
}

在这里插入图片描述
构成多态时调用函数的那句代码翻译成汇编后就变成了八条汇编指令,原因就是我们需要在运行时,先到指定对象的虚表中找到要调用的虚函数,然后才能进行函数的调用。

体现了静态绑定是在编译时确定的,而动态绑定是在运行时确定的。

单继承中的虚函数表

//基类
class Base
{
public:
	virtual void func1() 
	{ 
		cout << "Base::func1()" << endl;
	}
	virtual void func2() 
	{
		cout << "Base::func2()" << endl; 
	}
private:
	int _a = 0 ;
};
//派生类
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 =1 ;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

派生类和基类的内存分布

在这里插入图片描述
单继承关系当中,派生类的虚表生成过程如下:

1、继承基类的虚表内容到派生类的虚表。
2、对派生类重写了的虚函数地址进行覆盖,比如func1。
3、虚表当中新增派生类当中新的虚函数地址,比如func3和func4。

在调试过程中,某些编译器的监视窗口当中看不到虚表当中的func3和func4,可能是编译器的监视窗口故意隐藏了这两个函数,也可以认为这是一个小bug,此时想要看到派生类对象完整的虚表有两个方法。

使用内存监视窗口
在这里插入图片描述
使用代码打印虚表内容

typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{
	printf("虚表地址:%p\n", ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址
		ptr[i](); //使用虚函数地址调用虚函数
	}
	printf("\n");
}
int main()
{
	Base b;
	PrintVFT((VFPTR*)(*(int*)&b)); //打印基类对象b的虚表地址及其内容
	Derive d;
	PrintVFT((VFPTR*)(*(int*)&d)); //打印派生类对象d的虚表地址及其内容
	return 0;
}

多继承中的虚函数表

//基类1
class Base1
{
public:
	virtual void func1() 
	{ 
		cout << "Base1::func1()" << endl; 
	}
	virtual void func2() 
	{
		cout << "Base1::func2()" << endl;
	}
private:
	int _b1;
};

//基类2
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;
};

int main()
{
	Base1 b1;
	Base2 b2;
	Derive d;
	return 0;
}

在这里插入图片描述
多继承中,派生类的虚表生成过程如下:

1、分别继承各个基类的虚表内容到派生类的各个虚表当中。
2、对派生类重写了的虚函数地址进行覆盖(派生类中的各个虚表中存有该被重写虚函数地址的都需要进行覆盖),比如func1。
3、在派生类第一个继承基类部分的虚表当中新增派生类当中新的虚函数地址,比如func3。

看到派生类对象完整的虚表有两种方法。
一、使用内存监视窗口

在这里插入图片描述

二、使用代码打印虚表内容
在派生类第一个虚表地址的基础上,向后移sizeof(Base1)个字节即可得到第二个虚表的地址。

typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{
	printf("虚表地址:%p\n", ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址
		ptr[i](); //使用虚函数地址调用虚函数
	}
	printf("\n");
}
int main()
{
	Base1 b1;
	Base2 b2;
	PrintVFT((VFPTR*)(*(int*)&b1)); //打印基类对象b1的虚表地址及其内容
	PrintVFT((VFPTR*)(*(int*)&b2)); //打印基类对象b2的虚表地址及其内容
	Derive d;
	PrintVFT((VFPTR*)(*(int*)&d)); //打印派生类对象d的第一个虚表地址及其内容
	PrintVFT((VFPTR*)(*(int*)((char*)&d + sizeof(Base1)))); //打印派生类对象d的第二个虚表地址及其内容
	return 0;
}

菱形继承、菱形虚拟继承

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

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

相关文章

基于51单片机实现W25Q64-FLASH读写

一、前言 STC89C52是一款8位单片机,具有强大的功能和灵活性,广泛应用于各种嵌入式系统中。W25Q64是一款容量为64Mb的串行闪存芯片,采用SPI接口进行通信。本项目利用STC89C52单片机实现对W25Q64闪存芯片的读写操作,实现数据的读取和存储。 在本项目中,通过模拟SPI(Seria…

本末科技再获融资,直驱机器人来到价值兑现前夕?

在去年10月完成近亿元A轮融资后&#xff0c;今年9月&#xff0c;本末科技又宣布完成数千万元A轮融资&#xff0c;由立湾资本领投&#xff0c;建元投资跟投&#xff0c;北拓资本担任公司长期独家财务顾问。 本末科技长期专注于机器人机械模组与动力供应领域。虽然我国自2013年起…

项目交付谈判的6大技巧

针对项目交付问题&#xff0c;在面对甲方时&#xff0c;项目经理如果采用“和事佬”的态度&#xff0c;在不违背合理性或不产生无法承担后果前提下&#xff0c;尽量满足甲方的要求&#xff0c;以便顺利交付。这样往往容易导致项目范围蔓延&#xff0c;如果控制不当&#xff0c;…

信息技术--案例分析

文章目录 1 信息核心素养2 具备核心素养的学生3 导入原则4 导入方法5 新课讲授方法6 教学方法选择的依据7 教学方法的实施原则8 教学方法的设计意图9 小结10 作业 前言&#xff1a; 分值&#xff1a;本章节的内容在信息技术笔试中占据45分的分值&#xff0c;分别是18题10分&am…

合同交付类项目如何高效管理?

美国项目管理协会(PMI)保罗格蕾斯曾说:“当今社会,一切都是项目,一切也将成为项目。”在“万事皆项目”的背景下&#xff0c;企业在运营过程中会产生大量的项目型业务活动&#xff0c;例如&#xff1a;举办市场活动、产品研发、进行企业内训、采购招标、工程建设等等。那么按照…

涉案资金30个小目标,山东烟台网警打掉特大网络黑客犯罪团伙!

在人们日常生活中&#xff0c;有时会遇到“政府网站无法正常访问”“ 购物网页离奇丢失”“ 棋牌网游无法登录”等问题&#xff0c;就很莫名其妙。 这些事情一而再、再而三地发生&#xff0c;你能想到其背后潜藏着“黑手”和神秘组织吗&#xff1f; 近日&#xff0c;山东烟台…

【LeetCode-中等题】59. 螺旋矩阵 II

文章目录 题目方法一&#xff1a;二维数组缩圈填数字方法二&#xff1a; 题目 方法一&#xff1a;二维数组缩圈填数字 定义四个边界条件&#xff0c;每转一圈&#xff0c;把数值填进去&#xff0c;然后缩小一圈&#xff0c;直到不满足条件位置 结束循环条件可以是&#xff1a; …

精彩纷呈!安全狗亮相厦门市第五届网络安全宣传周开幕式

9月5日&#xff0c;厦门市第五届网络安全宣传周开幕式成功举行。 作为国内云原生安全领导厂商&#xff0c;安全狗也受邀参与此次大会。 厦门服云信息科技有限公司&#xff08;品牌名&#xff1a;安全狗&#xff09;成立于2013年&#xff0c;致力于提供云安全、&#xff08;云&a…

技术解码 | GB28181/SIP/SDP 协议--EasyGBS国标GB28181平台国标视频技术SDP解析

EasyGBS去年更换了新内核&#xff0c;新内核版本的平台性能更加稳定&#xff0c;我们也在不断对平台进行持续的功能优化和升级&#xff0c;始终保持EasyGBS平台在安防视频监控市场的技术先进性。EasyGBS拥有视频直播、录像存储、检索与回放、云台控制、告警上报、语音对讲、平台…

React+antd实现可编辑单元格,非官网写法,不使用可编辑行和form验证

antd3以上的写法乍一看还挺复杂&#xff0c;自己写了个精简版 没用EditableRowCell的结构&#xff0c;也不使用Context、高阶组件等&#xff0c;不使用form验证 最终效果&#xff1a; class EditableCell extends React.Component {state {editing: false};toggleEdit () &…

windows10下同时安装两个mysql服务的解决办法

windows10下同时安装两个mysql服务的解决办法 安装MySQL8.0.18版本 安装解压版MySQL8.0.25 &#xff08;1&#xff09; 下载解压包 &#xff08;2&#xff09;配置my.ini文件 [mysqld] port 3307 #mysql安装目录 basedir D:\HuanJing\Mysql\mysql-8.0.25-winx64 #mysql数据存…

WebDAV之π-Disk派盘 + FV管理器

FV管理器是一款清爽简洁的文件管理器。支持Android系统免root访问data和obb目录的工具。通过无线网络管理文件,局域网文件共享。支持jpg,png,gif,bmp,webp,tif,heif,dng,avif,ico,APNG等格式的图片,可能是支持最多图片格式的文件管理App了。 支持zip,rar,7z,iso…

学会制作企业纪念相册,记录企业成长瞬间

企业成长瞬间&#xff0c;美好记忆永存&#xff01; 今天来给大家分享一个超级实用的技能&#xff0c;如何制作企业纪念相册呢&#xff1f; 想要记录下企业成长历程中的每一个美好瞬间&#xff0c;把回忆永久保存下来吗&#xff1f;那就跟着我一起往下看吧&#xff01; 首先&…

分享20+个在线工具网站,60+常用工具

&#x1f482; 个人网站:【工具大全】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 今天给大家分享20在线工…

记录一次Docker与Redis冲突

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; 报错以及Bug ✨特色专栏&#xff1a; …

知网被处罚5000万,加强合规意识迫在眉睫

近日&#xff0c;国内知名学术搜索平台知网被处罚5000万的消息引起了广泛关注。此次处罚不仅是对知网在数据管理和用户隐私保护方面存在违规行为的警示&#xff0c;也是侧面反映了政府部门对企业合规力度监管的升级&#xff0c;更是提醒企业和个人加强合规意识的重要性。 合规…

mes系统开发和上位机软件开发哪个方向前景更好?

MES系统开发和上位机软件开发都是与工业自动化相关的领域&#xff0c;各自有不同的前景和发展方向。下面是对两者的简要介绍和前景评估&#xff1a;MES系统开发&#xff1a;MES系统&#xff08;制造执行系统&#xff09;是用于实现制造业生产管理和信息化的软件系统。它涵盖了生…

2023年中秋节和国庆节放假几天?用待办软件记录放假安排并提醒

进入公历9月&#xff0c;我们都期待着下个长假的到来。那么2023年中秋节和国庆节放假几天呢&#xff1f;因为今年的中秋节是公历的9月29日&#xff0c;所以今年的中秋节和国庆节是连在一起放假的。放假时间安排是9月29日至10月6日&#xff0c;一共放假8天。而10月7日和8日则是调…

Quartus Ⅱ中遇到的问题

记录Quartus中遇到的报错 一、Failed to launch MegaWizard Plug-In Manager 报错&#xff1a;Failed to launch MegaWizard Plug-In Manager. PLL IntelFPGA IP v18.1 could not be found in the specified librarypaths. 原因&#xff1a;编译后无法再打开IP核查看了&…

FreeRTOS 内存管理策略

目录 1. FreeRTOS 的核心功能 2. 为什么 FreeRTOS 要自己实现内存管理 3. FreeRTOS 的5种内存管理策略 3.1 Heap_1&#xff08;只建不删&#xff09; 3.2 Heap_2&#xff08;Heap1_Pro、提供删除&#xff0c;产生碎片&#xff09; 内存碎片是怎么出现的 3.3 Heap_3&…