【C++私房菜】面向对象中的多态

文章目录

  • 一、多态
  • 二、对象的静态类型和动态类型
  • 三、虚函数和纯虚函数
    • 1、虚函数
    • 2、虚析构函数
    • 3、抽象基类和纯虚函数
    • 4、多态的原理
  • 四、重载、覆盖(重写)、隐藏(重定义)的对比


一、多态

OOP的核心思想是多态性(polymorphism)。多态性这个词源自希腊语,其含义是“多种形式”。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”而无须在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。
当我们使用基类的引用或指针调用基类中定义的一个函数时,我们并不知道该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也可能是一个派生类的对象。如果该函数是虚函数,则直到运行时才会决定到底执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型。
另一方面,对非虚函数的调用在编译时进行绑定。类似的,通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。对象的类型是确定不变的,我们无论如何都不可能令对象的动态类型与静态类型不一致。因此,通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本上。

❕ 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

二、对象的静态类型和动态类型

⚠️在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。

当我们使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型(static type)与该表达式表示对象的动态类型(dynamic type)区分开来。表达式的静态类型是在编译时确定的,它是变量声明时的类型或表达式生成的类型:动态类型则是变量或表达式表示的内存中的对象类型,在运行时才可知。

❕ 如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型保持一致。

因此我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定,我们直到运行时才知道到底调用了哪个版本的虚函数,所以所有的虚函数都必须有定义。但是我们必须为每一个虚函数都提供定义,而不管它是否被用到了,这是因为连编译器也无法确定到底会使用哪个函数。

派生类可以继承其基类的成员,然而当遇到与类型相关的操作时,派生类必须对其重新定义。换句话说,派生类需要对这些操作提供自己的新定义以覆盖(override)从基类继承而来的旧定义。我们来看如下代码:

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(){B bb;B* p = &bb;p->test();		//B->1p->func();		//B->0bb.func();		//B->0A& a = bb;a.test();		//B->1a.func();		//B->1return 0;
}

当我们使用指向bb的指针p调用 test函数时,test函数中隐含的传入了 A* this因此在此处我们是多态调用。派生类用自己的新定义覆盖了从基类继承而来的旧定义,但是调用时仍使用的是基类的声明。下面 A& a 到底调用哪个版本的 func完全依赖于运行时绑定到它上面的动态类型。

虚函数与其他函数一样,虚函数也可以用有默认实参,如果某次虚函数调用使用了默认实参,则该实参指由本次调用的静态类型决定。

换句话说,如果我们通过基类的引用或指针调用函数。则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。如果派生类函数依赖不同的实参,则程序结果将与我们的预期不符。

class A{ public: void test(float a) { cout << a; } }; class B :public A{ public: void test(int b){ cout << b; } }; 
int main() { A *a = new A; B *b = new B; a = b; a->test(1.1);   //输出1.1
}

📔 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。

调用的虚函数在运行时才会被解析,当某个虚函数通过指针或引用被调用时,编译器产生的代码直到运行时才会确定应该调用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。

那么如果我们使用普通类型(非指针非引用)的表达式调用虚函数,在编译时就会将调用的版本确定下来。

那么我们就产生疑问,inline函数可以是虚函数吗?答案当然是可以,当我们使用普通类型调用虚函数时,具有inline属性。如果是多态调用,这个函数酒不再是inline,因为虚函数要放进虚表中去。

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

三、虚函数和纯虚函数

1、虚函数

在C++语言中,基类必须将它的两种成员函数区分开来,一种是基类希望其派生类进行覆盖的函数:另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通常将其定义未虚函数(virtual)。当我们使用指针或引用调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。

基类通过在其成员函数的声明语句之前加上关键字 virtual使得该函数执行动态绑定。任何构造函数之外的非静态函数都可以是虚函数。一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。

同样的,派生类中虚函数的返回类型也必须与基类函数匹配。该规则存在两个例外。

  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. 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统-处理成destructor。(析构函数的重写,我们将在后文再进行叙述)

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

