[C++基础]-多态

news2024/11/28 20:52:36

前言

作者小蜗牛向前冲

名言我可以接受失败,但我不能接受放弃

  如果觉的博主的文章还不错的话,还请点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。

本期学习目标:认识什么是多态, 认识抽象类,理解多态的原理,理解多承和多态常见的面试问题。 

目录

一、认识多态

1、什么是多态

2、虚函数

3、多态的定义

4、多态中虚函数的二种特殊情况

二、抽象类

1、概念

2、 接口继承和实现继承

三、多态的原理

1、虚函数表

2、多态原理剖析

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

四、多态的其他知识

1、C++11 override 和 final

2、重载、覆盖(重写)、隐藏(重定义)的对比

3、动态绑定与静态绑定

五、分享继承和多态常见的面试问题


一、认识多态

1、什么是多态

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态。

举个例子来说:某宝常年到大学的开学季就会举办扫描领红包活动,但是我们发现每个领到的金额都不同,而且相对来是非活跃用户红包金额更大,这种我们不同角色,分到吧同金额的红包我们就可以称为多态。

2、虚函数

在认识多态前,我们首先要认识一一下什么是虚函数:

在类中被关键字virtual修饰的函数我们就称为虚函数,就如red_packet函数,在继承中我用关键字virtual解决的菱形继承问题,这里要注意区分二则的用法,一个是在继承这作用于类,一个是在多态中作用于函数。

class Peson
{
	virtual void red_packet()
	{
		cout << "小额红包" << endl;
	}
protected:
	string _name;
	int _age;
};

3、多态的定义

那么在继承中要构成多态还有两个条件:

1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

 虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

下面我们来看一段代码:

class Person
{
public:
	//虚函数
	virtual void red_packet()
	{
		cout << "普通红包" << endl;
	}
protected:
	string _name;
	int _age;
};

class  active_people:public Person
{
public:
	virtual void red_packet()
	{
		cout << "小额红包" << endl;
	}
};

void fun(Peson& p)
{
	p.red_packet();
}
int main()
{
	Person p;
	active_people ap;
	fun(p);
	fun(ap);
	return 0;
}

 

这里我们通过父类的引用形成了多态。

特别注意子类函数可以不加victual,但父类必须加victual才构成虚函数

4、多态中虚函数的二种特殊情况

我们都知道,要构成多态就用就要满足构成多态的二个条件:其中被调用的函数必须是虚函数,也就是说必须加是virtual。

但是存在二种特殊情况也构成多态:

析构函数(函数名字不同构成的重写)

class Person
{
public:
	~Person()
	{
		cout << " Person delete" << endl;
		delete[]_p;
	}
protected:
	int* _p = new int[10];

};

class  active_people:public Person
{
public:
	~active_people()
	{
		cout << " active_people delete" << endl;
		delete[]_d;
	}
protected:
	int* _d = new int[15];

};

int main()
{
	Person p;
	active_people ap;
	return 0;
}

通过这段代码我们可以看出,父子类在调用析构函数的时候,是先调用子类的析构函数,子类的析构函数调用完成后,子类会自动调用父类的析构函数。 

但是如果我对代码进行一下更改:

int main()
{
	Person* ptr1 = new Person;
	Person* ptr2 = new active_people;
	delete ptr1;
	delete ptr2;
	return 0;
}

这里我们发现子类并没有被析构掉。为什么呢?

这就得提一下delete的特性:

1:使用指针调用析构

2:operator delete(ptr) 

也就是说这时候是一个普通调用 (是什么类型就调用什么用的析构函数),所以才只会调用父类的析构,但这样的行为不是我们期望的(存在内存泄露),其实,我们期望应该是一个多态调用,指向父类调用父类的析构,指向子类调用子类的析构函数,只要我们在父类的析构函数上加上virtual就可以解决这个问题了。

virtual ~Person()
{
	cout << " Person delete" << endl;
	delete[]_p;
}

就是为了让父类的析构函数为虚函数: 

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

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

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

举例:

class A
{};

class B : public A
{};
class Person
{
public:

	virtual A* red_packet()
	{
		cout << "普通红包" << endl;
		return nullptr;
	}
};

class  active_people :public Person
{
public:
	virtual B* red_packet()
	{
		cout << "小额红包" << endl;
		return nullptr;
	}
};

void fun(Person& p)
{
	p.red_packet();
}
int main()
{
	Person p;
	active_people ap;
	fun(p);
	fun(ap);
	return 0;
}

 这里我们只要简单知道在协变这要求:三同中,返回值可以不同,但是要求返回值必须是一个父子类关系的指针或者引用

