[C++]多态

news/2024/2/28 11:18:20

[C++]多态

文章目录

      • [C++]多态
        • 一、什么是多态
          • 1.概念
          • 2.虚函数 virtual
            • (1)概念
            • (2)重写/覆盖
            • (3)特殊情况
            • (4)总结
          • 3.多态的定义和实现
          • 4.析构函数的重写
          • 5.final/override
          • 6.重载/隐藏/重写
        • 二、抽象类
          • 1.概念
          • 2.接口继承和实现继承
        • 三、原理
          • 1.虚函数表
          • 2.多态原理
          • 3.动态绑定和静态绑定
        • 四、单继承和多继承的虚表
          • 1.单继承的虚表
          • 2.多继承的虚表
          • 3.菱形继承和菱形虚拟继承的虚表
        • 五、几道经典题目

一、什么是多态

1.概念

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

比如领外卖优惠券,假如你是新用户那你领到的优惠券额度就比较大,假如你是老用户(长期使用该APP)那你领到的优惠券额度就比较小,或者是长期没有使用的人领到的额度也会比较大。

2.虚函数 virtual
(1)概念

virtual修饰的成员函数我们称为虚函数。

class Person
{
public:virtual void BuyFood() {cout << "0优惠减免" << endl;}
};

在这里插入图片描述

(2)重写/覆盖

在继承关系中,如果子类中有一个跟父类完全相同的虚函数 : 函数名、函数参数、函数返回值都相同 (简称三同),则称子类的虚函数重写或者覆盖了父类的虚函数。

class Person
{
public:virtual void BuyFood() {cout << "0优惠减免" << endl;}
};
class NewUser : public Person
{
public:virtual void BuyFood() { cout << "满20减15" << endl; }
};
class OldUser : public Person
{
public:virtual void BuyFood() { cout << "满20减5" << endl; }
};

在这里插入图片描述

(3)特殊情况
  • 子类的virtual可以省略,但是父类的不能省略。

虚函数的继承是接口继承,子类和父类的虚函数接口是完全相同的,子类对虚函数进行重写,仅仅重写了函数实现并不是改变函数接口。所以子类不加virtual的函数类型和父类也是一样的(同为虚函数类型)。
在这里插入图片描述

虽然可以不用加virtual,但是我们建议都加上,方便我们理解代码。

  • 协作: 子类虚函数和父类虚函数的返回值可以不同,但必须是一个子类类型或父类类型的指针或者引用。

当前父子类:

class Person
{
public:virtual Person* BuyFood() { cout << "0优惠减免" << endl; return this; }
};
class NewUser : public Person
{
public:virtual NewUser* BuyFood() { cout << "满20减15" << endl; return this; }
};
class OldUser : public Person
{
public:virtual OldUser* BuyFood() { cout << "满20减5" << endl; return this; }
};

在这里插入图片描述

其他父子类:

class A{};
class B : public A{};
class Person
{
public:virtual A* BuyFood() { cout << "0优惠减免" << endl; return nullptr; }
};
class NewUser : public Person
{
public:virtual B* BuyFood() { cout << "满20减15" << endl; return nullptr; }
};
class OldUser : public Person
{
public:virtual B* BuyFood() { cout << "满20减5" << endl; return nullptr; }
};

在这里插入图片描述

(4)总结

1.如果父类和子类的函数不满足虚函数和三同(函数名、参数类型、返回值,协同除外),那一般他们仅仅构成隐藏(函数名相同)。

2.构成重写的条件:虚函数+三同(或特殊情况)。

3.多态的定义和实现
class Person
{
public:virtual void BuyFood() { cout << "0优惠减免" << endl; }
};
class NewUser : public Person
{
public:virtual void BuyFood() { cout << "满20减15" << endl; }
};
class OldUser : public Person
{
public:virtual void BuyFood() { cout << "满20减5" << endl;  }
};
void Func(Person& fp)
{fp.BuyFood();
}