⚠️关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。 而且 virtual不能与static同时使用。静态成员一定是不被包含在对象中的静态成员属于整个类,不属于任何对象,所以在整体体系中只有一份。

⚠️静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数。

当然我们也可以会批虚函数的机制,在某些情况下我们可能希望对虚函数的调用不进行动态绑定,而是强迫其执行虚函数的某个特定版本。我们可以使用作用域运算符实现此目的:

class Base {
public:virtual void func() {cout << "Base::func" << endl;}
};
class Derived :public Base {
public:virtual void func() {cout << "Derived::func" << endl;}
};
int main(){Derived d;Base* pb = &d;pb->func();			//"Base::func" pb->Base::func();	//"Derived::func" return 0;
}

运行时的多态性可通过和虚函数实现。不可通过模板实现,因为模板属于编译时多态。编译时的多态性可通过函数重载实现。

class A {
public:virtual void f() { cout << "A::f()" << endl; }
};class B : public A {
private:virtual void f() {cout << "B::f()" << endl;}
};
int main()
{A* pa = (A*)new B;	//或 A* pa = new B;均合法pa->f();			//B::f()
}

此段代码编译正确,虽然子类函数为私有,但是多态仅仅是用子类函数的地址覆盖虚表,最终调用的位置不变,只是执行函数发生变化。不强制也可以直接赋值,因为赋值兼容规则作出了保证。


2、虚析构函数

继承关系中对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。

当我们delete一个动态分配的对象的指针时将执行析构函数。如果该指针指向继承体系中的某个类型,则有可能出现指针的静态类型与被删除对象的动态类型不符的情况。

例如,QutoeBulk_quote 的父类。我们 delete一个 Quote*类型的指针,则该指针有可能实际指向了一个Bulk_quote 类型的对象。如果这样的话,编译器就必须清楚它应该执行的是Bu1k_quote的析构函数。和其他函数一样,我们通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本:

class Quote {
public:如果我们删除的是一个指向派生类对象的基类指针,则需要虚析构函数virtual ~Quote() = default;		动态绑定析构函数
};

和其他虚函数一样,析构函数的虚属性也会被继承。因此,无论Quote的派生类使用合成的析构函数还是定义自己的析构函数,都将是虚析构函数。只要基类的析构函数是虚函数,就能确保当我们 delete基类指针时将运行正确的析构函数版本:

Quote* itemP = new Quote;	//静态类型与动态类型一致
delete itemP;				//调用 Quote的析构函数
itemP = new Bulk_quote;		//静态类型与动态类型不一致
delete itemP				//调用Bulk guote的析构函数

⚠️如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。

析构函数需要构成重写,那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

如前所述,在析构函数体执行完成后,对象的成员会被隐式销毁。类似的,对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源:

class D :public Base {
public://Base::~Base被自动调用执行~D() {/*该处由用户定义清除派生类成员的操作*/ }
};

对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推,沿着继承体系的反方向直至最后。

那么我们在构造函数和析构函数中调用虚函数会发生什么呢?

如我们所知,派生类对象的基类部分将首先被构建。当执行基类的构造函数时,该对象的派生类部分是未被初始化的状态。类似的,销毁派生类对象的次序正好相反,因此当执行基类的析构函数时,派生类部分已经被销毁掉了。由此可知,当我们执行上述基类成员的时候,该对象处于未完成的状态。
为了能够正确地处理这种未完成状态,编译器认为对象的类型在构造或析构的过程中仿佛发生了改变一样。也就是说,当我们构建一个对象时,需要把对象的类和构造函数的类看作是同一个:对虚函数的调用绑定正好符合这种把对象的类和构造函数的类看成同一个的要求;对于析构函数也是同样的道理。上述的绑定不但对直接调用虚函数有效,对间接调用也是有效的,这里的间接调用是指通过构造函数(或析构函数)调用另一个函数。为了理解上述行为,不妨考虑当基类构造函数调用虚函数的派生类版本时会发生什么情况。这个虚函数可能会访问派生类的成员,毕竟,如果它不需要访问派生类成员的话,则派生类直接使用基类的虚函数版本就可以了。然而,当执行基类构造函数时,它要用到的派生类成员尚未初始化,如果我们允许这样的访问,则程序很可能会崩溃。

