C++ | 多态

news2024/11/16 3:34:33

目录

前言

一、多态的概念

二、多态的定义与使用

1、多态的构成条件

2、虚函数

3、虚函数的重写(覆盖)

4、虚函数重写的两个例外

(1)协变

(2)析构函数的重写

5、子类的指针或者引用调用

6、C++11的override与final关键字

7、重载、重定义(隐藏)、重写(覆盖)之间的对比 

三、抽象类

四、多态的原理

1、虚函数表

2、虚函数表的打印

3、多态的原理 

4、静态绑定与动态绑定

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

1、单继承中的虚函数表

2、多继承中的虚函数表


前言

        我们都知道类的三大特性分别为类的封装、继承与多态;前面我们介绍过了类的封装与继承,本章主要介绍类的多态这一性质,其实类的多态这一性质基于类的继承,可以说没有类的继承也就没有类的多态;

一、多态的概念

        所谓多态即多种形态,指不同的对象取调用一个函数(看起来像一个函数)会产生多种不同的效果;举个例子,在我们日常通勤中,我们可能会乘坐公共汽车、高铁等等交通工具;但是当不同人去完成买票这一动作时,会产生不同的效果;学生去买票会以学生票的价格卖给学生,成人去买票会以成人的价格卖给成人,我们今天学的多态也是如此;

二、多态的定义与使用

1、多态的构成条件

我们想构成一个多态,必须有以下条件(缺一不可);

1、虚函数的重写

2、必须通过基类的指针或者引用调用虚函数

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person BuyTicket" << endl;
	}
};

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

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

int main()
{
	Person p;
	Student stu;
	func(p);
	func(stu);
	return 0;
}

        上述代码实现了多态,该代码仅仅只是为了让大家看看多态,下面会介绍相关概念;

2、虚函数

        虚函数即为用关键字virtual修饰的成员函数;如上面的BuyTicket函数便是虚函数;

class A
{
public:
	virtual void func() {}; // 虚函数
};

3、虚函数的重写(覆盖)

        虚函数的重写,也叫覆盖,是构成多态的条件之一,子类中有一个与父类完全相同的虚函数,我们就称子类对该虚函数进行了重写(覆盖);这里的完全相同指的是返回值、函数参数、函数名相同,其中函数的参数只要参数类型相同即可,形参名可以不相同;

class A
{
public:
	// 虚函数
	virtual void func(int a1 = 1, double d1 = 2.0) 
	{
		cout << "A: " << endl;
		cout << a1 << ": " << d1 << endl;
	}; 
};

class B : public A
{
public:
	// 虚函数
	virtual void func(int a2 = 10, double d2 = 20.0)
	{
		cout << "B: " << endl;
		cout << a2 << ": " << d2 << endl;

	};
};

void Func(A* a)
{
	a->func();
}

int main()
{
	A a;
	B b;
	Func(&a);
	Func(&b);
	return 0;
}

        仔细观察上图与代码,我们发现虚函数的重写仅仅只是对实现进行了重写,我们父类的函数缺省值为1和2.0,而子类函数的缺省值为10和20.0,可打印出来的确实1和2,说明了虚函数的重写仅仅只是对实现部分进行了重写;

4、虚函数重写的两个例外

        上面我们说过,要实现虚函数的重写必须实现三同,可是,这其实也有特殊例外的语法;以下分别一一介绍;

(1)协变

        重写的虚函数可以返回值不同,但是他们的返回值必须为父子类关系的指针或引用;这种重写虚函数我们称为协变;如下所示;

class Person
{
public:
	virtual Person& BuyTicket()
	{
		cout << "Person BuyTicket" << endl;
		return *this;
	}
};

class Student : public Person
{
public:
	virtual Student& BuyTicket()
	{
		cout << "Student BuyTicket" << endl;
		return *this;
	}
};

void func(Person& p)
{
	p.BuyTicket();
	cout << endl;
}


int main()
{
	Person p;
	Student stu;

	func(p);

	func(stu);

	return 0;
}

其中返回父子类的指针和引用并非必须返回本类的父子类,还可以返回别的父子类;