在这里插入图片描述
在这里插入图片描述

我们通过传不同的类的对象参数调用Func函数,产生了不同的效果。这就是我们之前所说的不同对象进行同一种行为产生不同的状态,我们称它为多态。

  • 多态调用

刚刚我们的操作就是我们所说的多态调用,多态调用必须满足的前提是:继承+虚函数重写、通过父类的指针/引用调用虚函数。

在这里插入图片描述

  • 普通调用

与多态调用相反的调用方式,我们称为普通调用,即不满足多态调用的前提。

void Func(Person fp)
{fp.BuyFood();
}int main()
{Person p;NewUser nu;OldUser ou;Func(p);Func(nu);Func(ou);return 0;
}

在这里插入图片描述

仅仅是去掉了引用,传入后fp的类型都是Person,所以都调用的Person对象的BuyFood()函数

不满足多态调用的通过父类的指针/引用调用虚函数条件。

4.析构函数的重写

一般来说正常的析构是没有问题的,但是有一种特殊情况,如果我们不把析构函数进行重写,那就会造成内存泄漏。

class Person
{
public:~Person() { cout << "Person::delete" << endl; }
};
class NewUser : public Person
{
public:~NewUser() { cout << "NewUser::delete" << endl; }
};
class OldUser : public Person
{
public:~OldUser() { cout << "OldUser::delete" << endl; }
};int main()
{Person* p1 = new Person;Person* p2 = new NewUser;Person* p3 = new OldUser;delete p1;delete p2;delete p3;return 0;
}

在这里插入图片描述

delete会默认去调用类的析构函数operator delete(),由于p1,p2,p3都是Person类对象,所以都去调用了Person类的析构函数。这就是普通调用,什么对象调用什么函数。

Person指针不仅指向父类对象,还指向子类对象。但是由于子类没有多态实现析构,所以都去调用了父类Person的析构函数。

在我们实现多态调用(子类虚函数重写 + 父类的指针/引用调用虚函数)后就不会出现上述问题。

class Person
{
public:virtual ~Person() { cout << "Person::delete" << endl; }
};
class NewUser : public Person
{
public:virtual ~NewUser() { cout << "NewUser::delete" << endl; }
};
class OldUser : public Person
{
public:virtual ~OldUser() { cout << "OldUser::delete" << endl; }
};int main()
{Person* p1 = new Person;Person* p2 = new NewUser;Person* p3 = new OldUser;delete p1;cout << endl;delete p2;cout << endl;delete p3;cout << endl;return 0;
}

在这里插入图片描述

虽然析构函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

所以实现父类的时候,我们可以每次都给析构函数加上virtual。

5.final/override

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

在这里插入图片描述

2.override: 检查子类虚函数是否重写了父类某个虚函数,如果没有重写编译报错。

在这里插入图片描述

6.重载/隐藏/重写
  • 重载 : 两个函数要在同一个作用域之中,函数名相同,参数不同,与返回值无关!
  • 重定义(隐藏):两个函数分别在子类和父类的作用域,函数名相同,如果两个基类和派生类的同名函数不构成重写那就是重定义!
  • 重写(覆盖) : 两个函数分别在子类和父类的作用域,虚函数+三同(函数名、参数、返回值),除了两种特殊情况(协作/省略virtual)。

二、抽象类

1.概念

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

特点:

  • 抽象类不能实例化出对象
  • 如果一个子类的父类是抽象类,该子类也不能实例化出对象(除非重写虚函数)

纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Person//纯虚类
{
public:virtual void BuyFood() = 0;//纯虚函数
};
class NewUser : public Person
{
public:virtual void BuyFood()  { cout << "--- 满20减15 ---" << endl; }
};
class OldUser : public Person
{
public:virtual void BuyFood()  { cout << "--- 满20减5 ---" << endl; }
};int main()
{Person* pnu = new NewUser;pnu->BuyFood();Person* pou = new OldUser;pou->BuyFood();return 0;
}

