C++:多态究竟是什么?为何能成为面向对象的重要手段之一?

C++:多态究竟是什么?为何能成为面向对象的重要手段之一?

  • 前言
  • 一、多态的概念
  • 二、多态的定义及实现
    • 2.1 多态的构成条件
    • 2. 2 虚函数
    • 2.3 虚函数的重写
      • 2.3.1 虚函数重写的例外1:协变(基类与派生类虚函数返回值类型不同)
      • 2.3.2 虚函数重写的例外2:析构函数的重写(基类与派生类析构函数的名字不同)
    • 2.4 C++11 override 和 final
      • 2.4.1 final
      • 2.4.2 override
    • 2.5 重载、覆盖(重写)、隐藏(重定义)的对比
  • 三、抽象类
    • 3.1 概念
    • 3.2 接口继承和实现继承
  • 四、虚函数及虚函数表
    • 4.1 虚函数表
    • 4.2 虚函数重写覆盖虚表
    • 4.3 虚函数存在哪的?虚表存在哪的?
  • 五、多态原理
    • 5.1 多态调用过程及原理
    • 5.2 多态运行时确定?
    • 5.3 动态绑定与静态绑定
  • 六、单继承和多继承关系的虚函数表
    • 6.1 单继承中的虚函数表
  • 6.2 多继承中,子类新增虚函数存入那个虚表?
  • 6.3 总结
  • 七、菱形继承、菱形虚拟继承

前言

本篇博客基于VS2019X86环境下,后续关于多态原理相关验证都是基于vsX86环境,而虚表本质上是一个虚函数指针数组,在X86环境下VS编译器会在数组最后放一个unllptr!!

一、多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

二、多态的定义及实现

2.1 多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
但在继承中要构成多态还有两个条件:(后续会具体介绍其细节)

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

比如Student继承了Person。Person对象买票全价,Student对象买票半价:

class Person
{
public:virtual void BuyTicket()//被virtual修饰的类成员函数称为虚函数{cout << "买票全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket()//对基类虚函数重写{cout << "买票半价" << endl;}
};void Func(Person& people)
{people.BuyTicket();
}int main()
{Person p;Func(p);//涉及赋值兼容规则,后续会介绍。Student s;Func(s);return 0;
}

【运行结果】:
在这里插入图片描述

2. 2 虚函数

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

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

2.3 虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
tips:这里派生类的虚函数在不加virtual关键字时,我们认为也构成重写。原因在于,我们认为虚函数的重写继承了基类虚函数的接口,在派生类中重写实现

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*//*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person p;Student s;Func(p);Func(s);return 0;
}

2.3.1 虚函数重写的例外1:协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型可以不同,但必须是父子类关系的指针或者引用。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。

class A {};
class B : public A {};class Person 
{
public:virtual A* f() { return new A; }
};class Student : public Person 
{
public:virtual B* f() { return new B; }
};

2.3.2 虚函数重写的例外2:析构函数的重写(基类与派生类析构函数的名字不同)

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

【实例】:

class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;// delete p1编译器处理为: p1->destrutor() + operator delete(p1)delete p2;// delete p2编译器处理为: p2->destrutor() + operator delete(p2)return 0;
}

tips:大多数情况析构函数是不需要重写的。只有上述在上述情况下,如果普通析构函数,则p1、p2delete时调用的都是Person类的析构。但实际上p2指向的是Student类对象,原则上因该调用Student类的析构函数。所以为了正确调用析构函数,需要将析构实现为虚函数并重写。(记住上述情况,只有这种情况下才需要重写析构函数)

2.4 C++11 override 和 final

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

2.4.1 final

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

class Car
{
public:virtual void Drive() final {}
};class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};

2.4.2 override

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

class Car {
public:virtual void Drive() {}
};class Benz :public Car {
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

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

在这里插入图片描述

三、抽象类

3.1 概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象,一般用于接口的定义。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数间接强制派生类重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:virtual void Drive() = 0;//纯虚函数
};class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};void Test()
{Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
}

3.2 接口继承和实现继承

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

四、虚函数及虚函数表

4.1 虚函数表

在了解虚函数前,我们先来看看这段代码中类对象的大小(X86环境下)

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};int main()
{Base b1;cout << sizeof(b1) << endl;return 0;
}

【运行结果】:

在这里插入图片描述

我们发现b的大小竟然是8bytes。为什么呢?这就不得不提到虚函数表了。
原来Base对象中,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

在这里插入图片描述