class A
{
public:

};
class B : public A
{
public:

};
class Student;
class Person
{
public:
	virtual A* BuyTicket()
	{
		cout << "Person BuyTicket" << endl;
		A* p = new A;
		return p;
	}
};
class Student : public Person
{
public:
	virtual B* BuyTicket()
	{
		cout << "Student BuyTicket" << endl;
		B* p = new B;
		return p;
	}
};
void func(Person& p)
{
	p.BuyTicket();
	cout << endl;
}
int main()
{
	Person p;
	Student stu;

	func(p);

	func(stu);

	return 0;
}

        其中A、B类与Person、Student类并无关系,可是用他们作为返回值时,也可构成协变;其中父类写道父类虚函数的返回值中,子类写到子类虚函数的返回值中;

(2)析构函数的重写

        析构函数的重写可以不用不用相同的名字,实际上,底层还是会将其改成相同的名字---destructor,只不过我们看着好像不同的函数名实现了多态;

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

class B : public A
{
public:
	virtual ~B()
	{
		cout << "~B()" << endl;
	}
};

void Func(A* p)
{
	// 如果传入一个子类指针,如果没有多态根本不能完全释放
	delete p;
}

int main()
{
	A* ptra = new A;
	B* ptrb = new B;

	Func(ptra);
	Func(ptrb);

	return 0;
}

注意:有一个小细节,父类的虚函数必须写virtual关键字,子类可以不用写virtual,直接对其重写; 

5、子类的指针或者引用调用

        在上述所有代码中,我们实现多态的第二个条件就是子类的指针或者引用来调用虚函数,这是实现多态的必要条件;上述代码均有体现;

6、C++11的override与final关键字

在C++11中,新加了这两个关键字;

1、final用于修饰该虚函数不能在被重写了;

class A
{
public:
	virtual void func()final { }
 };

class B : public A
{
public:
	virtual void func() {};
};

2、override关键字用于检查子类中的某个虚函数是否被重写;

class A
{
public:
	virtual void func(int x) { }
 };

class B : public A
{
public:
	virtual void func(double x)override  {};
};

7、重载、重定义(隐藏)、重写(覆盖)之间的对比 

这三个是我们之前学过的概念,很容易混淆,此处对其一一进行对比;

重载:

        两个函数必须在同一个作用域中,且函数名相同,参数不同,底层使用函数名修饰规则实现;

重定义:

        两个函数作用域必须分别在派生类与基类中,函数名相同即可;

重写:

        两个函数作用域必须分别在派生类与基类中,函数名、参数、返回值都必须相同(除了那两个特例除外),两个函数必须是虚函数(基类必须加virtual关键字);

三、抽象类

        虚函数后买你加 = 0,这个函数就被称为纯虚函数,包含纯虚函数的类被称为抽象类;抽象类不能实例化处对象,继承后派生类也不可实例化处对象,除非对纯虚函数进行重写;

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

class Derive : public Base
{
	// 重写纯虚函数
	//virtual void func(){}
};

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

        纯虚函数可以有自己的函数体;只是有纯虚函数的类不能实例化处对象;

四、多态的原理

1、虚函数表

首先,给大家一道面试题,以下代码结果是多少;

// 32位机器下
class Base
{
public:
	virtual void func()
	{}
	int _a;
};

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

        结果是否超出你的预料呢? 当我们将一个函数声明为虚函数时,该类内会多出一个指针,这个指针被称为虚函数表指针,简称虚表指针;因此结果为8字节;

         我们再通过内存窗口来观察这个类,如下所示;

        虚函数表实际上是一个函数指针数组,里面储存的是虚函数指针,在VS系列编译器中通常以 nullptr 结尾;后面我们打印虚表也是通过这一特性进行打印;我们接着加入继承,继续观察;

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

class Derive : public Base
{
public:
	virtual void func1()
	{
		cout << "Derive func1" << endl;
	}
    int _b;
};

int main()
{
	Base b;

	Derive d;

	return 0;
}

        上述代码,我们新增了一个派生类,派生类对第一个虚函数进行了重写,然后在基类上增加了一个虚函数,一个普通成语函数;

        首先,通过监视窗口,我们发现派生类确实继承了父类的虚函数表,但是我们仔细观察发现虚函数表的地址不同,表中存放的虚函数地址也不同,其中,第一个虚函数我们对其进行了重写,因此第一个函数指针的地址不同,第二个虚函数我们并未对其进行重写,因此地址相同,而父类除了增加虚函数外,还增加了一个普通成员函数,普通成员函数并没存进虚函数表中;