在此我们看一道选择题:

假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则()

A.A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址

B.A类对象和B类对象前4个字节存储的都是虚表的地址

C.A类对象和B类对象前4个字节存储的虚表地址相同

D.A类和B类中的内容完全一样,但是A类和B类使用的不是同一张虚表

此题选 B。为什么呢?

A.父类对象和子类对象的前4字节都是虚表地址。
B.A类对象和B类对象前4个字节存储的都是虚表的地址,只是各自指向各自的虚表。
C.不相同,各自有各自的虚表。
D.A类和B类不是同一类内容不同。

如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对于的虚函数版本。

⚠️派生类对象销毁时,先调用基类析构函数,后调用子类析构函数!


3、抽象基类和纯虚函数

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。含有纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

抽象类负责定义接口,而后续的其他类可以覆盖该接口。

class LPL{
public:virtual void name() = 0;
};
class EDG :public LPL {
public:virtual void name() { cout << "EDG" << endl; }
};
class LNG :public LPL
{
public:virtual void name() { cout << "LNG" << endl; }
};
int main(){LPL* pEDG = new EDG;pEDG->name();		//EDGLPL* pLNG = new LNG;pLNG->name();		//LNGreturn 0;
}

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

我们可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,我们不能在类的内部为一个 =0 的函数提供函数体。若定义在类的内部,会出现错误:pure-specifier on function-definition。

4、多态的原理

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个_vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f 代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析

class Base{
public:virtual void func1()  { cout << "Base::func1()" << endl; }
private:int _b = 1;char _ch = 'a';
};
int main(){cout << sizeof(Base) << endl;//12//有了虚函数后对象中会多一个指针,虚函数表指针
}

在这里插入图片描述

添加了两个函数后,类的大小仍不改变。

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 = 'a';
};
int main(){cout << sizeof(Base) << endl;//12//有了虚函数后对象中会多一个指针,虚函数表指针Base bb;
}

⚠️虚函数表指针简称虚表指针。

在这里插入图片描述

我们增加一个派生类Derive去继承Base,且Derive中重写Func1。Base再增加一个虚函数Func2和一个普通函数Func3。代码如下后:

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 = 'a';
};
class Derive :public Base{
public:virtual void func1() { cout << "Derive::func1()" << endl; }
private:int _d = 2;
}; 
void t()
{Base bb;	Derive dd;cout << sizeof(Base) << endl;//12cout << sizeof(Derive) << endl;//16
}

虚函数的重写也叫覆盖。 在这里插入图片描述

派生类对象dd中也有一个虚表指针,dd对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。

基类对象和派生类对象虚表是不一样的,这里我们发现Func1完成了重写,所以dd的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

即派生类由父类和派生类构成,父类中有虚表,子类中包含的父类(含有虚表)+子类自己的成员(无虚表)。另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。

虚表中存储的是虚函数的地址。虚函数和普通函数都存在代码段。

在此我们总结一下派生类的虚表生成:

  1. 先将基类中的虚表内容拷贝一份到派生类虚表中。
  2. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。
  3. 派生类自己,新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

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

那么虚函数表存在哪呢? 栈区?堆区?还是常量区? (虚表地址存在对象的头四个字节上),我们通过如下代码观察:

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;
};
void t() {int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;  Derive d;Base* pb = &b;Derive* pd = &d;printf("Base虚表地址:%p\n", *(int*)pb);printf("Derive虚表地址:%p\n", *(int*)pd);
}

在这里插入图片描述

从打印结果可以看出 虚表位于常量区。(vs和linux下都是)

虚函数表是class specific的,也就是针对一个类来说的,这里就如同类里面的static成员遍历,即它是属于一个类所有对象的,不是属于某个对象特有的,是一个类所有对象公有的。

虚表是什么阶段生成的?

虚表是在编译时期生成的,而虚表指针是在构造函数的初始化列表生成的。一个类的不同对象用的同一张虚表。