二、抽象类

1、概念

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

抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

那这有什么用呢?

其实就是强制我们重写子类的虚函数

2、 接口继承和实现继承

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

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

三、多态的原理

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

我想不少同学会认为是8,可能认为 成员函数存放在公共的代码段,我们就只要计算成员变量大小就可以了,根据内存对齐就可以得到大小为8个字节。

但真的是这样吗?

我们打印出类的大小:

cout << sizeof(Base) << endl;

为什么是16个字节呢?

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

这时我们进行调试打开监视窗口 

发现b对象中存放了_vfptr的东西,这又是什么呢?

其实是一个指针, 对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

所以说根据内存对其很容易得到类的大小为16。

那我们在思考一个问题虚函数表到底存放在哪里呢?

下面我们通过一份代码大致推断一下:

int main()
{
	int a = 0;
	printf("栈:%08lxp \n",& a);

	const char* str = "hello world";
	printf("代码段 / 常量区:%08lxp \n", str);
	static int b = 0;
	printf("静态区/数据段: %08lxp\n", &b);
	//取类这前4个字节的地址,也就是虚表地址
	Base be;
	printf("虚表: %08lxp\n", *((int*)&be));

}

通过打印我们发现虚表位于代码段/常量区中。 

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

class Derive : public Base
{
public:
	//子类重写的虚函数
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	//非虚函数,重定义
	void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
private:
	int _d = 2;
};

虚表当中存储的就是虚函数的地址,因为父类当中的Func1和Func2都是虚函数,所以父类对象b的虚表当中存储的就是虚函数Func1和Func2的地址。

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

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

那我们不由的思考到底虚表是那个阶段就开始初始化?

  • 其实虚表指针是在构造函数初始化列表的时候填入对象的,虚表是在编译的时候就生成了。
  • 虚表里面存放是虚函数地址,同普通函数一样编译完成就放在代码段这
  • 一个类这所有的虚函数,都会放在虚表这。
  • 子类会将父类的虚表拷贝一份,然后用重写的虚函数地址覆盖到原来虚表中的函数地址因此虚函数的重写,也叫虚函数的覆盖。

2、多态原理剖析

前面我们说了那么多,但是虚表是如何实现多态的呢?

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;
	Student Johnson;
	Person* p1 = &Mike;
	Person* p2 = &Johnson;
	Func(p1);
	Func(p2);
	return 0;
}
  •  p1指针指向Mike对象,当我们调用Func函数,且将p1传给Func时,p1->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
  • p2指针指向johnson对象时p2->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket.
  •  这样就实现出了不同对象去完成同一行为时,展现出不同的形态

 我们都知道达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调
用虚函数,
但这是为什么呢?下面我们通过反汇编了解一下

