【c++】全面理解C++多态:虚函数表深度剖析与实践应用

Alt

🔥个人主页Quitecoder

🔥专栏c++笔记仓

Alt

朋友们大家好,通过本篇文章,来详细理解多态的内容

目录

  • `1.多态的定义及实现`
    • `1.1多态的构成条件`
    • `1.2虚函数的重写`
    • `1.3 C++11 override 和 final`
    • `1.4重载、覆盖(重写)、隐藏(重定义)的对比`
  • `2.多态的原理`
    • `2.1虚函数表`
    • `2.2多态的原理`
    • `2.3单继承的虚函数表`
  • `3.抽象类`
    • `3.1接口继承与实现继承`
    • `3.2静态多态与动态多态`
    • `3.3例题`
  • `4.多继承中的虚函数表`
    • `4.1菱形继承和菱形虚拟继承`
    • `4.2菱形虚拟继承:`
  • `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 ps;Student st;Func(ps);Func(st);return 0;
}

在这里插入图片描述
普通人全价,学生半价

1.1多态的构成条件

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

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

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

在这里插入图片描述
指向谁调用谁

void Func(Person  p)
{p.BuyTicket();
}

如果这样调用,就不是指针或引用了,现在就不是多态
在这里插入图片描述

1.2虚函数的重写

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

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

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

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

虚函数重写的三个例外

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

只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数

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

当我们通过基类的指针来删除一个派生类的对象时,如果基类的析构函数没有被声明为虚拟的(virtual),将会发生对象的不完全析构。这意味着只有基类的析构代码会被执行,而派生类的析构逻辑不会调用,可能导致资源泄露或其他问题。

在给定的代码中,Person 类的析构函数被声明为虚拟的:

virtual ~Person() { cout << "~Person()" << endl; }

这意味着任何从 Person 派生的类,像 Student,都应该提供析构函数的一个覆盖版本:

virtual ~Student() { cout << "~Student()" << endl; }

delete p2; 被执行的时候(其中 p2 是一个基类 Person 类型的指针,指向一个 Student 对象),Student 的析构函数首先会被调用(子类),然后是 Person 的析构函数(基类)

因此,重写基类的虚拟析构函数确保了当通过基类指向派生类对象的指针进行 delete 操作时,能够按照正确的顺序调用派生类和基类的析构函数

  1. 派生类可以不写virtual
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:void BuyTicket() { cout << "买票-半价" << endl; }
};

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

1.3 C++11 override 和 final

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

  1. final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};

在这里插入图片描述

用final修饰的类叫做最终类,不能被继承

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

在这里插入图片描述

  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car {
public:virtual void Drive() {}
};
class Benz :public Car {
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

在这里插入图片描述

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

重载发生在同一作用域内。当两个或者更多的函数拥有相同的名字,但是**参数列表不同(参数类型、参数个数或者参数顺序不同)**时,这些函数被称为重载函数。

class MyClass {
public:void func() void func(int i)void func(double d) 
};

重写仅在基类和派生类之间发生,且只针对虚函数。当派生类定义一个与基类中虚函数签名完全相同的函数时(即函数名、参数列表和返回类型相同),派生类函数会覆盖(重写)基类中对应的虚函数。这是多态的基础,使得在运行时可以通过基类的指针或引用调用派生类的函数实现

示例:

class Base {
public:virtual void func() { /* ... */ }
};class Derived : public Base {
public:void func() override { /* ... */ } // 覆盖(重写)基类中的func
};

隐藏也是在类的继承关系中发生,但它和是否为虚函数无关。在派生类中定义了一个新的函数,如果这个函数的名字与基类中的某个函数的名字相同,但是参数列表不同,那么它会隐藏(也称为重定义)所有与它同名的基类函数,不论基类中同名函数参数列表如何

示例:

class Base {
public:void func() { /* ... */ }void func(int i) { /* ... */ }
};class Derived : public Base {
public:void func(double d) { /* ... */ } // 隐藏了基类的func()// 注意:现在Base的func()和func(int)都被隐藏,只能通过Derived的对象访问新的func(double)
};

在继承的类中隐藏了基类中的同名函数(不论是重载还是同签名的函数),如果想要调用被隐藏的函数,需要显式地指明作用域:

Derived obj;
obj.Base::func(); // 显式调用Base类中被隐藏的func()
obj.Base::func(42); // 显式调用Base类中被隐藏的func(int)
obj.func(3.14); // 调用Derived类中的func(double)

两个基类和派生类的同名函数,不构成重写就是隐藏

2.多态的原理

2.1虚函数表

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

sizeof(Base)是多少?
答案是8,我们进行测试观察:

在这里插入图片描述
除了_b成员,还多一个__vfptr放在对象的前面,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表
用内存窗口观察:
在这里插入图片描述
它是占八个字节的

2.2多态的原理

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }private:int _i = 1;
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }int _j = 2;
};void Func(Person* p)
{p->BuyTicket();
}int main()
{Person Mike;Func(&Mike);Student Johnson;Func(&Johnson);return 0;
}

在这里插入图片描述

这里的指向父类调父类,指向子类调子类是怎么实现的呢? 我们进行调试

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

Johnson首先继承了父类的部分,有虚表和虚表指针,这两个虚表指针不一样,他们指向内容不一样,一个指向父类的Buyticket,另一个指向子类的

p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数Person::BuyTicket
p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket

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

反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。反思一下为什么

满足多态条件,这里的调用生成的指令就会指向对象的虚表中找对应的虚函数调用

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

	p->BuyTicket();
009924E1  mov         eax,dword ptr [p]  
009924E4  mov         edx,dword ptr [eax]  
009924E6  mov         esi,esp  
009924E8  mov         ecx,dword ptr [p]  
009924EB  mov         eax,dword ptr [edx]  
009924ED  call        eax  
009924EF  cmp         esi,esp  
009924F1  call        __RTC_CheckEsp (09912B2h)

满足多态的情况下

  • p中存的是mike对象的指针,将p移动到eax中

  • [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx

  • [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax

  • call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的

同类型共用一个虚表

Person Mike;
Func(&Mike);Person p1;
Func(&p1);

在这里插入图片描述
现在如果不满足多态呢?

我将父类进行修改

class Person {
public:void BuyTicket() { cout << "买票-全价" << endl; }
private:int _i = 1;
};
	p->BuyTicket();
005B24E1  mov         ecx,dword ptr [p]  
005B24E4  call        Person::BuyTicket (05B149Ch)

它在编译链接时就确定了

2.3单继承的虚函数表

来看下面的类:

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;
};
int main()
{Base b;Derive d;return 0;
}

在这里插入图片描述
我们发现Derive少了两个虚表指针,它只有重写的func1和继承的func2,没有func3,func4,这里是监视窗口的问题

Derive 类的虚表中,会有以下指向虚函数的指针:

  1. 指向 Derive::func1 的指针 (重写了 Base::func1
  2. 指向 Base::func2 的指针 (继承自 BaseDerive 没有重写)
  3. 指向 Derive::func3 的指针 (Derive 新增的虚函数)
  4. 指向 Derive::func4 的指针 (Derive 新增的虚函数)

我们通过内存来确认:

在这里插入图片描述
我们不是很确认后面两个地址就是func3和func4的地址

那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数

这里我们用到函数指针数组来实现:

虚函数表的本质就是函数指针数组

void(*p[10])();

这个就定义了一个函数指针数组,我们用typedef来进行优化一下:

typedef void(*VFPTR)();
VFPTR p2[10];

我们定义一个打印虚表的函数

void PrintVFT(VFPTR* vft)
{for (size_t i = 0; i < 4; i++){printf("%p->", vft[i]);VFPTR pf = vft[i];(*pf)();//pf();}
}

依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数

函数写好后,关键是我如何取到它的地址?

Derive d;
int ptr = (int)d;  

上面是不支持转换的,只有有关联的类型才能互相转换

但是,指针可以随意转换

VFPTR* ptr = (VFPTR*)(*((int*)&d));
  1. &d 取得 d 对象的地址。
  2. (int*)&dd 对象的地址转换为 int* 类型的指针。这里假定 int 大小足够存储指针
  3. *((int*)&d) 对转换后的指针进行解引用,得到的是 d 对象内存起始处的值。由于在C++中,一个包含虚函数的对象在内存起始地址处通常存储着指向虚表的指针,因此这步操作实际上获取的是指向 Derive 虚表的指针
  4. (VFPTR*)int 类型的值强制转换为 VFPTR* 类型,也就是指向函数指针的指针。
  5. 最终,ptr 就是指向 Derive 类的虚表的指针

因此,VFPTR* ptr 就是指向目标对象 d 的虚表的指针。之后调用 PrintVFT(ptr); 就可以遍历虚表中的每个条目并调用对应的函数(这里的函数都是通过函数指针 VFPTR 调用的)

在这里插入图片描述

3.抽象类

在虚函数的后面写上 =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;}
};

3.1接口继承与实现继承

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

3.2静态多态与动态多态

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

3.3例题

下面函数输出结果是什么?

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;
}
A: A->0  B: B->1  C: A->1  D: B->0  E: 编译出错  F: 以上都不正确

正确答案是B

B 继承自类 A 并且 复写了 A 中的虚函数 func

首先,复写(覆盖)的本质是派生类提供基类虚函数的一个新的实现。基类中的虚函数定义了一个接口,而派生类通过覆盖这个虚函数,提供了这个接口的特定实现

当创建了派生类 B 的实例,并通过它调用 test() 时,过程如下:

  1. test() 是在基类 A 中定义的,因此它会调用 func 时使用 A 中定义的默认参数,即 1
  2. 由于 func 是虚函数,并且我们实际上是在操作 B 类的对象,因此调用的是 B 类中覆盖的 func 版本。
  3. 被调用的 B 类的 func 输出 “B->”,然后使用传递给它的参数值,此时是基类的默认参数值 1

综上所述,输出是 B->1

要明白一个重要的细节:虚函数的默认参数是静态绑定的,而非动态绑定。也就是说,虚函数的默认参数会在编译时根据函数的静态类型决定,而函数的动态类型会决定在运行时实际调用哪个版本的覆盖函数。这意味着即使 B::func 定义了一个默认值 0,在 A::test 中调用 func() 时,由于它在编译时是视为 A 类型的函数调用,所以使用的是 A::func 定义的默认参数 1。这就是为什么是 B->1 而不是 B->0

4.多继承中的虚函数表

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

在这里插入图片描述
这里有两个虚表指针,继承了两个父类,两个父类的虚表不能合在一起,这里对两张虚表都进行了重写,那么这里func3放在哪个虚表中了呢,是都放呢还是只放一个呢?

我们可以用上面的打印虚表的函数进行打印

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;
}
void test()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);
}

这里第一个虚表已经讲过,找第二个虚表先强转为char,再进行字节相加*
在这里插入图片描述

func3放入第一个虚表中
在这里插入图片描述

4.1菱形继承和菱形虚拟继承

class A
{
public:virtual void func1() { cout << "A::func1" << endl; }int _a;
};
class B : public A
//class B : virtual public A
{
public:virtual void func2() { cout << "B::func2" << endl; }int _b;
};class C : public A
//class C : virtual public A
{
public:virtual void func3() { cout << "C::func3" << endl; }int _c;
};class D : public B, public C
{
public:virtual void func4() { cout << "D::func4" << endl; }int _d;
};int main()
{D d;cout << sizeof(d) << endl;	return 0;
}

在这里插入图片描述
在这里插入图片描述
菱形继承与多继承相似,d里面的虚函数放在B的虚表中

4.2菱形虚拟继承:

class B : virtual public A
class C : virtual public A

在这里插入图片描述
在这里插入图片描述
这里除了虚表指针,还有上篇文章讲解的存储偏移量的虚基表指针

int main()
{D d;cout << sizeof(d) << endl;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

在这里插入图片描述
在这里插入图片描述
菱形虚拟继承,每个类都有一个虚函数,这里ABC都有自己的虚表,但是BC的虚函数不能放在A的虚表中,因为这里虚基类A是共享的

子类有虚函数,继承的父类有虚函数就有虚表,子类对象中就不需要单独建立虚表

在这里插入图片描述
但是菱形虚拟继承就需要自己建立虚表,不能往父类中放

在这里插入图片描述

再看下面的代码:

class A {
public:A(const char* s) { cout << s << endl; }~A() {}
};class B :virtual public A
{
public:B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};class C :virtual public A
{
public:C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};class D :public B, public C
{
public:D(const char* s1, const char* s2, const char* s3, const char* s4):B(s1, s2)  //A  B, C(s1, s3)  //A  C, A(s1)      //A{// Dcout << s4 << endl;}
};
int main() {D* p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}

当创建一个派生类的对象时,构造函数会按照特定的顺序执行,确保所有的基类和成员变量都被正确初始化。在多继承和虚继承的情况下,这个顺序变得更加复杂。上面代码涉及到虚继承,这意味着基类 A 只会有一个实例,即使它被多次包含在派生类层次结构中,在 BC

D(const char* s1, const char* s2, const char* s3, const char* s4):B(s1, s2)  //A  B, C(s1, s3)  //A  C, A(s1)      //A{

D 的构造函数,我们发现它首先调用 B 的构造函数,然后是 C 的构造函数,最后调用 A 的构造函数。然而,在虚继承的情况下,共享的基类(在该例子中是 A)只会被初始化一次,而且是由最底层的派生类(D)来初始化。无论 BC 在其构造函数中怎么尝试初始化 A,它们的尝试都会被忽略

根据上述规则,执行 new D("class A", "class B", "class C", "class D"); 的过程如下:

  1. 首先,最底层的派生类 D 的构造器被调用。
  2. 因为 A 是通过虚继承被 BC 继承的,所以 D 的构造器负责初始化 A。这里将输出 “class A”
  3. 接下来,D 的构造器调用 B 的构造函数。虽然 B 试图先调用 A 的构造函数,但这个调用会被忽略,因为 A 已经被初始化了。然后,B 的构造器继续执行并输出 “class B”
  4. C 的构造函数也会被调用,但同样,其对 A 构造函数的调用被忽略,并且 C 的构造器继续执行,输出 “class C”
  5. 最后,在 D 的构造函数中的代码执行之前,所有基类都已经初始化完成。最后输出 “class D”。
class A
class B
class C
class D

所以,尽量不要写菱形虚拟继承,坑点十分多

5.虚表的存储位置

我们可以通过下面的代码来推断虚表在哪存储的:

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void tese()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Person p;Student s;Person* p3 = &p;Student* p4 = &s;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);
}

在这里插入图片描述
可以推断出存储位置在常量区

本节内容到此结束!!感谢大家阅读!!

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

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

相关文章

什么品牌洗地机最好?怎么选?2024家用洗地机推荐攻略

随着科技的不断发展&#xff0c;家用洗地机已经成为人们家庭清洁任务重非常重要的辅助工具。家用洗地机集吸尘、扫地、拖地等功能于一体&#xff0c;通过高速旋转的滚刷和强力的吸力&#xff0c;将地面上的污渍、细菌和毛发等吸入污水箱&#xff0c;从而达到清洁地面的目的。但…

网络库-POCO介绍

1.简介 POCO C Libraries 提供一套 C 的类库用以开发基于网络的可移植的应用程序&#xff0c;它提供了许多模块&#xff0c;包括网络编程、文件系统访问、线程和并发、数据库访问、XML处理、配置管理、日志记录等功能。Poco库的设计目标是易于使用、高度可定制和可扩展。 包含…

java项目之英语知识应用网站源码(springboot+vue+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的英语知识应用网站。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 英语知识应用网站的主要…

React 第三十章 React 和 Vue 描述页面的区别

面试题&#xff1a;React 和 Vue 是如何描述 UI 界面的&#xff1f;有一些什么样的区别&#xff1f; 标准且浅显的回答&#xff1a; React 中使用的是 JSX&#xff0c;Vue 中使用的是模板来描述界面 前端领域经过长期的发展&#xff0c;目前有两种主流的描述 UI 的方案&#xf…

node和npm版本太高导致项目无法正常安装依赖以及正常运行的解决办法:如何使用nvm对node和npm版本进行切换和管理

1&#xff0c;点击下载 nvm 并且安装 进入nvm的github&#xff1a; GitHub - coreybutler/nvm-windows: A node.js version management utility for Windows. Ironically written in Go. 这里下载发行版&#xff0c;Releases coreybutler/nvm-windows GitHub 找到 这个 nv…

huggingface:利用git克隆目标资源

前言 因为有很多模型资源都被放在了huggingface上&#xff0c;为了下载它们&#xff0c;着实让一个不懂git的人犯了难&#xff0c;绕了很多远路&#xff0c;甚至将不需要解决的问题也都拿上了台面&#xff0c;因此我将在本篇博客中记载一些关于【huggingface】中利用git克隆目标…

【MIT6.S081】Lab7: Multithreading(详细解答版)

实验内容网址:https://xv6.dgs.zone/labs/requirements/lab7.html 本实验的代码分支:https://gitee.com/dragonlalala/xv6-labs-2020/tree/thread2/ Uthread: switching between threads 关键点:线程切换、swtch 思路: 本实验完成的任务为用户级线程系统设计上下文切换机制…

C++笔试强训day22

目录 1.添加字符 2.数组变换 3.装箱问题 常规一维优化&#xff1a; 1.添加字符 链接 因为lenA < lenB < 50&#xff0c;因此可以无脑暴力解题&#xff1a; 遍历所有符合条件的匹配方法&#xff0c;找出最小的不同的数量&#xff0c;即最大的相同的数量 #include &…

棒材直线度测量仪 专为圆形产品研发设计 在线无损检测

棒材直线度测量仪采用了先进的技术&#xff0c;能够实现在线无损检测&#xff0c;为生产过程提供了极大的便利。专为圆形产品设计&#xff0c;它能够精确测量棒材的米直线度及外径、椭圆度尺寸&#xff0c;为质量控制提供可靠的数据支持。 在线直线度测量仪不仅具有出色的性能…

MySQL的msi格式安装

一、下载链接 MySQL :: Download MySQL Installer (Archived Versions) 二、安装步骤 ①选择自定义安装 ②选择要安装的产品 ③安装依赖环境 ④安装 ⑤点击下一步 ⑥配置 ⑦设置密码 ⑧命名 ⑨数据存放路径 ⑩安装配置 ①①配置环境变量 ①②验证 方法一&#xff1a; 方法二…

Docker需要代理下载镜像

systemctl status docker查看docker的状态和配置文件是/usr/lib/systemd/system/docker.service vi /usr/lib/systemd/system/docker.service&#xff0c; 增加如下配置项 [Service] Environment"HTTP_PROXYhttp://proxy.example.com:8080" "HTTPS_PROXYhttp:…

金融业开源软件应用 评估规范

金融业开源软件应用 评估规范 1 范围 本文件规定了金融机构在应用开源软件时的评估要求&#xff0c;对开源软件的引入、维护和退出提出了实现 要求、评估方法和判定准则。 本文件适用于金融机构对应用的开源软件进行评估。 2 规范性引用文件 下列文件中的内容通过文中的规范…

AI 图像生成-环境配置

一、python环境安装 Windows安装Python&#xff08;图解&#xff09; 二、CUDA安装 CUDA安装教程&#xff08;超详细&#xff09;-CSDN博客 三、Git安装 git安装教程&#xff08;详细版本&#xff09;-CSDN博客 四、启动器安装 这里安装的是秋叶aaaki的安装包 【AI绘画…

想要安装Word、Excel、PowerPoint,但却找不到对应软件?

前言 前几天有小伙伴在找Word和Excel软件&#xff0c;但找了半天都没发现怎么安装。 这件事情其实很简单&#xff0c;那就是Word、Excel并不是单独的一个个软件&#xff0c;而是集成在MS Office套件里的。 咱们大部分人常用的办公软件大概是Word、Excel和PowerPoint这三个。还…

Vue3组件库开发项目实战——03封装Button组件/输出vitePress文档

Vue3组件库开发项目实战——01组件开发必备知识导学-CSDN博客 Vue3组件库开发项目实战——02项目搭建&#xff08;配置Eslint/Prettier/Sass/Tailwind CSS/VitePress/Vitest&#xff09;-CSDN博客 在前面两篇博客中&#xff0c;我分别介绍了组件库开发必学知识&#xff0c;以及…

C语言笔记15

指针2 1.数组名的理解 int arr[ 10 ] { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 }; int *p &arr[ 0 ];17391692786 arr是数组名&#xff0c;数组名是首元素地址&#xff0c;&arr[0]就是取出首元素的地址放在指针变量p中。 #include <stdio.h> int main()…

【激活函数--中】激活函数和阶跃函数的可视化及对比

文章目录 一、Python中绘制阶跃函数的图形二、实现和可视化Sigmoid函数2.1 Python实现2.2 可视化Sigmoid函数 三、比较Sigmoid函数与阶跃函数3.1 Sigmoid函数与阶跃函数的差异3.2 Sigmoid函数与阶跃函数的共同点 一、Python中绘制阶跃函数的图形 在Python中实现阶跃函数的代码…

NodeJS编写后端接口

技术栈 1.express&#xff1a;Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建 各种 Web 应用&#xff0c;和丰富的 HTTP 工具&#xff0c;使用 Express 可以快速地搭建一个完整功能的网站。 2.mysql&#xff1a;用于操作MySQL数据库 3.bod…

高校普法|基于SSM+vue的高校普法系统的设计与实现(源码+数据库+文档)

高校普法系统 目录 基于SSM&#xff0b;vue的高校普法系统的设计与实现 一、前言 二、系统设计 三、系统功能设计 1系统功能模块 2管理员功能模块 3律师功能模块 4学生功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获…