虚表是在编译时生成的。
在构造函数中,走初始化列表之前,初始化虚表指针。

我们可以通过如下代码打印类的虚表,大家可以拿来实验:

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; }virtual void func3() { cout << "Derive::func3()" << endl; }
private:int _d = 2;
};
typedef void(*VF_PTR)();
void PrintVFT(VF_PTR vtf[])
{cout << " 虚表地址>" << vtf << endl;for (int i = 0; vtf[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vtf[i]);VF_PTR f = vtf[i];f();}cout << endl;
}
void t()
{Base b;Derive d;// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数///指针的指针数组,这个数组最后面放了一个nullptr// 1.先取b的地址,强转成一个int*的指针// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。// 4.虚表指针传递给PrintVTable进行打印虚表// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。VF_PTR* vTableb = (VF_PTR*)(*(int*)&b);PrintVFT(vTableb);VF_PTR* vTabled = (VF_PTR*)(*(int*)&d);PrintVFT(vTabled);
}

⚠️多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

下面我们看一道关于继承的选择题,来帮我们理解:

假设D类先继承B1,然后继承B2,B1和B2基类均包含虚函数,D类对B1和B2基类的虚函数重写了,并且D类增加了新的虚函数,则:( )

A.D类对象模型中包含了3个虚表指针

B.D类对象有两个虚表,D类新增加的虚函数放在第一张虚表最后

C.D类对象有两个虚表,D类新增加的虚函数放在第二张虚表最后

D.以上全部错误

此题选 B。为什么呢?

A.D类有几个父类,如果父类有虚函数,则就会有几张虚表,自身子类不会产生多余的虚表,所以只有2张虚表。
C.子类自己的虚函数只会放到第一个父类的虚表后面,其他父类的虚表不需要存储,因为存储了也不能调用。


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

在C++中,重载、覆盖(重写)和隐藏(重定义)都是面向对象编程中的概念,用于处理函数的多态性。下面对它们进行比较:

  1. 重载(Overloading)
    • 定义:重载是指在同一个作用域内,使用相同的函数名但具有不同的参数列表的情况。函数重载可以根据参数的类型、顺序和个数进行区分。
    • 特点:
      • 函数名相同,参数列表不同。
      • 返回值类型可以相同也可以不同。
      • 发生在同一个类或命名空间中。
  2. 覆盖(重写,Override)
    • 定义:覆盖是指在派生类中重新实现基类中已经存在的虚函数。通过在派生类中使用相同的函数名、参数列表和返回类型来覆盖基类的函数。(协变除外)
    • 特点:
      • 函数名、参数列表和返回类型相同。
      • 发生在继承关系中,基类函数必须声明为虚函数。
  3. 隐藏(重定义,Hide)
    • 定义:隐藏是指在派生类中定义了与基类中相同名称的非虚函数,从而隐藏了基类中的同名函数。隐藏并不涉及到动态绑定。
    • 特点:
      • 函数名相同,参数列表可以相同也可以不同。
      • 发生在继承关系中,两个基类和派生类的同名函数不构成重写就是重定义。

总结:

  • 重载发生在同一个类或命名空间中的函数之间,根据参数的类型、顺序和个数进行区分。
  • 覆盖发生在继承关系中,派生类重新实现了基类中的虚函数,函数名、参数列表和返回类型相同。
  • 隐藏发生在继承关系中,派生类定义了与基类中同名的非虚函数,基类中的同名函数被隐藏。

需要注意的是,覆盖只能发生在虚函数上,而隐藏可以发生在虚函数和非虚函数上。使用 virtual 关键字声明函数为虚函数,从而允许覆盖。使用作用域解析运算符 :: 可以指定访问被隐藏的基类函数。

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

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

相关文章

【黑马程序员】1、TypeScript介绍_黑马程序员前端TypeScript教程,TypeScript零基础入门到实战全套教程