void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Person mike;
Func(&mike);
mike.BuyTicket();
return 0;
}
// 以下汇编代码中跟你这个问题不相关的都被去掉了
void Func(Person* p)
{
...
p->BuyTicket();
// p中存的是mike对象的指针,将p移动到eax中
001940DE mov eax,dword ptr [p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来
以后到对象的中取找的。
001940EA call eax
00头1940EC cmp esi,esp
}
int main()
{
...
// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调
用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
mike.BuyTicket();
00195182 lea ecx,[mike]
00195185 call Person::BuyTicket (01914F6h)
...
}

看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的 .

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

单继承的虚函数表

这里没什么可以过多分析,上面我们都是多单继承虚表的分析,但是我们要注意一个现象。

class Base
{
public:
	//父类虚函数
	virtual void fun1()
	{
		cout << "Base::fun1" << endl;
	}
	virtual void fun2()
	{
		cout << "Base::fun2" << endl;
	}
private:
	int a;
};

class Derive :public Base
{
	//重写的虚函数
	virtual void fun1()
	{
		cout << "Derive::fun1" << endl;
	}
	//虚函数
	virtual void fun3()
	{
		cout << "Derive::fun3" << endl;
	}
	//普通函数
	void fun4()
	{
		cout << "DERIVE::fun4" << endl;
	}
private:
	int b;
};

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

这里虽然从监视窗口我们并没有从虚表中看到fun3虚函数的地址,但是我们通过查询虚表指针的地址,因为虚表本质是一个函数指针数组,我们可以发现fun3函数的地址是存在的,也就说明,只要是虚函数就会进虚表,但是vs编译器可能会对非重写的虚函数优化,从而在监视窗口中我们不能发现他。

为了更好的验证,我们对多继承关系的虚函数表的讨论,我们写一个函数来打印虚函数的地址:

//为函数指针数组重新取个名字
// 写法1函数指针数组
//void PrintVFTbale(VFPtr vft[], int n)
//{
//	for (int i = 0; i < n; ++i)
//	{
//		printf("[%d]:%p\n", i, vft[i]);
//	}
//}
typedef void(*VFPtr)();
//写法2
void PrintVFTbale(VFPtr vft[])
{
	for (int i = 0; vft[i] != nullptr; ++i)
	{
		printf("[%d]:%p->", i, vft[i]);
		vft[i]();
	}
	cout << endl;

 

这里我们要注意,要想打印虚表,就要取类这前4个字节(32位平台)/前8个字节(64位平台), 

我们上面的取法是通用的,(void*)在32位平台4个字节,64位平台8个字节。而我们*(void**)就自然的找到了void*从而在不同平台上形成自适应。

多继承虚表函数(在vs2013上测试)

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:
	//父类重写了func1
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};


// 函数指针数组
typedef void(*VFPtr)();
void PrintVFTbale(VFPtr vft[])
{
	for (int i = 0; vft[i] != nullptr; ++i)
	{
		printf("[%d]:%p->", i, vft[i]);
		vft[i]();
	}
	cout << endl;
}


int main()
{
	Base1 b1;
	Base2 b2;
	PrintVFTbale((VFPtr*)(*(void**)&b1));
	PrintVFTbale((VFPtr*)(*(void**)&b2));

	Derive d;
	PrintVFTbale((VFPtr*)(*(void**)&d));
	//打印父类的第二张虚表    写法1
	//PrintVFTbale((VFPtr*)(*(void**)((char*)&d+sizeof(Base1))));
	//写法2
	Base2* ptr2 = &d;
	PrintVFTbale((VFPtr*)(*(void**)ptr2));

	return 0;
}

  •  base1h和base2都有一张虚表,都被Derive继承(有二张虚表),但是没有被重写的func3通常是放在第一张虚表中的。
  • 我们在找父类的第二张虚表的时候,可以通过字节偏远的方法找到,也可以直接用    Base2* ptr2 = &d;切片的方式自动偏移。
  • 这里我们打印虚函数地址结束调条件是最后一个元素为nullptr,但是这种情况在大部分情况下是适应的,要注意的是,虚表的具体实现方式可能因编译器而异,不同的编译器可能会有不同的实现细节。因此,在特定的编译器和环境中,虚表的最后一个元素是否为nullptr可能会有所不同。但根据常见的编译器实现,将最后一个元素设为nullptr是一种常见的做法。

四、多态的其他知识

1、C++11 override 和 final

在学习override和final时,我们先思考一个问题如何实现一个不能被继承的类:

方法1:将构造函数私有(c++98)

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

class B : public A
{};

这时候因为对象对无法建立,自然就无法被继承

方法2 类定义的时候加final

这时候我们称类为最终类,类不能被继承。

class A final
{

};

其实final还有一个功能修饰函数,这该函数就不能被重写 

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

2、重载、覆盖(重写)、隐藏(重定义)的对比

重载

二个函数在同一个作用域

函数名相同/参数不同

重定义(隐藏)

二个函数分别在父类和子类的作用域

函数名相同

二个父类和子类的同名函数不构成重写就是重定义

重写(覆盖)

二个函数分别在父类和子类的作用域

函数名/参数/返回值必须相同(协变除外)

二个函数必须是虚函数

3、动态绑定与静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载.
  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

五、分享继承和多态常见的面试问题

1. 什么是多态

它指的是同一种操作或接口可以被不同类型的对象以不同的方式实现和处理的能力。具体来说,多态性使得我们可以使用基类(父类)的指针或引用来引用子类(派生类)的对象,并且根据具体的对象类型执行对应的方法或操作。

多态有两种表现形式:静态多态(编译时多态)和动态多态(运行时多态)。

  1. 静态多态:通过函数重载和运算符重载实现,编译器在编译阶段根据参数的静态类型决定调用哪个函数或操作符。

  2. 动态多态:通过虚函数和基类指针/引用实现,运行时根据对象的动态类型来确定调用哪个函数。即使使用基类指针或引用,也能够在运行时确定实际调用的是子类的方法。

2. 什么是重载、重写(覆盖)、重定义(隐藏)?

重载:同一作用域内,函数名相同参数不同
重写:子类和父类的虚函数,名称、返回值、参数都相同,称子类重写了父类的虚函数
重定义:子类和父类的函数名相同,称子类隐藏了父类的某个函数。

3. 多态的实现原理?

父类和子类之中保存的虚表指针是不一样的,通过传入指针或者引用(本质也是指针)确定去子类还是父类之中去寻找虚表指针,最后达到调用不同虚函数的目的。 

4. inline函数可以是虚函数吗

 可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去,如果不构成多态直接调用,则内联展开。在类里面定义的函数默认内联

5. 静态成员可以是虚函数吗

 不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6. 构造函数可以是虚函数吗

 不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

 7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

 可以,并且最好把基类的析构函数定义成虚函数。

通过基类指针或引用来管理派生类对象。如果基类中的析构函数不是虚函数,当通过基类指针或引用删除派生类对象时,可能只会调用到基类的析构函数,而不会调用派生类的析构函数,从而导致派生类可能存在资源泄漏或未被正确清理的问题

8. 对象访问普通函数快还是虚函数更快?

 先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

9. 虚函数表是在什么阶段生成的,存在哪的?

 虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

10. C++菱形继承的问题?虚继承的原理? 

 菱形虚拟继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。
虚继承对于相同的虚基类在对象当中只会存储一份,若要访问虚基类的成员需要通过虚基表获取到偏移量,进而找到对应的虚基类成员,从而解决了数据冗余和二义性的问题。

11.什么是抽象类?抽象类的作用?

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类 。

抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

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

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

相关文章

智能AI系统源码ChatGPT系统源码+详细搭建部署教程+AI绘画系统+已支持OpenAI GPT全模型+国内AI全模型

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统&#xff0c;支持OpenAI GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作Chat…

NFT Insider#110:The Sandbox与TB Media Global合作,YGG Web3游戏峰会阵容揭晓

引言&#xff1a;NFT Insider由NFT收藏组织WHALE Members、BeepCrypto出品&#xff0c;浓缩每周NFT新闻&#xff0c;为大家带来关于NFT最全面、最新鲜、最有价值的讯息。每期周报将从NFT市场数据&#xff0c;艺术新闻类&#xff0c;游戏新闻类&#xff0c;虚拟世界类&#xff0…

双字单字拆分/合并操作(博途SCL源代码)

博途PLC的位、字节拆分和合并操作还可以参考下面的文章链接: 博途PLC 位/字/字节 Bit/ Word/Byte拆分与合并_博途的bit-CSDN博客有时候我们需要将分散分布的开关量信号组合为一个整体比如一个字节再完成发送,或者一些报警联锁控制,组合为一个字方便触摸屏报警记录等,下面我…

day11_oop_面向对象基础

零、今日内容 一、作业 二、面向对象 一、作业 题目26 设计方法,在一个数组中&#xff0c;返回所有的 指定数据的下标 例如, 这个数组[1,2,8,4,5,7,8,7,8,9],找到其中元素8的下标[2,6,8]public static void main(String[] args) {int[] arr {1,2,8,4,5,7,8,7,8,9};int[] ind…

云计算安全的新挑战:零信任架构的应用

文章目录 云计算的安全挑战什么是零信任架构&#xff1f;零信任架构的应用1. 多因素身份验证&#xff08;MFA&#xff09;2. 访问控制和策略3. 安全信息和事件管理&#xff08;SIEM&#xff09;4. 安全的应用程序开发 零信任架构的未来 &#x1f389;欢迎来到云计算技术应用专栏…

vue-3

一、文章内容概括 1.生命周期 生命周期介绍生命周期的四个阶段生命周期钩子声明周期案例 2.工程化开发入门 工程化开发和脚手架项目运行流程组件化组件注册 二、Vue生命周期 思考&#xff1a;什么时候可以发送初始化渲染请求&#xff1f;&#xff08;越早越好&#xff09…

二叉搜索树的初步认识

二叉搜索树与常见的查找算法有什么区别&#xff1f; 首先&#xff0c;如果有同学不知道有哪些查找算法可以查看&#xff1a;常见查找算法_加瓦不加班的博客-CSDN博客 如果还有一些不了解的&#xff0c;请查看加瓦不加班_数据结构,链表,递归-CSDN博客 接下来&#xff0c;我们来…

玄子Share- IDEA 2023 SpringBoot 热部署

玄子Share- IDEA 2023 SpringBoot 热部署 修改 IDEA 部署设置 IDEA 勾选如下选项 新建 SpringBoot 项目 项目构建慢的将 Spring Initializr 服务器 URL 改为阿里云&#xff1a;https://start.aliyun.com/ 在这里直接勾选Spring Boot Devtools插件即可 测试 切出 IDEA 项目文…

「专题速递」AR协作、智能NPC、数字人的应用与未来

元宇宙是一个融合了虚拟现实、增强现实、人工智能和云计算等技术的综合概念。它旨在创造一个高度沉浸式的虚拟环境&#xff0c;允许用户在其中交互、创造和共享内容。在元宇宙中&#xff0c;人们可以建立虚拟身份、参与虚拟社交&#xff0c;并享受无限的虚拟体验。 作为互联网大…

2023年H1汽车社媒营销趋势报告

2023上半年车市“内卷”&#xff0c;汽车营销玩法越来越多样&#xff0c;品牌的矩阵式营销也成为头部车企重点营销战略。 据乘联会预测&#xff0c;2023下半年车市将受到二季度价格战的透支&#xff0c;需要一定修复期。在整体形势较不利的情况下&#xff0c;车企如何破局实现销…

通过IP地址如何计算相关地址

以IP地址为1921681005 子网掩码是2552552550为例。算出网络地址、广播地址、地址范围、主机数。 分步骤计算 1&#xff09; 将IP地址和子网掩码换算为二进制&#xff0c;子网掩码连续全1的是网络地址&#xff0c;后面的是主机地址。 虚线前为网络地址&#xff0c;虚线后为主机…

基于Java的个性化旅游攻略系统设计与实现(源码+lw+ppt+部署文档+视频讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

yolov7的bug,无法指定显卡(程序默认0号卡)

**问题&#xff1a;**命令行参数指定的是4号卡&#xff0c;但实际却总是在0号卡建立进程 真抽象啊&#xff0c;这一步&#xff0c;模型被送到0号卡&#xff0c;但实际上&#xff0c;送到了4号卡&#xff08;进程是在4号卡上建立的&#xff09; 解决办法&#xff1a; 在train.py…

【LeetCode热题100】--230.二叉搜索树中第K小的元素

230.二叉搜索树中第K小的元素 给定一个二叉搜索树的根节点 root &#xff0c;和一个整数 k &#xff0c;请你设计一个算法查找其中第 k 个最小元素&#xff08;从 1 开始计数&#xff09;。 二叉搜索树的中序遍历是有序的&#xff0c;所有先得到其有序序列&#xff0c;然后在取…

端口没有占用,Springboot却提示端口占用了

1.问题描述 *************************** APPLICATION FAILED TO START ***************************Description:Web server failed to start. Port 19004 was already in use.Action:Identify and stop the process thats listening on port 19004 or configure this applica…

【设计模式】使用原型模式完成业务中“各种O”的转换

文章目录 1.原型模式概述2.浅拷贝与深拷贝2.1.浅拷贝的实现方式2.2.深拷贝的实现方式 3.结语 1.原型模式概述 原型模式是一种非常简单易懂的模型&#xff0c;在书上的定义是这样的&#xff1a; Specify the kinds of objects to create using a prototypical instance,and cre…

RobotFramework 自动化测试实战基础篇

RobotFramework 简介和特点 RobotFramework 不是一个测试工具&#xff0c;准确来说&#xff0c;它是一个自动化测试框架&#xff0c;或者说它是一个自动化测试平台。他拥有的特性如下&#xff1a; 支持关键字驱动、数据驱动和行为驱动测试执行报告和日志是HTML格式&#xff0…

并发编程基础知识

一、线程的基础概念 一、基础概念 1.1 进程与线程A 什么是进程&#xff1f; 进程是指运行中的程序。 比如我们使用钉钉&#xff0c;浏览器&#xff0c;需要启动这个程序&#xff0c;操作系统会给这个程序分配一定的资源&#xff08;占用内存资源&#xff09;。 什么线程&a…

android 修改输出apk的包名

一&#xff0c;打包方式使用IDE菜单选项 二、在app级别的build.gradle下配置&#xff1a; static def releaseTime() {return new Date().format("yyyyMMdd.kkmm", TimeZone.getTimeZone("GMT8")) }android.applicationVariants.all { variant ->print…

滚珠螺母的生产流程

滚珠螺母是机械领域中重要的零部件之一&#xff0c;它的生产过程涉及多个环节和步骤。以下是一个概括性的滚珠螺母生产流程&#xff1a; 1、原料采购&#xff1a;首先需要采购适合制造滚珠螺母的原材料&#xff0c;如钢棒、钢板等。 2、钢材切割&#xff1a;将采购的钢棒、钢板…