4.2 虚函数重写覆盖虚表

我们再来看看下面这段代码:

// 1.我们增加一个派生类Derive去继
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和
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:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};int main()
{Base b;Derive d;return 0;
}

在这里插入图片描述
通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 派生类的虚表生成总结:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

4.3 虚函数存在哪的?虚表存在哪的?

后续所有代码都是基于vsX86环境,而虚表本质上是一个虚函数指针数组,VS编译器会在数组最后放一个unllptr!!
虚函数存在哪的?虚表存在哪的?比较简单的方法就是将虚函数、虚表以及每个存储区间地址都打印出来进行比较。

比如:

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int _a;
};void func()
{cout << "void func()" << endl;
}int main()
{Base b1;int a;int* a2 = new int;//const int a3 = 1;const char* s = "hello world";static int d4 = 0;printf("栈:%p\n", &a);printf("堆:%p\n", a2);printf("静态区:%p\n", &d4);//printf("代码段:%p\n", &a3);printf("代码段:%p\n", s);printf("虚表:%p\n", *(int*)(&b1));//在类域中的函数地址比较特殊,要加&printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", func);return 0;
}

【运行结果】:
在这里插入图片描述
我们发现虚表和代码段的存储在一起,虚函数和普通函数存储在一起,而普通函数又是存在代码段的。

总结:

  1. 虚函数和普通函数以及虚表都是存在代码段的!!!

五、多态原理

5.1 多态调用过程及原理

多态的原理到底是什么?为啥指向不同对象,能调用不同函数呢?
我们来看看开头的这段代码:

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

在这里插入图片描述
多态实现过程:

  1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
  2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

5.2 多态运行时确定?

多态运行时确定还是编译时确定?我们接着看:
在这里插入图片描述

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  
001940EC  cmp         esi,esp  
}
int main()
{
... 
// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调
用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址mike.BuyTicket();
00195182  lea         ecx,[mike]
00195185  call        Person::BuyTicket (01914F6h)  
... 
}
  1. 满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

5.3 动态绑定与静态绑定

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

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

6.1 单继承中的虚函数表

我们来看看的段代码:

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};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; }void func5() { cout << "Derive::func5" << endl; }
private:int b;
};class X :public Derive {
public:virtual void func3() { cout << "X::func3" << endl; }
};int main()
{Base b;Derive d;X x;return 0;
}

【监视窗口】:
在这里插入图片描述
在上述监视窗口中我们发现虚函数表中竟然只存有父类的虚函数以及相关虚函数的重写。也就是说新增的虚函数不会存储到虚表中!但真的如此吗?

真正原因在于:VS监视窗口是微软工程师处理过方便使用者观察的,但不一定是真实的!编译器的监视窗口故意隐藏了这些东西,也可以认为是他的一个小bug。要了解虚函数的地址是否一定会被放进类的虚函数,我们可以通过其真实内存,打印虚函数表来观察?

//将函数指针void (*)() 重定义为VFNC
typedef void (*VFUNC)();
//打印虚函数表
void printVFT(VFUNC* a)
{//由于VSX86环境下,编译器会在虚表结尾放一个nullptr,所以此处结束条件为a[i]!=0for (int i = 0; a[i] != 0; i++){printf("[%d]->%p ", i, a[i]);//调用a[i]中存储的函数VFUNC f = a[i];f();//(*f)()cout << endl;}
}int main()
{Base b;
// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
//      指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
//后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。printVFT((VFUNC*)(*((int*)(&b))));printf("----------------------------------\n");Derive d;printVFT((VFUNC*)(*(int*)(&d)));printf("----------------------------------\n");X x;printVFT((VFUNC*)(*(int*)(&x)));return 0;
}

【运行结果】:
在这里插入图片描述

总结:

1.我们发现实际上虚函数的地址一定会被放进类的虚函数表。

6.2 多继承中,子类新增虚函数存入那个虚表?

和6.1一样,我们只需要将虚表中存储的值都打印出来,即可确定这个问题答案!
【代码如下】:

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;
};//打印虚函数
typedef void(*VFUNC)();
void printVFT(VFUNC* a)
{for (int i = 0; a[i] != 0; i++){printf("[%d]->%p ", i, a[i]);VFUNC f = a[i];f();}cout << endl;
}int main()
{Derive d;printVFT((VFUNC*)(*(int*)&d));printf("-----------------------------\n");//由于d对象中存有两张虚表,要打印第2张虚表,需要将&d偏移指向Base2开头,有如下几种方式//printvft((vfunc*)(*(int*)((char*)&d+sizeof(base1))));Base2* ptr = &d;printVFT((VFUNC*)(*(int*)ptr));return 0;
}