课程地址&#xff1a;【黑马程序员前端TypeScript教程&#xff0c;TypeScript零基础入门到实战全套教程】 https://www.bilibili.com/video/BV14Z4y1u7pi/?share_sourcecopy_web&vd_sourceb1cb921b73fe3808550eaf2224d1c155 目录 1、TypeScript介绍 1.1 TypeScript是什…

信号通信与消息队列实现的通信:2024/2/23

作业1&#xff1a;将信号和消息队列的课堂代码敲一遍 1.1 信号 1.1.1 信号默认、捕获、忽略处理(普通信号) 代码&#xff1a; #include <myhead.h> void handler(int signo) {if(signoSIGINT){printf("用户键入 ctrlc\n");} } int main(int argc, const ch…

Windows Server 2019 IIS HTTPS证书部署流程详解

一、下载SSL证书 1、下载IIS 类型的证书 以阿里云证书为例&#xff1a; 2、解压已下载的SSL证书压缩包 二、导入SSL证书 1、在服务器上使用WinR组合键&#xff0c;打开运行对话框&#xff0c;输入mmc&#xff0c;单击确定 打开控制台操作界面&#xff0c;如下&#xff1a; …

可视化 RAG 数据 — EDA for Retrieval-Augmented Generation

目录 一、说明 二、准备好 三、准备文件 四、拆分和创建数据集的嵌入 五、构建 LangChain 六、问一个问题 七、可视化 八、下一步是什么&#xff1f; 九、引用 一、说明 像 GPT-4 这样的大型语言模型 &#xff08;LLM&#xff09; 在文本理解和生成方面表现出令人印象深刻的能力…

介绍 CI / CD

目录 一、介绍 CI / CD 1、为什么要 CI / CD 方法简介 1、持续集成 2、持续交付 3、持续部署 2、GitLab CI / CD简介 3、GitLab CI / CD 的工作原理 4、基本CI / CD工作流程 5、首次设置 GitLab CI / CD 6、GitLab CI / CD功能集 一、介绍 CI / CD 在本文档中&#x…

Python中format()方法的基本使用,第一种用法 <模板字符串>.format(<参数列表>)。

第一种用法&#xff1a; <模板字符串>.format(<参数列表>) 解析&#xff1a; 其中&#xff1a; <模板字符串>是包含占位符或者叫槽&#xff08;用花括号 {} 表示&#xff09;的字符串&#xff0c;用来指定最终格式化后的字符串的样式和结构。<参数列表…

STM32 系统滴答时钟启动过程 SysTick_Config

STM32 系统滴答时钟启动过程 SysTick_Config 1. 系统滴答时钟1.1 简介1.2 配置1.3 启动和更新 1. 系统滴答时钟 1.1 简介 SysTick&#xff1a;系统滴答时钟&#xff0c;属于Cortex-M4内核中的一个外设&#xff0c;24bit向下递减计数。 Systick定时器常用来做延时&#xff0c;…

二次元风格个人主页HTML源码

源码介绍 直接上传服务器压缩包解压就完事了&#xff0c;修改index.html内代码即可&#xff0c;注释写的很全&#xff0c;替换图片在文件夹img&#xff0c;只有前端&#xff0c;没有后台&#xff0c;大佬如果需要&#xff0c;可以自行添加后台。本源码非常适合个人工作室主页。…

CMake管理CUDA并使用cuSOLVER等

一、出现问题 我在使用官方案例的时候&#xff0c;使用VS2022CMake管理编译的时候出现如下的错误&#xff1a; 官方CMakeLists.txt&#xff1a; cmake_minimum_required(VERSION 3.9)set(ROUTINE bicgstab)project("${ROUTINE}_example"DESCRIPTION "GPU-Acce…

在Linux服务器上部署一个单机项目

目录 一、jdk安装 二、tomcat安装 三、MySQL安装 四、部署项目 一、jdk安装 1. 上传jdk安装包 jdk-8u151-linux-x64.tar.gz 进入opt目录&#xff0c;将安装包拖进去 2. 解压安装包 这里需要解压到usr/local目录下&#xff0c;在这里我新建一个文件夹保存解压后的文件 [r…

使用 ES|QL 优化可观察性:简化 Kubernetes 和 OTel 的 SRE 操作和问题解决