总结一下:

        关于虚函数表(也称虚表),虚表也会被子类继承,只不过虚表被继承是子类拷贝父类的虚表,然后判断子类是否对其中某个虚函数进行重写,若重写,则用新的函数地址覆盖在原来虚表上的地址,若派生类新增了虚函数,也会继续依次填充在虚表后,虚表只会存放虚函数的地址;

        还有一些问题,我带着大家一起验证,关于虚表存在哪里、虚函数又存在哪里?有许多小伙伴对其充满疑惑;

        首先解答,虚表存在哪里?

// 该测试代码仅仅限于32位机器下
class Base
{
public:
	virtual void func1()
	{
		cout << "Base func1" << endl;
	}
	virtual void func2() 
	{
		cout << "Base func2" << endl;
	}
	void func3() { }
	int _a = 1;
};

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

int main()
{
	Base ba;
	Derive d;

	int a = 10;
	int* pa = new int;
	static int b = 20;
	const char* c = "xxxxxxxxxxxxxxxxxx";

	printf("栈区: %p\n", &a);
	printf("堆区: %p\n", pa);
	printf("静态区: %p\n", &b);
	printf("常量区: %p\n", c);
	printf("父类对象虚表地址: %p\n", *(int*)&ba);
	printf("子类对象虚表虚表地址: %p\n", *(int*)&d);

	return 0;
}

        不难看出,虚表的地址更接近与常量区,因此不难推测虚表存放在常量区,当然,往上也有一部分人说虚表存在静态区;

 虚函数又存放在哪呢?

        与大部分函数一样,虚函数也是存在代码段中的,此处补充一个小知识,代码段中存的并不是我们写的代码,而是二进制机器代码,我们写的代码首先转换成汇编代码,然后再由汇编代码转换成二进制机器代码;

2、虚函数表的打印

        前面我们也说了,在VS系类的编译器下,虚函数表的最后一位会补上 nullptr ,因此我们可通过这一特性打印我们的虚函数表,还是上面那个类,此处就不重复了;

// 函数指针重定义
typedef void (*Vf_ptr)();
void PrintVf_ptr(Vf_ptr table[])
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("table[%d]: %p ->", i, table[i]);
		Vf_ptr f = table[i];
		f();
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;

	Base* pa = &b;
	Derive* pd = &d;
	// 写法一
	PrintVf_ptr(*(Vf_ptr**)pa);
	PrintVf_ptr(*(Vf_ptr**)pd);
	// 写法二 只是用于32位机器
	//PrintVf_ptr((Vf_ptr*)*(int*)pa);
	//PrintVf_ptr((Vf_ptr*)*(int*)pd);
	return 0;
}

3、多态的原理 

        前面说了这么多,也只是为多态的原理进行铺垫;不止小伙伴们是否记得构成多态的基本条件,其中有一个是必须用父类的指针调用,那么为什么必须用父类的指针进行调用呢?我们假设不用父类的指针或引用,我们就使用值传递;拿以下类讲解;

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person BuyTicket" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Student BuyTicket" << endl;
	}
};
// 这里使用值传递
void func(Person p)
{
	p.BuyTicket();
	cout << endl;
}

int main()
{
	Person p;
	Student stu;
	func(p);
	func(stu);
	return 0;
}

        如果是值传递这里必然涉及拷贝的问题,那么虚表指针拷贝么,如果是拷贝,那么是拷贝父类虚表指针还是拷贝子类的虚表指针呢?这是不确定的,因为我们在调用这个函数里,不知道将来会被父类调用,还是子类调用,因此无法确定拷贝哪个虚表指针,因此必须要用父类的指针或者引用;而不能用切片直接传值;

        那么是如何实现的呢?当我们传指针或者引用时,加入原对象是父类对象,则直接传,若是子类对象,则会发生我们之前讲过的赋值兼容(切片);

        如果传过去的是父类,则类中存的是父类的虚表指针,而传过去的是子类,类中存的是子类的虚表指针;所以有了如下调用逻辑;

4、静态绑定与动态绑定

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

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

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

