<C++> 多态

news2025/1/20 21:50:05

1.多态的概念

多态是指同一个函数在不同情况下表现出不同的行为。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

多态有两种形式:静态多态动态多态。静态多态是指在编译时就能确定函数的调用版本,而动态多态是指在运行时才能确定函数的调用版本。

实现多态需要使用虚函数。虚函数是在基类中使用关键字virtual声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接或后期绑定。

纯虚函数是一种特殊的虚函数,它没有实现. 在基类中定义纯虚函数,以便在派生类中重新定义该函数更好地适用于对象. 纯虚函数可以通过在声明中使用 “= 0” 来指定.

2.多态的定义及实现

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件

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

虚函数

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

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

注意:

  • 只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual。
  • 虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何关系。虚函数这里的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性。

虚函数的重写

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

示例:

#include <iostream>
using namespace std;

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

class Student : public Person {
public:
    //派生类重写的虚函数也可以不加virtual
    virtual void BuyTicket() {
        cout << "买票-半价" << endl;
    }
};

void Func(Person &people) {
    people.BuyTicket();
}

void Func(Person *people){
	people->BuyTicket();
}

int main() {
    Person Mike;
    Func(Mike);   //买票-全价
	Func(&Mike);   //买票-全价
    
    Student Johnson;
    Func(Johnson);  //买票-半价
	Func(&Johnson); //买票-半价
    
    return 0;
}

现在我们就可以通过父类Person的指针或者引用调用虚函数BuyTicket,此时不同类型的对象,调用的就是不同的函数,产生的也是不同的结果,进而实现了函数调用的多种形态。

在这里插入图片描述

注意:

在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

虚函数重写的两个例外

1.协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变

例如,假设我们有一个名为 Base 的基类和一个名为 Derived 的派生类。如果 Base 类中有一个名为 foo 的虚函数,它返回一个指向 Base 对象的指针:

class Base {
public:
    virtual Base* foo();
};

那么在 Derived 类中,我们可以重写 foo 函数并将其返回类型更改为指向 Derived 对象的指针:

class Derived : public Base {
public:
    virtual Derived* foo() override;
};

这就是协变。

2.析构函数的重写(基类与派生类析构函数的名字不同)

析构函数虽然函数名不同,但是也可以构成重写。因为编译器为了让析构函数实现多态,会将它们的名字都处理为 destructor,这样就可以构成多态。如果析构函数不构成多态,则指针什么类型就会调用什么类型的析构函数,这也就导致:如果派生类的析构函数中有资源要释放,而这里却没有释放掉那些资源,就会导致内存泄漏的问题存在。所以为了防止这种情况,必须要将析构函数定义为虚函数。这也就是编译器将析构函数重命名为 destructor 的原因。析构函数构成多态后,会先调用子类对象的析构函数,再调用父类对象的析构函数,完成对象的资源清理。

例如,下面代码中父类Person和子类Student的析构函数构成重写。

#include <iostream>
using namespace std;
//父类
class Person {
public:
    virtual ~Person() {
        cout << "~Person()" << endl;
    }
};
//子类
class Student : public Person {
public:
    virtual ~Student() {
        cout << "~Student()" << endl;
    }
};

int main() {
    Student s;

    return 0;
}
/*输出结果:
~Student()
~Person()
*/

C++11final和override

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

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

例如,父类Person的虚函数BuyTicket被final修饰后就不能再被重写了,子类若是重写了父类的BuyTicket函数则编译报错。

#include <iostream>
using namespace std;
//父类
class Person {
public:
    //被final修饰,该虚函数不能再被重写
    virtual void BuyTicket() final {
        cout << "买票-全价" << endl;
    }
};
//子类
class Student : public Person {
public:
    //重写,编译报错
    virtual void BuyTicket() {
        cout << "买票-半价" << endl;   //err编译报错
    }
};

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

例如,子类Student和Soldier的虚函数BuyTicket被override修饰,编译时就会检查子类的这两个BuyTicket函数是否重写了父类的虚函数,如果没有则会编译报错。