在这里插入图片描述

纯虚函数强制我们实现多态。

override 和 纯虚函数的区别

  • override 是检查重写,没重写直接编译报错;
  • 纯虚函数是强制我们重写,因为不重写就不能实例化出对象,但是不会不实例化对象也可以不重写。
2.接口继承和实现继承
  • 普通函数的继承是一种实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实现。

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

三、原理

1.虚函数表

我们每次实现多态后,进入调试窗口发现多了一个_vfptr。

这个东西叫做虚函数表指针,它指向一个虚函数表(又称虚表,与之前的虚基表不是同一个),其中存放的是虚函数的地址。(我们可以看到普通函数func3() 没有进到虚函数表中)

class A
{
public:virtual void func1(){cout << "A::func1()" << endl;}virtual void func2() {cout << "A::func2" << endl;}void func3() {cout << "A::func3" << endl;}
protected:int _a;
};
class B : public A
{
public:virtual void func1(){cout << "B::func1()" << endl;}
protected:int _b;
};int main()
{A a;B b;return 0;
}

在这里插入图片描述

我们再构造一个B对象继承A对象再重写func1(),来看看B对象虚函数表中是什么样的。

在这里插入图片描述

我们可以得到以下结论:

1.子类对象 b 由两部分构成(父类成员+本身);同时我们发现,子类对象中也有虚表指针,并且该虚表指针是存在于子类对象中父类那一部分中的,说明子类对象的虚基表是从父类继承/拷贝过来的

2.我们发现,虽然子类并没有对 func2 进行重写,但是虚表中仍然有 func2 函数的地址,这是因为子类会继承父类的 func2;

3.同时,还可以发现子类虚表指针指向的虚表和父类虚表指针指向的虚表的内容是不同的,并且不同的那部分内容刚好是子类中进行虚函数重写的那部分内容
所以子类虚表是通过拷贝父类虚表,然后将子类进行重写得到的新的虚函数地址覆盖掉虚表中该需函数原来的地址得到的。这就是说为什么要把重写叫做覆盖。

4.其实func3()也被继承了下来,只是它并不是虚函数,没有进虚表而已。

那虚表的位置到底是存在哪里呢?

我们通过一段代码进行推理一下。

int main()
{int a;cout << "栈 " << &a << endl;int* p = new int;cout << "堆 " << p << endl;const char* str = "hello world";cout << "代码段/常量区 " << (void*)str << endl;static int b;cout << "静态区/数据段 " << &b << endl;A aa;cout << "虚表 " << (void*)*((int*)&aa) << endl;return 0;
}

在这里插入图片描述

不用我多说,大家也能猜出在哪了吧。(代码段/常量区)

通过对比我们发现,虚表和代码段的地址十分接近,这说明它就在代码段中。

为什么这么写呢?因为32位平台下指针的大小是4字节,所以我们用int*来强转取出vfptr虚表的前4字节,再进行解引用即可获得vfptr的地址。由于得到的是整形,不转换为(void*)不容易和其他地址对比,所以我们再强转为void* 类型(指针类型)。

  • 由于虚表存储在代码段中,所以同一类型的虚表是共享的
  • 子类的虚表是先拷贝父类虚表,然后进行覆盖,覆盖完毕后存储到代码段中。
2.多态原理
class A
{
public:virtual void func1(){cout << "A::func1()" << endl;}virtual void func2(){cout << "A::func2" << endl;}void func3(){cout << "A::func3()" << endl;}
protected:int _a;
};
class B : public A
{
public:virtual void func1(){cout << "B::func1()" << endl;}void func3(){cout << "B::func3()" << endl;}
protected:int _b;
};int main()
{A aa;B bb;A* p = &aa;p->func1();p = &bb;p->func1();p = &aa;p->func3();p = &bb;p->func3();return 0;
}