1、单继承中的虚函数表

        单继承中,父类的虚函数放在父类的虚函数表中,子类重写于父类的虚函数放在子类的虚函数表中,子类定义的虚函数也放在子类的虚函数表中;(放在理解为其指针存在虚函数表中);

class A
{
public:
	virtual void func1()
	{
		cout << "A::func1" << endl;
	}
	virtual void func2()
	{
		cout << "A::func2" << endl;
	}
	int _a = 1;
};

class B : public A
{
public:
	virtual void func1()
	{
		cout << "B::func1" << endl;
	}
	virtual void func3()
	{
		cout << "A::func1" << endl;
	}
	int _b = 2;
};

int main()
{
	A a;

	B b;
	return 0;
}

        父类的虚函数表中存放了func1与func2地址,而子类虚函数表中,存放了重写的func1,因此地址不同;以及继承父类没有重写的func2,因此地址相同;还有在子类定义的虚函数func3;

2、多继承中的虚函数表

        在多继承的体系下,又是如何继承的呢?首先我们好奇的是,多继承体系下,会有几张虚表呢?即派生类会有几个虚表指针呢?我们做了如下测试;

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

class Base2
{
public:
	virtual void func1() { cout << "Base2 void func1()" << endl; }
	virtual void func2() { cout << "Base2 void func2()" << endl; }
    int _b2;
};

class Derive : public Base1, public Base2
{
public:
	virtual void func1() { cout << "Derive void func1()" << endl; }
	virtual void func3() { cout << "Derive void func3()" << endl; }
    int _d;
};

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

        通过监视窗口不难看出,d类中有两个虚表指针,意味着有两种张虚表;那么问题又来了,我们在派生类定义的虚函数func3存在先继承的Base1中的虚表,还是存在Base2中的虚表呢?我们使用前面的虚表打印的代码,进行测试;结果如下;

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

class Base2
{
public:
	virtual void func1() { cout << "Base2 void func1()" << endl; }
	virtual void func2() { cout << "Base2 void func2()" << endl; }
    int _b2;
};

class Derive : public Base1, public Base2
{
public:
	virtual void func1() { cout << "Derive void func1()" << endl; }
	virtual void func3() { cout << "Derive void func3()" << endl; }
    int _d;
};

// 函数指针重定义
typedef void (*Vf_ptr)();
void PrintVf_ptr(Vf_ptr table[])
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("table[%d]: %p ->", i, table[i]);
		Vf_ptr f = table[i];
		f();
	}
	cout << endl;
}

int main()
{
	Derive d;
	// 打印Base1虚表
	PrintVf_ptr(*(Vf_ptr**)&d);
	// 打印Base2虚表
	Base2* ptr = &d; // 切片
	PrintVf_ptr(*(Vf_ptr**)ptr);
	return 0;
}

        经过测试,我们发现我们在Derive新定义的虚函数,存在了先继承的Base1的虚表中,仔细观察的小伙伴们注意到了,Derive对func2没有进行重写,因此,我们继承的两张虚表的func2函数的地址不同可以理解,因为是两个不同的函数,可是为什么重写的func1的地址也不同呢?我们可以看到,他们明明是调用的同一个函数(—>后面是函数执行结果),那为什么地址不同呢??

int main()
{
	Derive d;
	Base1* ptr1 = &d;
	Base2* ptr2 = &d;
    // 分别调用两个虚函数表中重写的func1
	ptr1->func1();
	ptr2->func1();

}

        关于调用同一个函数,这个函数却有两个地址的问题,我们还得观察汇编代码;接下来我带着大家一起观察我们程序的汇编代码;(call为汇编指令中的函数调用指令,后面接函数地址,而jmp为跳转指令,后接地址)

接着我们看Base2中的func1是在汇编代码中是如何调用的;

 

        重新捋一下思路,在多重继承下,子类继承了两个及以上的来自父类的虚表,我们重写来自父类的虚函数(且这个虚函数多个父类都有)时,我们会对其重写,重写的虚函数只有一个,两个虚表中,各保存一份,上述问题讨论的是,为什么同一个虚函数地址不同;

        观察汇编代码,我们发现,保存在Base2中的func1会多经过几次跳转;但最终还是会来到最终的函数入口地址;完成调用;其实与this指针有关;当我们分别用Base1*与Base2*类型的指针调用func1时,模型图如下所示;

        当我们用Base2*调用func1时,其中有一个动作时减8,其实那个动作正是调整this指针的指向; 由于Base1*的指向本来就是子类Derive的开始,因此不用调整,所以两个虚函数表中的func1地址不同的本质原因是,Base2*调用fucn1时还需要调用一个调整this指针的动作;

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

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