//父类
class Person {
public:
    virtual void BuyTicket() {
        cout << "买票-全价" << endl;
    }
};
//子类
class Student : public Person {
public:
    //子类完成了父类虚函数的重写,编译通过
    virtual void BuyTicket() override {
        cout << "买票-半价" << endl;
    }
};
//子类
class Soldier : public Person {
public:
    //子类没有完成了父类虚函数的重写,编译报错
    virtual void BuyTicket(int i) override {
        cout << "优先-买票" << endl;
    }
};

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

在这里插入图片描述

3.抽象类

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

示例1:

#include <iostream>
using namespace std;
//抽象类(接口类)
class Car {
public:
    //纯虚函数
    virtual void Drive() = 0;
};

int main() {
    Car c;//抽象类不能实例化出对象,error
    return 0;
}

实例2:

#include <iostream>
using namespace std;
//抽象类(接口类)
class Car {
public:
    //纯虚函数
    virtual void Drive() = 0;
};
//派生类
class Benz : public Car {
public:
    //重写纯虚函数
    virtual void Drive() {
        cout << "Benz-舒适" << endl;
    }
};
//派生类
class BMV : public Car {
public:
    //重写纯虚函数
    virtual void Drive() {
        cout << "BMV-操控" << endl;
    }
};

int main() {
    //派生类重写了纯虚函数,可以实例化出对象
    Benz b1;
    BMV b2;
    //不同对象用基类指针调用Drive函数,完成不同的行为
    Car *p1 = &b1;
    Car *p2 = &b2;
    p1->Drive();//Benz-舒适
    p2->Drive();//BMV-操控
    return 0;
}

接口继承和实现继承

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

4.多态的原理

虚函数表

Base类实例化出对象的大小是多少?

#include <iostream>
using namespace std;
class Base {
public:
    virtual void Func1() {
        cout << "Func1()" << endl;
    }

private:
    int _b = 1;
};

int main() {
    Base b;
    cout << sizeof(b) << endl;//8/16
    return 0;
}

在VS2022环境下x86为8,x64为16

在这里插入图片描述

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关)。

对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表

那么派生类中这个表放了些什么呢?

针对上面的代码我们做出以下改造

1.我们增加一个派生类Derive去继承Base

2.Derive中重写Func1

3.Base再增加一个虚函数Func2和一个普通函数Func3

#include <iostream>
using namespace std;
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;
}

通过调试可以发现,父类对象b和基类对象d当中除了自己的成员变量之外,父类和子类对象都有一个虚表指针,分别指向属于自己的虚表。

在这里插入图片描述

实际上虚表当中存储的就是虚函数的地址,因为父类当中的Func1和Func2都是虚函数,所以父类对象b的虚表当中存储的就是虚函数Func1和Func2的地址。

而子类虽然继承了父类的虚函数Func1和Func2,但是子类对父类的虚函数Func1进行了重写,因此,子类对象d的虚表当中存储的是父类的虚函数Func2的地址和重写的Func1的地址。这就是为什么虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数地址的覆盖,重写是语法的叫法,覆盖是原理层的叫法。

其次需要注意的是:Func2是虚函数,所以继承下来后放进了子类的虚表,而Func3是普通成员函数,继承下来后不会放进子类的虚表。此外,虚函数表本质是一个存虚函数指针的指针数组,一般情况下会在这个数组最后放一个nullptr。

总结一下,派生类的虚表生成步骤如下

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

虚表是什么阶段初始化的?虚函数存在哪里?虚表存在哪里?

虚表实际上是在构造函数初始化列表阶段进行初始化的,注意虚表当中存的是虚函数的地址不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中。另外,对象中存的不是虚表而是指向虚表的指针。

多态的原理

下面代码中,为什么当父类Person指针指向的是父类对象Mike时,调用的就是父类的BuyTicket,当父类Person指针指向的是子类对象Johnson时,调用的就是子类的BuyTicket?