【运行结果】:
在这里插入图片描述
结论:

  1. 多继承中,子类新增虚函数存入到第一张虚表中!!

6.3 总结

  1. 虚函数的地址一定会被放进类的虚函数表。
  2. 多继承中,子类新增虚函数存入到第一张虚表中。
  3. 同一个类型共用同一张虚表,不同类型则可能存在多张虚表。(可以将虚表打印出来,各位自行验证)

七、菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以博主就不介绍了。如果感兴趣,这里附上两篇相关文章:

  1. C++ 虚函数表解析
  2. C++ 对象的内存布局

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

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

相关文章

领域驱动模型之各层实体严格分层处理

为什么要分层处理呢&#xff1f; 在领域驱动模型中&#xff0c;分为应用层&#xff08;application&#xff09;、领域层&#xff08;domain&#xff09;、基础设施层&#xff08;infrastructure&#xff09;。各层只能处理和访问自己所属层的 entity 或者 dto 对象&#xff0…

基于ssm的理财通的设计与实现+jsp论文

摘 要 在如今社会上&#xff0c;关于信息上面的处理&#xff0c;没有任何一个企业或者个人会忽视&#xff0c;如何让信息急速传递&#xff0c;并且归档储存查询&#xff0c;采用之前的纸张记录模式已经不符合当前使用要求了。所以&#xff0c;对理财信息管理的提升&#xff0c…

针对大规模服务日志敏感信息的长效治理实践

文章目录 1 背景2 目标与措施3 实施3.1 脱敏工具类3.2 JSON脱敏3.3 APT自动脱敏3.3.1 本地缓存问题3.3.2 JDK序列化问题 3.4 弃用方案 4 规划5 总结 1 背景 近年来&#xff0c;国家采取了多项重要举措来加强个人数据保护&#xff0c;包括实施《中华人民共和国网络安全法》和《…

TypeError: Cannot read properties of undefined (reading ‘namespace‘)

项目场景&#xff1a; 背景&#xff1a; Java 项目中使用 activi 流程引擎&#xff0c; 创建一个带有排他网关的 申请审核流程&#xff0c; 创建之后 查看 流程图出现 如下所示的 错误信息。 前端页面 不显示 任何 流程图信息。 问题描述 问题&#xff1a; 例如&#xff1…

鸿蒙系列--属性动画

一、定义 当组件的通用属性发生改变时而产生的属性渐变效果 说明&#xff1a; 当组件的通用属性发生改变时&#xff0c;组件状态由初始状态逐渐变为结束状态的过程中&#xff0c;会创建多个连续的中间状态&#xff0c;逐帧播放后&#xff0c;就会形成动画 二、创建 给组件(如…

SCS模型(径流曲线法)概述

目录 1.介绍&#xff1a;2.计算公式&#xff1a;参考文献&#xff1a;小结&#xff1a; 1.介绍&#xff1a; SCS模型&#xff08;径流曲线法&#xff09;是由美国农业部水土保持局(Soil Conservation Service) 基于经验提出&#xff0c;最初用于预测在农业用地小型流域降雨所累…

【算法每日一练]-dfs (保姆级教程 篇9) #俄罗斯方块 #ABC Puzzle #lnc的工资

目录 今日知识点&#xff1a; 二维图形的状态压缩&#xff0c;存下所有的合法状态然后暴力遍历 dfs的优化剪枝 二项式定理 俄罗斯方块 ABC Puzzle lnc的工资 俄罗斯方块 322D 题意&#xff1a;在4*4方格中分别给出3个俄罗斯方块&#xff0c;问是否可以经过旋转&#xf…

C/C++动态内存管理

文章目录 前言1.C/C内存分布2.C语言中动态内存管理方式&#xff1a;malloc/calloc/realloc/free3.C内存管理方式3.1 new/delete操作内置类型3.2 new和delete操作自定义类型 4. operator new与operator delete函数4.1 operator new与operator delete函数 5. new和delete的实现原…

软件测试|MySQL BETWEEN AND:范围查询详解

简介 在MySQL数据库中&#xff0c;使用BETWEEN AND操作符可以进行范围查询&#xff0c;即根据某个字段的值在指定范围内进行检索数据。这个操作符非常有用&#xff0c;因为它可以让我们轻松地筛选出位于两个特定值之间的数据&#xff0c;而不需要使用复杂的条件语句。 BETWEE…