在这里插入图片描述

  • 调用func1()时,由于func1是一个多态调用(虚函数+三同:函数名、参数、返回值),此时与p指针指向的对象有关。

指向父类aa时,调用aa::func1()。指向子类bb时,调用bb::func1()。

  • 调用func3()时,func3是一个普通函数,此时调用只与指针类型有关。

指针类型都为A*,无论指向什么对象类型都会去调用父类的func3()。

我们通过反汇编再进行理解。
多态调用
在这里插入图片描述
普通调用
在这里插入图片描述

3.动态绑定和静态绑定
  • 静态绑定(又称编译时绑定): 它在程序编译时就确定程序的行为,也叫静态多态。例如,函数重载就是静态绑定。

  • 动态绑定(又称运行时绑定): 它在程序运行时,根据具体拿到的类确定程序的具体行为,也叫动态多态。

  • 反汇编中就很好理解这两种绑定,一个是运行直接call函数的地址,另一个运行等取到地址后再去那个地址进行调用

为什么父类对象不能实现多态,且必须是父类的指针/引用?

因为子类对象赋值给父类对象时会切片赋值,父类对象中不会有子类的虚表指针,无法进行动态绑定(也就是无法取到该函数地址)。父类只会去调用已有的虚表指针(也就是自己的虚表指针),从而实现静态绑定。

四、单继承和多继承的虚表

1.单继承的虚表
class A
{
public:virtual void func1() { cout << "A::func1()" << endl; }virtual void func2() { cout << "A::func2" << endl; }
protected:int _a;
};
class B : public A
{
public:virtual void func1() { cout << "B::func1()" << endl; }virtual void func2() { cout << "B::func2()" << endl; }virtual void func3() { cout << "B::func3()" << endl; }virtual void func4() { cout << "B::func4()" << endl; }virtual void func5() { cout << "B::func5()" << endl; }
protected:int _b;
};int main()
{A aa;B bb;return 0;
}

在这里插入图片描述

明明对象B bb中,有四个虚函数而调试窗口中只看到了2个,而不是4个,这是因为编译器(VS2019)对这里进行了优化(继承来的A中只有两个虚函数,这里就不能显示其他的虚函数了),隐藏了另外两个虚函数。

其实他们两个一直是存在这个虚表指针中的,我们写一个打印代码验证一下。

//将返回值为void,参数为void的函数指针重命名为 VFPTR
typedef void(*VFPTR)();void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址->" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" [%d]:%p,->", i, vTable[i]);vTable[i]();}cout << endl;
}

在这里插入图片描述

  • 函数指针的 typedef 与其他类型的 typedef 不同,重命名的名字要放在括号里面作为函数名 (函数名就是函数指针);

  • 在其他平台下虚表最后一个元素不一定是 nullptr(Linux下必须指明size;使用函数指针调用函数是不必解引用,因为函数名就是函数指针(可以用[]访问内容)。

2.多继承的虚表
class A
{
public:virtual void func1() { cout << "A::func1()" << endl; }virtual void func2() { cout << "A::func2()" << endl; }protected:int _a;
};
class B : public A
{
public:virtual void func1() { cout << "B::func1()" << endl; }virtual void func2() { cout << "B::func2()" << endl; }//virtual void func3() { cout << "B::func3()" << endl; }//virtual void func4() { cout << "B::func4()" << endl; }//virtual void func5() { cout << "B::func5()" << endl; }protected:int _b;
};
class C : public A, public B
{
public:virtual void func1() { cout << "C::func1()" << endl; }virtual void func3() { cout << "C::func3()" << endl; }
};//将返回值为void,参数为void的函数指针重命名为 VFPTR
typedef void(*VFPTR)();void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址->" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" [%d]:%p,->", i, vTable[i]);vTable[i]();}cout << endl;
}int main()
{A aa;PrintVTable((VFPTR*)*((int*)&aa));B bb;PrintVTable((VFPTR*)*((int*)&bb));C cc;PrintVTable((VFPTR*)*((int*)&cc));B* pcc = &cc;PrintVTable((VFPTR*)*((int*)pcc));return 0;
}

