【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,就是纯虚函数。抽象类的意义:强制子类进行虚函数的重写,并且抽象类体现了接口继承关系。

总结

多态内容就讲到这里。

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

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

相关文章

pycharm 远程连接服务器并且debug, 支持torch.distributed.launch debug

未经允许&#xff0c;本文不得转载&#xff0c;vx&#xff1a;837007389 文章目录 step1&#xff1a;下载专业版本的pycharmstep2 配置自动同步文件夹&#xff0c;即远程的工程文件和本地同步2.1 Tools -> Deployment -> configuration2.2 设置同步文件夹2.3 同步服务器…

【深度学习】WaveMix: A Resource-efficient Neural Network for Image Analysis 论文

论文&#xff1a;https://arxiv.org/abs/2205.14375 代码&#xff1a;https://github.com/pranavphoenix/WaveMix 文章目录 ABSTRACTIntroductionBackground and Related WorksWaveMix Architectural FrameworkOverall architectureWaveMix block Experiments and ResultsTasks…

机器学习深度学习——Dropout

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——权重衰减 &#x1f4da;订阅专栏&#xff1a;机器学习&&深度学习 希望文章对你们有所帮助 Drop…

centos7安装nginx

一、下载安装包 方式一&#xff1a;官网下载 到nginx官网下载 然后上传到linux 服务器 方式二: wget 下载 wget http://nginx.org/download/nginx-1.22.0.tar.gz二、安装nginx 先安装相关依赖 yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel 创建…

使用vim-cmd工具给ESXi虚机定期打快照

VMware虚拟化 - 建设篇 第四章 使用vim-cmd工具给ESXi虚机定期打快照 VMware虚拟化 - 建设篇系列文章回顾使用vim-cmd工具给ESXi虚机定期打快照前言前提条件ESXi新增执行快照备份的sh脚本ESXi添加crond任务并使其生效ESXi指定部分虚拟机不执行定期快照(附加)虚拟机自定义属性…

ChatGPT有几个版本,哪个版本最强,如何选择适合自己的?

​ChatGPT就像内容生产界的瑞士军刀。它可以是数学导师、治疗师、职业顾问、编程助手&#xff0c;甚至是旅行指南。只要你知道如何让它做你想做的事&#xff0c;ChatGPT几乎可以提供你要的任何东西。 但重要的是&#xff0c;你知道哪个版本的ChatGPT最能满足你的需求吗&#x…

Windows 11 下 OpenFace 2.2.0 的安装

写在前面 最近需要做关于面部的东西&#xff0c;所以需要使用到OpenFace这个工具&#xff0c;本文仅用来记录本人安装过程以供后续复现&#xff0c;如果可以帮助到读者也是非常荣幸。 安装过程 不编译直接使用 这种方法可以直接从官方下载下来编译好的exe以及gui进行使用&a…

在 “小小容器” WasmEdge 里运行小小羊驼 llama 2

昨天&#xff0c;特斯拉前 AI 总监、OpenAI 联合创始人 Andrej Karpathy 开源了 llama2.c 。 只用 500 行纯 C 语言就能训练和推理 llama 2 模型的框架&#xff0c;没有任何繁杂的 python 依赖。这个项目一推出就受到大家的追捧&#xff0c;24 小时内 GitHub 收获 4000 颗星&am…

KY222 打印日期+KY111日期差值

一、KY222题目 二、代码 #include <climits> #include <iostream> using namespace std; class Date{public:Date(int year 1,int month 2,int day 3){_year year;_month month;_day day;}int GetDay(int year ,int month);void Define(int n);public:int _yea…

查看进程方式

目录 ps top uptime pstree ps 查看静态的进程统计信息 top 实时显示系统中各个进程的资源占用情况 第一行 top - 17:00:23 up 15 min, 1 user, load average: 1.05, 1.22, 0.98 17:00:23————当前时间 up 15 min————系统运行时间 1 user————当前登录用户数…

陪诊小程序软件|陪诊系统定制|医院陪诊小程序