作者&#xff1a;Bahubali Shetti 作为一名运营工程师&#xff08;SRE、IT 运营、DevOps&#xff09;&#xff0c;管理技术和数据蔓延是一项持续的挑战。 简单地管理大量高维和高基数数据是令人难以承受的。 作为单一平台&#xff0c;Elastic 帮助 SRE 将无限的遥测数据&#…

责任链模式与spring容器的搭配应用

背景 有个需求&#xff0c;原先只涉及到一种A情况设备的筛选&#xff0c;每次筛选会经过多个流程&#xff0c;比如先a功能&#xff0c;a功能通过再筛选b功能&#xff0c;然后再筛选c功能&#xff0c;以此类推。现在新增了另外一种B情况的筛选&#xff0c;B情况同样需要A情况的筛…

算法学习(十一)拓扑排序

拓扑排序 1. 概念 对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序&#xff0c;是将G中所有顶点排成一个线性序列&#xff0c;使得图中任意一对顶点u和v&#xff0c;若边<u,v>∈E(G)&#xff0c;则u在线性序列中出现在v之前。通常&#xff0c;这样的线性…

【Java程序员面试专栏 数据结构】三 高频面试算法题:栈和队列

一轮的算法训练完成后,对相关的题目有了一个初步理解了,接下来进行专题训练,以下这些题目就是汇总的高频题目,因为栈和队列这两哥们结构特性比较向对应,所以放到一篇Blog中集中练习 题目题干直接给出对应博客链接,这里只给出简单思路、代码实现、复杂度分析 题目关键字…

ChatGPT助你成功求职:智能引导下的职场新起点【文章底部添加可得内推码汇总表】

在当今竞争激烈的就业市场中&#xff0c;如何有效地进行求职已成为许多人面临的挑战。然而&#xff0c;随着人工智能的不断发展&#xff0c;ChatGPT作为一种强大的语言模型&#xff0c;不仅可以为我们提供信息&#xff0c;还可以成为求职过程中的得力助手。在这篇文章中&#x…

每日一学—由面试题“Redis 是否为单线程”引发的思考

文章目录 &#x1f4cb; 前言&#x1f330; 举个例子&#x1f3af; 什么是 Redis&#xff08;知识点补充&#xff09;&#x1f3af; Redis 中的多线程&#x1f3af; I/O 多线程&#x1f3af; Redis 中的多进程&#x1f4dd; 结论&#x1f3af;书籍推荐&#x1f525;参与方式 &a…

K线实战分析系列之五:刺透形态——多方反攻信号

K线实战分析系列之五&#xff1a;刺透形态——多方反攻信号 一、刺透形态二、类似刺透形态三、刺透形态的总结 一、刺透形态 阴线在前&#xff0c;阳线在后显示市场曾经跌到了低位&#xff0c;但是在盘中又将价格收回&#xff0c;并且多方收复了前一天大部分的失地 二、类似刺…

[SUCTF 2019]EasySQL1 题目分析与详解

一、题目介绍 1、题目来源&#xff1a; BUUCTF网站&#xff0c;网址&#xff1a;https://buuoj.cn/challenges 2、题目描述&#xff1a; 通过以上信息&#xff0c;拿到flag。 二、解题思路 首先打开靶机&#xff0c;尝试输入1查看回显&#xff0c;回显如图所示&#xff1a;…

利用psutil库检查脚本是否在运行

摘要 如果要判断某一脚本是否在运行&#xff0c;可以通过psutil库获取所有进程的cmdline&#xff0c;并判断指定的文件名是否在cmdline中。 目录 1.psutil库简介 2.检查代码及说明 2.1检查思路 2.2异常捕获 2.3执行方法 1.psutil库简介 psutil 是一个跨平台&#xff08;…

记一次生产jvm oom问题

前言 jvm添加以下参数&#xff0c;发生OOM时自动导出内存溢出文件 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/opt 内存分析工具&#xff1a; MAT, 下载地址&#xff1a;Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation&#xff0c; 注意工具地址…