相关文章

unity对象池系统

当游戏场景中出现大量的可重复利用的物体时&#xff0c;通过Destory来销毁再创建会触发不必要的GC回收机制&#xff0c;浪费性能&#xff0c;我们可以利用unity自带的对象池系统&#xff0c;从而节约性能来得到同样的效果。 为了使用这个对象池系统&#xff0c;我写了一个瞬间产…

element-plus 报错 ResizeObserver loop limit exceeded 解决

使用Element-plus&#xff0c;页面重置大小时&#xff0c;出现如下报错&#xff1a; Uncaught runtime errors: ERROR ResizeObserver loop limit exceeded at handleError (webpack-internal:///./node_modules/webpack-dev-server/client/overlay.js:252:58) at ev…

DETR3D: 3D Object Detection from Multi-view Images via 3D-to-2D Queries

DETR3D: 3D Object Detection from Multi-view Images via 3D-to-2D Queries 目的 本文提出了一个 仅使用 2D 信息的&#xff0c;3D 目标检测网络&#xff0c;并且比依赖与 密集的深度预测 或者 3D 重建的过程。该网络使用了 和 DETR 相似的 trasformer decoder &#xff0c;…

SpringBoot中集成jasypt-spring-boot实现配置文件数据加密脱敏

场景 经常会遇到这样一种情况&#xff1a;项目的配置文件中总有一些敏感信息&#xff0c;比如数据源的url、用户名、 密码....这些信息一旦被暴露那么整个数据库都将会被泄漏&#xff0c;那么如何将这些配置隐藏呢。 除了使用手动将加密之后的配置写入到配置文件中&#xff…

Win把老外惹恼了!

随着Windows 10创意者更新&#xff08;民间称Win10.3&#xff09;推送规模的加大&#xff0c;其暴露出来的槽点越来越多。 此前&#xff0c;我们已经报道过新版的Defender升级为安全助手&#xff0c;类似于国内有名的xx管家和数字卫士&#xff0c;全面接管PC健康事务。 然而&a…

【css】用css样式快速写右上角badge徽标,颜色设置为渐变色

先看效果展示&#xff0c;已公开显示在图片卡片的右上角。 首先是dom代码&#xff1a;需要两个view或者div&#xff0c;public-badge是“已公开”那个矩形&#xff0c;show-signal是右边那个下三角&#xff0c;也就是阴影部分&#xff0c;这样看起来比较有立体感。 <view…

Linux驱动进阶(四)——内外存访问

文章目录 前言内存分配kmalloc函数vmalloc()函数后备高速缓存 页面分配内存分配物理地址和虚拟地址之间的转换 设备I/O端口的访问Linux I/O端口读写函数I/O内存读写使用I/O端口 小结 前言 驱动程序加载成功的一个关键因素&#xff0c;就是内核能够为驱动程序分配足够的内存空间…

论文解读:Inpaint Anything: Segment Anything Meets Image Inpainting

论文&#xff1a;https://arxiv.org/pdf/2304.06790.pdf 代码&#xff1a;https://github.com/geekyutao/Inpaint-Anything 图1&#xff1a;Inpaint Anything示意图。用户可以通过点击图像中的任何对象来选择它。借助强大的视觉模型&#xff0c;例如SAM[7]、LaMa [13]和稳定扩散…

我叫李明,我是一名开发人员

目录 一、这是一个故事 二、不屈不挠的李明 三、化解于无形 四、总结 一、这是一个故事 这个故事的主人公是一个年轻的程序员&#xff0c;他叫做李明。李明是一名技术过硬、工作认真负责的程序员&#xff0c;他的工作是开发一款新的软件产品。这款软件是一款在线购物平…

【Java基础】第四章 Object 类应用

系列文章目录 [Java基础] 第一章 String类应用及分析 [Java基础] 第二章 数组应用及源码分析 [Java基础] 第三章 StringBuffer 和 StringBuilder 类应用及源码分析 [Java基础] 第四章 Object 类应用 文章目录 系列文章目录前言一、如何使用Object&#xff1f;1.1、显式继承1.2…