在这里插入图片描述

  • 在C类中,有两个继承下来的虚表指针(分别继承A/B), 其中C类只是重写了func1()。

  • 所以在C类继承的A的虚表指针中有重写的func1()和继承来的func2(),还有一个自身的func3()。(子类将自身特有的虚函数会存到最先继承的父类虚表的最后)

  • 第二次打印为了访问C类中B类的虚表指针,所以用B类指针切片C取到B类虚表指针开始的位置(4字节)即可访问第二个虚表(B类虚表)。

3.菱形继承和菱形虚拟继承的虚表

他们没有太多的实际意义,不建议设计出来。

如果很好奇,大家可以自行查阅相关专业的资料。

五、几道经典题目

1.下列代码类的初始化顺序是什么?

#include<iostream>
using namespace std;
class A {
public:A(char* s) { cout << s << endl; }~A() {}
};
class B :virtual public A
{
public:B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:D(char* s1, char* s2, char* s3, char* s4) :C(s1, s3), B(s1, s2),  A(s1){cout << s4 << endl;}
};
int main() {D* p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}

ABCD为菱形虚拟继承关系,其中A只有一份,所以A只会调用一次构造函数,且变量初始化的顺序与变量在初始化列表出现的顺序无关,只与变量的声明顺序有关(即先被继承,先完成初始化)。

答案:ABCD

我们略微修改代码,这下初始化的顺序又是什么?

#include<iostream>
using namespace std;
class A {
public:A(char* s) { cout << s << endl; }~A() {}
};
class B : public A
{
public:B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C : public A
{
public:C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:D(char* s1, char* s2, char* s3, char* s4) :C(s1, s3), B(s1, s2),  A(s1){cout << s4 << endl;}
};
int main() {D* p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}

A B C D 为菱形继承,所以 D 对象中的属于 B 和 属于 C 的部分各有一个 A,即 A 应该调用两次构造;同时,变量初始化 的顺序与变量在初始化列表出现的顺序无关,而与变量的声明顺序有关,对应到继承关系中,就是先被继承的先完成初始化。

所以,输出顺序是:ABACD

2.下列说法正确的是()

A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

class Base1 { public:  int _b1; };
class Base2 { public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main() {Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

子类对象模型中先被继承的父类,其父类模型会被放在子类对象模型的上方

  • 栈的使用规则是先使用高地址,再使用低地址。