//父类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
	int _p = 1;
};
//子类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
	int _s = 2;
};

int main()
{
	Person Mike;
	Student Johnson;
	Johnson._p = 3; //以便观察是否完成切片
	Person* p1 = &Mike;
	Person* p2 = &Johnson;
	p1->BuyTicket(); //买票-全价
	p2->BuyTicket(); //买票-半价
	return 0;
}

通过调试可以发现,对象Mike中包含一个成员变量_p和一个虚表指针,对象Johnson中包含两个成员变量_p和_s以及一个虚表指针,这两个对象当中的虚表指针分别指向自己的虚表。

在这里插入图片描述

围绕此图分析便可得到多态的原理:

  • 父类指针p1指向Mike对象,p1->BuyTicket在Mike的虚表中找到的虚函数就是Person::BuyTicket。
  • 父类指针p2指向Johnson对象,p2>BuyTicket在Johnson的虚表中找到的虚函数就是Student::BuyTicket。

这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

为什么必须使用父类的指针或者引用去调用虚函数呢?为什么使用父类对象去调用虚函数达不到多态的效果呢?

使用父类指针或者引用时,实际上是一种切片行为,切片时只会让父类指针或者引用得到父类对象或子类对象中切出来的那一部分。

Person* p1 = &Mike;
Person* p2 = &Johnson;

在这里插入图片描述

因此,我们后序用p1和p2调用虚函数时,p1和p2通过虚表指针找到的虚表是不一样的,最终调用的函数也是不一样的。

Person p1 = Mike;
Person p2 = Johnson;

使用父类对象时,切片得到部分成员变量后,会调用父类的拷贝构造函数对那部分成员变量进行拷贝构造,而拷贝构造出来的父类对象p1和p2当中的虚表指针指向的都是父类对象的虚表。因为同类型的对象共享一张虚表,他们的虚表指针指向的虚表是一样的。

在这里插入图片描述

因此,我们后序用p1和p2调用虚函数时,p1和p2通过虚表指针找到的虚表是一样的,最终调用的函数也是一样的,也就无法构成多态。

总结一下:

  1. 构成多态,指向谁就调用谁的虚函数,跟对象有关。
  2. 不构成多态,对象类型是什么就调用谁的虚函数,跟类型有关。

动态绑定与静态绑定

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

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

5.单继承和多继承关系的虚函数表

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的

单继承关系

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

private:
    int b;
};

其中,基类和派生类对象的虚表模型如下:

在这里插入图片描述

在单继承关系当中,派生类的虚表生成过程如下:

  1. 继承基类的虚表内容到派生类的虚表。
  2. 对派生类重写了的虚函数地址进行覆盖,比如func1。
  3. 虚表当中新增派生类当中新的虚函数地址,比如func3和func4。

使用程序打印出虚表地址

思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr

  1. 先取b的地址,强转成一个int*的指针
  2. 再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
  3. 再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
  4. 虚表指针传递给PrintVTable进行打印虚表
  5. 需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
#include <iostream>
using namespace std;
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; }

private:
    int b;
};

typedef void (*VFPTR)();
void PrintVTable(VFPTR vTable[]) {
    // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
    cout << " 虚表地址>" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i) {
        printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
        VFPTR f = vTable[i];
        f();
    }
    cout << endl;
}

int main() {
    Base b;
    Derive d;
    VFPTR* vTableb = (VFPTR*)(*(int*)&b);
    PrintVTable(vTableb);
    VFPTR* vTabled = (VFPTR*)(*(int*)&d);
    PrintVTable(vTabled);
    return 0;
}