c++内存映射文件

概念 将一个文件直接映射到进程的进程空间中&#xff08;“映射”就是建立一种对应关系,这里指硬盘上文件的位置与进程逻辑地址空间中一块相同区域之间一 一对应,这种关系纯属是逻辑上的概念&#xff0c;物理上是不存在的&#xff09;&#xff0c;这样可以通过内存指针用读写内…

Web-登录功能实现(含JWT令牌)

登录功能 这个登陆功能先不返回JWT令牌 登陆会返回JWT令牌 一会在登陆验证时讲解JWT令牌&#xff08;返回的data就是它&#xff09; 登录校验 概述 就是你比如复制一个url 用一个未曾登陆对应url系统的浏览器访问 他会先进入登陆页面 登陆校验就是实现这个功能 简而言之…

基于EasyExcel的单元格合并自定义算法处理

基于EasyExcel导出Excel后&#xff0c;通过对合并单元格的简单规则配置&#xff0c;实现如下图所示的单元格合并效果&#xff1a; 效果截图 原表格数据如下&#xff1a; 通过配置单元格合并规则后&#xff0c;生成的合并后的表格如下&#xff1a; 注&#xff1a;其中第三列&a…

Android Studio连接安卓手机

1. 创建项目 2. 下载Google USB Driver 点击右上角红框的【SDK Manager】->【SDK Tools】。 也可以在 【tools】->【SDK Manager】->【SDK Tools】下进入。 点击Google USB Driver&#xff0c;下载后点ok。 3. 环境变量 右键【我的电脑】->【高级系统设置】-&g…

基于微信小程序的高校新生自助报道系统设计与实现(Java+spring boot+MySQL+小程序)

获取源码或者论文请私信博主 演示视频&#xff1a; 基于微信小程序的高校新生自助报道系统设计与实现&#xff08;Javaspring bootMySQL微信小程序&#xff09; 使用技术&#xff1a; 前端&#xff1a;html css javascript jQuery ajax thymeleaf 微信小程序 后端&#xff1…

123、仿真-基于51单片机的电流控制仿真系统设计(Proteus仿真+程序+原理图+参考论文+配套资料等)

方案选择 单片机的选择 方案一&#xff1a;STM32系列单片机控制&#xff0c;该型号单片机为LQFP44封装&#xff0c;内部资源足够用于本次设计。STM32F103系列芯片最高工作频率可达72MHZ&#xff0c;在存储器的01等等待周期仿真时可达到1.25Mip/MHZ(Dhrystone2.1)。内部128k字节…

java报错- 类文件具有错误的版本 61.0, 应为 52.0 请删除该文件或确保该文件位于正确的类路径子目录中。

SpringBoot使用了3.0或者3.0以上&#xff0c;因为Spring官方发布从Spring6以及SprinBoot3.0开始最低支持JDK17&#xff0c;所以仅需将SpringBoot版本降低为3.0以下即可。

ES6类-继承-Symbol-模版字符串

目录 类 继承 ES5 如何继承 ES6继承 Symbol 用途 可以产生唯一的值&#xff0c;独一无二的值 解决命名冲突 getOwnPropertySymbols() 作为全局注册表 缓存 Symbol.for() 消除魔术字符串 模版字符串 类 在javascript语言中&#xff0c;生成实例对象使用构造函数&#xf…

数据库基本操作-----数据库用户管理和授权

目录 一、数据库用户管理 1&#xff0e;新建用户 2&#xff0e;查看用户信息 3&#xff0e;重命名用户 4&#xff0e;删除用户 ​编辑5&#xff0e;修改当前登录用户密码 6&#xff0e;修改其他用户密码 7&#xff0e;忘记 root 密码的解决办法 &#xff08;1&#xff09;修…

Redis数据类型 — List

List 列表是简单的字符串列表&#xff0c;按照插入顺序排序&#xff0c;可以从头部或尾部向 List 列表添加元素。 List内部实现 List 类型的底层数据结构是由双向链表或压缩列表实现的&#xff1a; 如果列表的元素个数小于 512 个&#xff08;默认值&#xff0c;可由 list-m…