  • 对象模型内部是先使用低地址,再使用高地址,即先继承的父类其对象模型在子类模型的低地址处。

即为:p1 == p3 != p2

在这里插入图片描述

我们再略微修改代码:

class Base1 { public:  int _b1; };
class Base2 { public:  int _b2; };
class Derive : public Base2, public Base1 { public: int _d; };
int main() {Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

子类对象模型中先被继承的父类,其父类模型会被放在子类对象模型的上方;

对象模型内部是先使用低地址,再使用高地址,即先继承的父类其对象模型在子类模型的低地址处。

即为:p1 > p2 == p3

在这里插入图片描述

3.以下程序输出结果是什么()

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错

F: 以上都不正确

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;
}
  • func()是多态调用,一种接口继承。多态调用看调用对象指向的类型,this指向B类型,所以调用B重写后的func()。
  • 接口继承,只是对{}中内容进行重写,而缺省值还是用的A中继承下来的val = 1。(test()函数的指针this是A*类型,访问时p->this->func())。

http://www.ppmy.cn/news/35777.html

相关文章

前端基础HTML、CSS--4(CSS-1)

目标&#xff1a; 能够说出什么是CSS 能够使用CSS基础选择器 能够设置字体样式 能够说出CSS的三种引入方式 能够使用Chrome调试工具样式 目录&#xff1a; CSS简介 CSS基础选择器 CSS字体属性 CSS文本属性 CSS的引入方式 综合案例 Chrome调试工具 1.CSS简介 CSS…

RocketMq如何保证消息的顺序性

文章目录消息的有序性生产者保证消息的有序性消费者端保证消息消费的有序性消息的有序性 消息有序指的是一类消息消费时&#xff0c;能按照发送的顺序来消费。例如&#xff1a;一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义…

Linux常用命令汇总

一、文件和文件夹操作 1、进入某一文件夹&#xff1a; cd xxx/yyy/简便记忆&#xff1a;改变目录&#xff0c;change directory&#xff0c;cd 2、复制文件到另一个文件&#xff1a; cp xxx/xxx.yyy ddd/rrr.zzz简便记忆&#xff1a;复制&#xff0c;copy&#xff0c;cp 第…

51单片机(LCD1602液晶屏)

一、LCD1602模块 LCD1602&#xff08;Liquid Crystal Display&#xff09;是一种工业字符型液晶&#xff0c;能够同时显示 1602 即 32 字符(16列两 行) 1、引脚说明 第 1 脚: VSS 为电源地 第 2 脚: VDD 接 5V 正电源 第 3 脚: VL 为液晶显示器对比度调整端,接正电源时对比度最…

MySQL、HBase、ElasticSearch三者对比

1、概念介绍 MySQL&#xff1a;关系型数据库&#xff0c;主要面向OLTP&#xff0c;支持事务&#xff0c;支持二级索引&#xff0c;支持sql&#xff0c;支持主从、Group Replication架构模型&#xff08;本文全部以Innodb为例&#xff0c;不涉及别的存储引擎&#xff09;。 HBas…

“独裁者”何小鹏,再造小鹏汽车

文丨智能相对论 作者丨沈浪 如果没有何小鹏&#xff0c;小鹏汽车将失去灵魂。 2014年&#xff0c;夏珩、何涛等人在广州组建小鹏汽车&#xff08;当时还叫“橙子汽车”&#xff09;&#xff0c;何小鹏还只是股权投资人。 夏珩、何涛原任职于广汽&#xff0c;负责新能源汽车…

VAE的NLP理解(离散,没有序列性)

Variational Auto-Encoder 变分自编码器 严格来说&#xff0c;VAE 实现的是“文本重建”&#xff0c;它虽然也包含“编码器”和“解码器”两个部分&#xff0c;但和 NLP 中的 encoder-decoder 架构还是有所区别。最大的差异在于&#xff0c;VAE 发源于 CV&#xff0c;它天然地不…

uniapp项目打包apk相关(androidStudio,Hbuildx,dCloud)

1、先注册和登陆dCloud平台&#xff0c;管理应用信息。 需要准备的参数(3个) APP_ID&#xff08;如&#xff1a;__UNI__123ABCD&#xff09; 包名&#xff08;如&#xff1a;com.hx.mhoa&#xff09; 应用签名&#xff08;应用sha1&#xff0c;应用md5&#xff0c;应用sha256&…

mysql高可用集群搭建

文章目录卸载系统自带的MariaDB开放访问端口安装依赖安装前请确定 openssl版本官网下载PXC地址找到对应的版本进行下载、解压下载解压建立软连接建立用户组增加用户建立工作目录目录授权每一台都要配置安装xtrabackup 2.4.20 CentOS7 percona-xtrabackup-24-2.4.20-1.el7.x86_6…

pytorch项目实战之实时人脸属性检测系统

简介 本项目采用CelebA人脸属性数据集训练人脸属性分类模型&#xff0c;使用mediapipe进行人脸检测&#xff0c;使用onnxruntime进行模型的推理&#xff0c;最终在intel的奔腾cpu上实现30-100帧完整的实时人脸属性识别系统。 ps:本来是打算写成付费专栏的&#xff0c;毕竟这是…

数据结构学习之路-队列

队列&#xff08;Queue&#xff09;定义队列的接口设计&#xff08;使用双向链表&#xff09;用栈实现队列的接口设计双端队列&#xff08;Deque&#xff09;循环队列&#xff08;Circle Queue&#xff09;循环双端队列&#xff08;Ciecle Deque&#xff09;定义 队列是一种特…

spark第四章:SparkSQL基本操作

系列文章目录 spark第一章&#xff1a;环境安装 spark第二章&#xff1a;sparkcore实例 spark第三章&#xff1a;工程化代码 spark第四章&#xff1a;SparkSQL基本操作 文章目录系列文章目录[TOC](文章目录)前言一、添加pom二、常用操作1.类型转换2.连接mysql3.UDF函数4.UDAF函…

ip-guard是否支持设置软件安装控制白名单?

如何禁止在上班时间使用跟工作无关的软件? 可以设置应用程序策略,通过设置在特定时间段禁用指定应用程序来实现这一的效果(自定义时间的最小单位为半小时)。 比如工作时间是上午9:00-12:00;13:30-17:30,在此期间要禁止游戏相关的应用程序。则设置策略如下: 时间:选择工作…

后端Springboot框架搭建APi接口开发(第二章)

上一章我讲述了如何使用Mybatis操作数据库。这一章我讲述如何利用Sptring框架搭建API接口 第一节&#xff1a;封装SqlSessionFactory工具类 在API操作数据库大量调用SqlSessionFactory&#xff0c;因此应将SqlSessionFactory封装成工具类供方法随时调用 在文件结构中的util文…

编译安装redis实验文档

一、什么是Redis Redis是一种主流的非关系型数据库&#xff0c;是完全开源的&#xff0c;遵守 BSD 协议&#xff0c;是一个高性能的 key-value 数据库。它支持数据的持久化&#xff0c;可以将内存中的数据保存在磁盘中&#xff0c;重启的时候可以再次加载进行使用。Redis不仅仅…

node_fs文件系统模块

1. 写入内容到文本文件里 Node.js 文件系统&#xff08;fs 模块&#xff09;模块中的方法均有异步和同步版本&#xff0c;例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。 异步的方法函数最后一个参数为回调函数&#xff0c;回调函数的第一个参数包…

VS Code 将推出更多 AI 功能给 Java 开发者

大家好&#xff0c;欢迎来到我们的二月更新&#xff01;我们将为您带来与 JUnit 5 并行测试相关的新功能以及用于 Spring Boot Dashboard 的过滤功能。另外&#xff0c;OpenAI 和 ChatGPT 是最近的热点&#xff0c;所以在 GitHub Copilot 方面也有一些令人激动的消息&#xff0…

Vue3通透教程【八】获取DOM、操作组件

文章目录 🌟 写在前面🌟 Vue2 ref 的使用🌟 Vue3获取DOM🌟 Vue3操作组件🌟 写在最后🌟 写在前面 专栏介绍: 凉哥作为 Vue 的忠实 粉丝输出过大量的 Vue 文章,应粉丝要求开始更新 Vue3 的相关技术文章,Vue 框架目前的地位大家应该都晓得,所谓三大框架使用人数最…

「解析」牛客网-华为机考企业真题 21-40

又是一年春招时&#xff0c;有幸收到华为自动驾驶算法岗&#xff0c;之前刷题不多&#xff0c;在此汇总下牛客网的真题&#xff0c;主要采用Python编写&#xff0c;个人觉得语言只是实现工具而已&#xff0c;并不是很关键&#xff0c;Python简洁易懂&#xff0c;更加适合算法工…

GPS时间序列分析---剔除跳跃点,拟合时间序列

通常利用GPS时间序列进行数据分析时&#xff0c;会遇到大地震的发生&#xff0c;这个时候会导致GPS的观测结果出现很大的跳跃值&#xff0c;这对后续的数据处理和分析带来了困难。这里分享一个最近了解的&#xff0c;可以用于处理这一问题的工具包---TSAnalyzer。下面主要介绍该…
最新文章