数据挖掘在制造业中的预测与优化应用

随着大数据时代的到来&#xff0c;数据挖掘技术在各行各业的应用日益广泛&#xff0c;尤其在制造业中&#xff0c;其对于提升生产效率、降低运营成本、优化供应链管理等方面发挥着不可替代的作用。本文将探讨数据挖掘在制造业中的预测与优化应用&#xff0c;通过深入剖析实际案…

如何选择最适合的采购付款 (P2P) 解决方案?

无论企业的业务流程执行得如何&#xff0c;流程中始终存在改进空间。更好的管理系统是获得更好结果的关键&#xff0c;尤其是当企业处于增长阶段时。强大的采购到付款&#xff08;P2P&#xff09;系统是加快采购流程&#xff0c;同时保持采购支出可见性的最有效方法之一。 什么…

尚硅谷vite+vue3 (尚医通)项目实战笔记

如何配置项目运行后自动打开&#xff1f; 在package.json 启动命令中添加 “dev”: “vite --open”, 如何配置src的别名&#xff1f; 利用node内置模块path设置一下在vite.config.ts 中的别名 import { defineConfig } from vite import vue from vitejs/plugin-vue import …

IPv6路由协议---IPv6动态路由(OSPFv3-4)

OSPFv3的链路状态通告LSA类型 链路状态通告是OSPFv3进行路由计算的关键依据,链路状态通告包含链路状态类型、链路状态ID、通告路由器三元组唯一地标识了一个LSA。 OSPFv3的LSA头仍然保持20字节,但是内容变化了。在LSA头中,OSPFv2的LS age、Advertising Router、LS Sequence…

Vue3技术解析(小册子)

随着 Vue 3 正式版本的发布&#xff0c;未来 Vue 3 将会成为前端的主流框架&#xff0c;这个毋庸置疑。Vue 3 在使用方面会兼容部分 Vue 2.x 的特性&#xff0c;比如 options API。 所以&#xff0c;究竟是要先学习 Vue 2 打好基础&#xff0c;还是直接学习 Vue 3 呢&#xff…

职场日常英语口语,成人英语培训学校,柯桥学英语推荐哪里

“玩手机”用英语怎么说&#xff1f;你的第一反应是不是&#xff1a;play the phone&#xff1f; 在英语中&#xff0c;play这个动词通常表示“玩耍、娱乐、操纵”等意思&#xff0c;而手机是一种工具&#xff0c;不是玩耍的对象。 换句话说&#xff0c;我们“玩手机”&#xf…

主食冻干哪款好?十大放心主食冻干名单推荐

作为养猫的人&#xff0c;我们都知道每天最担心的事情就是如何为心爱的猫咪选择一款高品质的猫粮。我们都希望为猫咪提供最好的营养&#xff0c;让它们健康快乐地成长。然而&#xff0c;近期的一些事件&#xff0c;如百利猫粮生虫和VE主食冻干掰开有虫&#xff0c;让我们不得不…

软件测试|Windows系统配置pytest+allure环境教程

前言 allure可以输出非常精美的测试报告&#xff0c;也可以和pytest进行完美结合&#xff0c;不仅可以渲染页面&#xff0c;还可以控制用例的执行。本文我们将介绍Windows系统中如何配置allure环境。 第一步&#xff1a;配置Java环境 因为allure的运行依赖于Java环境&#x…

Spring Security介绍

一、Spring Security&#xff1a; 1、简介&#xff1a;Spring Security 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架&#xff0c;提供了一套 Web 应用安全性的完整解决方案。一般来说&#xff0c;Web 应用的安全性包括用户认证&#xff08;A…

vue3引用类型和基础类型深度克隆

深度克隆失效的一个例子 import { cloneDeep } from "lodash"; import { ref } from "vue";const navArr ref(["recommend","hot","new", ]) const list1: any ref([]) const list2: any ref([]) const list3: any ref(…

如何设置电脑桌面提醒,电脑笔记软件哪个好?

对于大多数上班族来说&#xff0c;每天要完成的待办事项实在太多了&#xff0c;如果不能及时去处理&#xff0c;很容易因为各种因素导致忘记&#xff0c;从而给自己带来不少麻烦。所以&#xff0c;我们往往会借助一些提醒类的软件将各项任务逐一记录下来&#xff0c;然后设置上…