开发一个陪诊小程序需要投入一定的费用&#xff0c;具体金额会因项目的复杂程度、功能需求和推广政策而有所差异在投入资金之前&#xff0c;建议进行市场调研和需求分析&#xff0c;制定出合理的预算&#xff0c;并选择专业的开发团队进行合作&#xff0c;那么开发陪诊小程序需…

2023大同首届信息技术产业峰会举行,共话数字经济新未来

7月28日&#xff0c;“聚势而强共领信创”2023大同首届信息技术产业峰会圆满举行。本次峰会由中共大同市委、大同市人民政府主办&#xff0c;中国高科技产业化研究会国际交流合作中心、山西省信创协会协办&#xff0c;中共大同市云冈区委、大同市云冈区人民政府、诚迈科技&…

密码学的一些常识

1&#xff0c;对称密码、公钥密码、消息认证、数字签名的对比 对称密码公钥密码发送者共享秘钥加密公钥加密接收者共享秘钥解密私钥解密秘钥配送问题存在不存在&#xff0c;但需要CA认证公钥机密性√√ 消息认证数字签名发送者共享秘钥计算MAC使用私钥对文本HASH值做签名接收者…

JavaScript学习 -- SM3算法基本原理

SM3算法是一种由国家密码管理局发布的哈希算法&#xff0c;被广泛用于数字签名和消息认证等应用中。在JavaScript中&#xff0c;我们可以使用第三方库来计算数据的SM3哈希值。本篇文章将介绍SM3算法的基本原理和相关技术&#xff0c;并提供一些实例来演示如何在JavaScript中使用…

DAY14_FilterListenerAjaxAxiosJsonfastjson综合案例-axios和html交互

目录 1 Filter1.1 Filter概述1.2 Filter快速入门1.2.1 开发步骤1.2.2 代码演示 1.3 Filter执行流程1.4 Filter拦截路径配置1.5 过滤器链1.5.1 概述1.5.2 代码演示1.5.3 问题 1.6 案例1.6.1 需求1.6.2 分析1.6.3 代码实现1.6.3.1 创建Filter1.6.3.2 编写逻辑代码1.6.3.3 测试并抛…

ERROR in unable to locate ‘***/public/**/*‘ glob

前提 自己搭了一个react项目的脚手架&#xff0c;npm包下载一切都很正常&#xff0c;启动的时候突然就报ERROR in unable to locate ***/public/**/* glob这个错误&#xff0c;根据百度分析了一下产生的原因&#xff1a;webpack配置文件中的CopyWebpackPlugin导致的 网上给出的…

【idea工具】idea工具,build的时候提示:程序包 com.xxx.xx不存在的错误

idea工具&#xff0c;build的时候提示:程序包 com.xxx.xx不存在的错误&#xff0c;如下图&#xff0c;折腾了好一会&#xff0c; 做了如下操作还是不行&#xff0c;idea工具编译的时候&#xff0c;还是提示 程序包不存在。 a. idea中&#xff0c;重新导入项目&#xff0c;也还…

从源码角度去深入分析关于Spring的异常处理ExceptionHandler的实现原理

ExceptionHandler的作用 ExceptionHandler是Spring框架提供的一个注解&#xff0c;用于处理应用程序中的异常。当应用程序中发生异常时&#xff0c;ExceptionHandler将优先地拦截异常并处理它&#xff0c;然后将处理结果返回到前端。该注解可用于类级别和方法级别&#xff0c;…

苍穹外卖心得与总结【对比瑞吉】【如何获得铁粉】

对于苍穹外卖项目&#xff0c;从学习课程加复习已经13天了。 对于一名已经学习过SSMLinuxRedis数据库的Java练习生来说&#xff0c;这个项目相对于之前学习的《瑞吉外卖》新增了很多功能和技术&#xff0c;是很值得练手和提升的课程&#xff0c;下面给出自己的一些见解。&#…

django channels实战(websocket底层原理和案例)

1、websocket相关 1.1、轮询 1.2、长轮询 1.3、websocket 1.3.1、websocket原理 1.3.2、django框架 asgi.py在django项目同名app目录下 1.3.3、聊天室 django代码总结 小结 1.3.4、群聊&#xff08;一&#xff09; 前端代码 后端代码 1.3.5、群聊&#xff08;二&#xff09…