/*输出结果
 虚表地址>007231D4
 第0个虚函数地址 :0X721050,->Base::func1
 第1个虚函数地址 :0X721070,->Base::func2

 虚表地址>007231BC
 第0个虚函数地址 :0X721090,->Derive::func1
 第1个虚函数地址 :0X721070,->Base::func2
 第2个虚函数地址 :0X7210b0,->Derive::func3
 第3个虚函数地址 :0X7210d0,->Derive::func4

多继承关系

以下列多继承关系为例,我们来看看基类和派生类的虚表模型。

//基类1
class Base1 {
public:
    virtual void func1() { cout << "Base1::func1()" << endl; }
    virtual void func2() { cout << "Base1::func2()" << endl; }

private:
    int _b1;
};
//基类2
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;
};

其中,两个基类的虚表模型如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pl54mAIM-1693279473026)(C:\Users\32784\AppData\Roaming\Typora\typora-user-images\image-20230829110410907.png)]

而派生类的虚表模型就不那么简单了,派生类的虚表模型如下:

在这里插入图片描述

在多继承关系当中,派生类的虚表生成过程如下:

  1. 分别继承各个基类的虚表内容到派生类的各个虚表当中。
  2. 对派生类重写了的虚函数地址进行覆盖(派生类中的各个虚表中存有该被重写虚函数地址的都需要进行覆盖),比如func1。
  3. 在派生类第一个继承基类部分的虚表当中新增派生类当中新的虚函数地址,比如func3。

菱形继承关系

以下列菱形虚拟继承关系为例,我们来看看基类和派生类的虚表模型。

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

private:
    int _a;
};

class B : virtual public A {
public:
    virtual void funcA() {
        cout << "B::funcA()" << endl;
    }
    virtual void funcB() {
        cout << "B::funcB()" << endl;
    }

private:
    int _b;
};

class C : virtual public A {
public:
    virtual void funcA() {
        cout << "C::funcA()" << endl;
    }
    virtual void funcC() {
        cout << "C::funcC()" << endl;
    }

private:
    int _c;
};

class D : public B, public C {
public:
    virtual void funcA() {
        cout << "D::funcA()" << endl;
    }
    virtual void funcD() {
        cout << "D::funcD()" << endl;
    }

private:
    int _d;
};

代码当中的继承关系图如下:

在这里插入图片描述

其中,A类当中有一个虚函数funcA,B类当中有一个虚函数funcB,C类当中有一个虚函数funcC,D类当中有一个虚函数funcD。此外B类、C类和D类当中均对A类当中的funcA进行了重写。

A类对象的成员包括一个虚表指针和成员变量_a,虚表指针指向的虚表当中存储的是A类虚函数funcA的地址。

在这里插入图片描述

B类由于是虚拟继承的A类,所以B类对象当中将A类继承下来的成员放到了最后,除此之外,B类对象的成员还包括一个虚表指针、一个虚基表指针和成员变量_b,虚表指针指向的虚表当中存储的是B类虚函数funcB的地址。
虚基表当中存储的是两个偏移量,第一个是虚基表指针距离B虚表指针的偏移量,第二个是虚基表指针距离虚基类A的偏移量。

在这里插入图片描述

C类对象当中的成员分布情况与B类对象当中的成员分布情况相同。C类也是虚拟继承的A类,所以C类对象当中将A类继承下来的成员放到了最后,除此之外,C类对象的成员还包括一个虚表指针、一个虚基表指针和成员变量_c,虚表指针指向的虚表当中存储的是C类虚函数funcC的地址。
虚基表当中存储的是两个偏移量,第一个是虚基表指针距离C虚表指针的偏移量,第二个是虚基表指针距离虚基类A的偏移量。

在这里插入图片描述

D类对象当中成员的分布情况较为复杂,D类的继承方式是菱形虚拟继承,在D类对象当中,将A类继承下来的成员放到了最后,除此之外,D类对象的成员还包括从B类继承下来的成员、从C类继承下来的成员和成员变量_d。
需要注意的是,D类对象当中的虚函数funcD的地址是存储到了B类的虚表当中。

在这里插入图片描述
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面使用这样的模型访问基类成员有一定的性能损耗。

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

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

相关文章

电子价签如何让电信门店数字化事半功倍?

数字化转型&#xff0c;高效的工具首先跟上。早在2020年&#xff0c;深圳电信就与云里物里开展商业合作&#xff0c;在深圳所有电信营业厅安装云里物里的ESL电子标签&#xff0c;以替代传统纸质标签的显示。经过几年的效果认证&#xff0c;云里物里的数字化智显设备得到了深圳电…

Python基础学习第三天:Python语法

执行 Python 语法 正如我们在上一节中学习到的&#xff0c;可以直接在命令行中编写执行 Python 的语法&#xff1a; >>> print("Hello, World!") Hello, World!或者通过在服务器上创建 python 文件&#xff0c;使用 .py 文件扩展名&#xff0c;并在命令行…

“互联网+”背景下燃气行业的数字化之路

文章来源&#xff1a;智慧美好生活 关键词&#xff1a;智慧燃气、智慧燃气场站、智慧燃气平台、设备设施数字化、数字孪生、工业互联网 近年来&#xff0c;随着互联网行业的发展&#xff0c;其影响力正在逐渐渗透到各个领域。在能源行业&#xff0c;各个互联网巨头与燃气企业…

引领未来商业:循环购模式的创新突破-微三云门门

尊敬的创业者们&#xff0c;我是微三云门门。今天&#xff0c;我将与您深入探讨一种崭新的商业模式——循环购模式。该模式在私域流量领域取得了巨大成功&#xff0c;仅用6个月时间就创造了超过400万的用户数量&#xff01; 循环购商业模式的核心概念涵盖三个关键要素&#xf…

python爬虫实战(5)--获取小破站热榜

1. 分析地址 打开小破站热榜首页&#xff0c;查看响应找到如下接口地址 2. 编码 定义请求头 拿到标头 复制粘贴&#xff0c;处理成json 处理请求头代码如下: def format_headers_to_json():f open("data.txt", "r", encoding"utf-8") # 读…

C语言(第三十天)

1. 什么是bug bug本意是昆虫”或“虫子”&#xff0c;现在一般是指在电脑系统或程序中&#xff0c;隐藏着的一些未被发现的缺陷或问 题&#xff0c;简称程序漏洞。 “Bug” 的创始人格蕾丝赫柏&#xff08;Grace Murray Hopper&#xff09;&#xff0c;她是一位为美国海军工作的…

【SpringBoot】使用 HandlerInterceptor 拦截器进行用户登录验证? 为什么不使用 SpingAOP ?

文章目录 前言一、为什么不使用 SpringAOP ?1, 需求分析2, SpringAOP 能实现吗? 二、使用 HandlerInterceptor1, 实现 HandlerInterceptor 接口2, 将自定义拦截器加入到系统配置 三、HandlerInterceptor 实现原理源码分析 总结 前言 各位读者好, 我是小陈, 这是我的个人主页,…

crawlab通过docker单节点部署简单爬虫

crawlab 单节点docker安装 此处介绍的是单节点的方式&#xff0c;多节点的情况可以把爬虫上传到一个节点中&#xff0c;之后会同步到其它节点上 version: 3.3 services:master:image: crawlabteam/crawlabcontainer_name: crawlab_masterrestart: alwaysenvironment:CRAWLAB…

GaussDB技术解读系列:高级压缩之OLTP表压缩

8月16日&#xff0c;第14届中国数据库技术大会&#xff08;DTCC2023&#xff09;在北京国际会议中心顺利举行。在GaussDB“五高两易”核心技术&#xff0c;给世界一个更优选择的专场&#xff0c;华为云数据库GaussDB首席架构师冯柯对华为云GaussDB数据库的高级压缩技术进行了详…

〔018〕Stable Diffusion 之 批量替换人脸 篇

✨ 目录 &#x1f388; 下载插件&#x1f388; 插件基础使用&#x1f388; 基础使用效果&#x1f388; 批量处理图片&#x1f388; 多人脸部替换 &#x1f388; 下载插件 如果重绘图片的时候&#xff0c;你只想更换人物面部的话&#xff0c;可以参考这篇文章扩展地址&#xff…

【GAMES202】Real-Time Environment Mapping2—实时环境光照2

一、Shadow from Environment Lighting 上篇我们说了给定Environment&#xff0c;如何计算一个着色点的Shading&#xff0c;但没说Shadow。而事实上&#xff0c;实时渲染中很难做到环境光的Shadow。 原因也很容易想到&#xff0c;一种观点我们把环境光当成多光源问题&#xff…

软件测试实训系统建设方案

一 、系统概述 软件测试实训系统是软件开发过程中的一项重要测试活动&#xff0c;旨在验证不同软件模块或组件之间的集成与交互是否正常。综合测试确保各个模块按照设计要求正确地协同工作&#xff0c;以实现整个软件系统的功能和性能。以下是软件测试实训系统的一般流程和步骤…

2023腾讯云服务器多少钱一年?CPU内存带宽配置报价

腾讯云服务器租用价格表&#xff1a;轻量应用服务器2核2G4M带宽112元一年&#xff0c;540元三年、2核4G5M带宽218元一年&#xff0c;2核4G5M带宽756元三年、云服务器CVM S5实例2核2G配置280.8元一年、GPU服务器GN10Xp实例145元7天&#xff0c;腾讯云服务器网长期更新腾讯云轻量…

格式化u盘怎么做?分享几个简单方法!

“u盘用了好久&#xff0c;存储了很多不太重要的文件&#xff0c;想将它格式化之后继续使用。但是不知道怎样才能格式化u盘&#xff0c;大家有什么好的建议吗&#xff1f;” U盘是常见的移动存储设备&#xff0c;然而&#xff0c;在日常生活中使用u盘时&#xff0c;我们可能因为…

锁策略、原子编程CAS 和 synchronized 优化过程

前言 锁冲突&#xff1a;两个线程获取一把锁&#xff0c;一个线程阻塞等待&#xff0c;一个线程加锁成功。 目录 前言 一、锁策略 &#xff08;一&#xff09;乐观锁和悲观锁 &#xff08;二&#xff09;重量级锁和轻量级锁 &#xff08;三&#xff09;自旋锁和挂起等待…

HTML基础1

一、创建项目和标签基础 1.1 什么是HTML 英文全称Hyper Text Markup Language&#xff0c; 中文全称为超文本标记语言。 超文本标记语言是标准通用标记语言下的一个应用&#xff0c;也是一种规范&#xff0c;一种标准&#xff0c;它通过标记符号来标记要显示的网页中的各个部…

进程与线程(概念、并行、并发)

进程与线程 一、定位二、什么是进程&#xff1f;三、进程管理1、PCB相关属性&#xff08;了解&#xff09;2、多任务的处理方式 四、什么是线程&#xff1f;总结 一、定位 在计算机系统中&#xff0c;操作系统是其中的重要一环。对上&#xff0c;给软件提供稳定的运行环境&…

ssm+vue线上体验馆管理系统源码和论文

ssmvue线上体验馆管理系统源码和论文085 开发工具&#xff1a;idea 数据库mysql5.7 数据库链接工具&#xff1a;navcat,小海豚等 技术&#xff1a;ssm 摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0…

el-collapse折叠面板默认全部展开/关闭

所要展开项的name标识符与v-model绑定值匹配即可默认展开。 1. 案例 <el-collapse v-model"activeNames"><el-collapse-item name"0" title段落1>那一年&#xff0c;花开得不是最好&#xff0c;可是还好&#xff0c;我遇到你&#xff1b;&l…

高忆管理:A股已具备年度配置价值

周末利好四箭齐发&#xff0c;财政部、证监会、三大买卖所均宣布严重方针调整&#xff0c;首要包含印花税调降、IPO节奏阶段性收紧、融资保证金比例降至80%、限制大股东和实控人减持等。二级商场上&#xff0c;大盘大幅高开后一路震动走低&#xff0c;终究收盘涨幅显着收窄。业…