当前位置: 首页 > news >正文

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

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

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

相关文章:

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

&#x1f525;个人主页&#xff1a;Quitecoder &#x1f525;专栏&#xff1a;c笔记仓 朋友们大家好&#xff0c;通过本篇文章&#xff0c;来详细理解多态的内容 目录 1.多态的定义及实现1.1多态的构成条件1.2虚函数的重写1.3 C11 override 和 final1.4重载、覆盖(重写)、隐藏…...

分享四种免费获取SSL的方式

SSL证书目前需要部署安装的网站很多&#xff0c;主要还是基于国内目前对证书的需求度在不断的升高&#xff0c;网站多了、服务器多了之后。网络安全问题就成为了大家不得不面对的一个重要的问题了。SSL证书的作用有很多&#xff0c;这里就不一一详述了&#xff0c;本期作品主要…...

2024.5.14晚训题解

非线性结构没懂的同学多去看看课程回放。 A题Overall Winner 题解 很基础的题目&#xff0c;输入字符串&#xff0c;用计数器去统计一下就好了。 因为赢的次数一样的时候优先判断前面的那个人赢&#xff0c;所以说两个人赢的次数相同的时候我们不必 去记录胜者。 #include<…...

jQuery的选择器与自带函数详解

在前端开发中&#xff0c;jQuery是一个广泛使用的JavaScript库&#xff0c;它极大地简化了HTML文档遍历、事件处理、动画以及AJAX交互等操作。本文将通过一个示例页面&#xff0c;详细介绍jQuery的选择器和一些常用的自带函数。 示例代码优化 首先&#xff0c;我们来优化和完…...

Next.js与SSR:构建高性能服务器渲染应用

1. 创建项目 通过create-next-app脚手架创建一个新的Next.js项目&#xff1a; npx create-next-app my-app cd my-app2. 自动SSR 在Next.js中&#xff0c;每个.js或.jsx文件的组件都会被自动处理为SSR页面。例如&#xff0c;创建一个pages/index.js文件&#xff1a; // pag…...

什么是MVC?什么是SpringMVC?什么是三层架构?

文章目录 应用分层什么是MVC?什么是 SpringMVC&#xff1f;三层架构三层架构和MVC的关系 应用分层 在讲解什么是MVC之前&#xff0c;先来理解一下什么是应用分层。 应用分层是一种软件开发设计思想&#xff0c;将应用程序划分成N个层次&#xff0c;每个层次都分别负责自己的…...

基于springboot+vue+Mysql的在线答疑系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…...

ssl证书免费申请指南

同学们可以私信我加入学习群&#xff01; 正文开始 前言一、购买证书二、创建证书三、 验证证书等待出现如下页面&#xff0c;说明申请成功&#xff1a; ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/6b6c1dd11d4c467687318552da7cdbb2.png) 总结 前言 今天为大…...

Java构造方法详解

在Java方法内部定义一个局部变量时&#xff0c;必须要初始化&#xff0c;否则就会编译失败&#xff0c;如下&#xff1a; 要让上述代码通过编译&#xff0c;只需在使用a之前给a赋一个初始值即可 如果是对象&#xff1a;下面用一个日期类演示 我们没有给年月日赋值&#xff0c;…...

Spring WebFlux:响应式编程

在软件开发领域&#xff0c;随着互联网应用的规模和复杂性不断增加&#xff0c;传统的编程模型逐渐暴露出一些局限性&#xff0c;尤其是在面对高并发、大规模数据流处理等场景时。为了应对这些挑战&#xff0c;响应式编程&#xff08;Reactive Programming&#xff09;应运而生…...

uniapp、web网页跨站数据交互及通讯

来来来&#xff0c;说说你的创作灵感&#xff01;这就跟吃饭睡觉一样&#xff0c;饿了就找吃的&#xff0c;渴了就倒水张口灌。 最近一个多月实在是忙的没再更新日志&#xff0c;好多粉丝私信说之前的创作于他们而言非常有用&#xff01;受益菲浅&#xff0c;这里非常感谢粉丝…...

2024-05-10 Ubuntu上面使用libyuv,用于转换、缩放、旋转和其他操作YUV图像数据,测试实例使用I420ToRGB24

一、简介&#xff1a;libyuv 最初是由Google开发的&#xff0c;主要是为了支持WebRTC项目中的视频处理需求。用于处理YUV格式图像数据的开源库。它提供了一系列的函数&#xff0c;用于转换、缩放、旋转和其他操作YUV图像数据。 二、执行下面的命令下载和安装libyuv。 git clo…...

怎么给视频加水印?2招轻松搞定

在数字媒体时代&#xff0c;视频水印作为一种有效的版权保护手段&#xff0c;被广泛应用于各种场景。给视频添加水印不仅可以防止内容被恶意盗用&#xff0c;还能增加视频的辨识度&#xff0c;提升品牌形象。本文将为您介绍2种简单易行的方法&#xff0c;教您怎么给视频加水印&…...

SpringBootWeb 篇-深入了解请求响应(服务端接收不同类型的请求参数的方式)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 请求响应概述 1.1 简单参数 1.2 实体参数 2.3 数组集合参数 2.4 日期参数 2.5 json 参数 2.6 路径参数 3.0 完整代码 1.0 请求响应概述 当客户端发送不同的请求参…...

实验十 智能手机互联网程序设计(微信程序方向)实验报告

实验目的和要求 完成以下页面设计。 二、实验步骤与结果&#xff08;给出对应的代码或运行结果截图&#xff09; Wxml <view class"container"> <view class"header"> <view class"logo"…...

Python图形复刻——绘制母亲节花束

各位小伙伴&#xff0c;好久不见&#xff0c;今天学习用Python绘制花束。 有一种爱&#xff0c;不求回报&#xff0c;有一种情&#xff0c;无私奉献&#xff0c;这就是母爱。祝天下妈妈节日快乐&#xff0c;幸福永远&#xff01; 图形展示&#xff1a; 代码展示&#xff1a; …...

【算法优选】 动态规划之子数组、子串系列——壹

文章目录 &#x1f38b;前言&#x1f38b;最大子数组和&#x1f6a9;题目描述&#x1f6a9;算法思路&#x1f6a9;代码实现 &#x1f334;环形子数组的最大和&#x1f6a9;题目描述&#x1f6a9;算法思路&#xff1a;&#x1f6a9;代码实现 &#x1f332;乘积最大子数组&#x…...

PXE+Kickstart无人值守安装安装Centos7.9

文章目录 一、什么是PXE1、简介2、工作模式3、工作流程 二、什么是Kickstart1、简介2、触发方式 三、无人值守安装系统工作流程四、实验部署1、环境准备2、服务端&#xff1a;关闭防火墙和selinux3、添加一张仅主机的网卡4、配置仅主机的网卡4.1、修改网络连接名4.2、配IP地址4…...

C语言实现通讯录,包括增删改查以及动态开辟内存,写入文件等功能

文章目录 前言一、注意二、源码1. test.c源文件2. contact.h头文件3. contact.c源文件 总结 前言 C语言实现通讯录&#xff0c;包括增删改查以及动态开辟内存&#xff0c;写入文件等功能 一、注意 在通讯录菜单栏使用枚举定义PeoInfo类型时&#xff0c;每个结构体类型的成员…...

flowable多对并发网关跳转的分析

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 http://218.75.87.38:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; h…...

Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!

一、引言 在数据驱动的背景下&#xff0c;知识图谱凭借其高效的信息组织能力&#xff0c;正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合&#xff0c;探讨知识图谱开发的实现细节&#xff0c;帮助读者掌握该技术栈在实际项目中的落地方法。 …...

力扣热题100 k个一组反转链表题解

题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...

Elastic 获得 AWS 教育 ISV 合作伙伴资质,进一步增强教育解决方案产品组合

作者&#xff1a;来自 Elastic Udayasimha Theepireddy (Uday), Brian Bergholm, Marianna Jonsdottir 通过搜索 AI 和云创新推动教育领域的数字化转型。 我们非常高兴地宣布&#xff0c;Elastic 已获得 AWS 教育 ISV 合作伙伴资质。这一重要认证表明&#xff0c;Elastic 作为 …...

DAY 45 超大力王爱学Python

来自超大力王的友情提示&#xff1a;在用tensordoard的时候一定一定要用绝对位置&#xff0c;例如&#xff1a;tensorboard --logdir"D:\代码\archive (1)\runs\cifar10_mlp_experiment_2" 不然读取不了数据 知识点回顾&#xff1a; tensorboard的发展历史和原理tens…...

Selenium 查找页面元素的方式

Selenium 查找页面元素的方式 Selenium 提供了多种方法来查找网页中的元素&#xff0c;以下是主要的定位方式&#xff1a; 基本定位方式 通过ID定位 driver.find_element(By.ID, "element_id")通过Name定位 driver.find_element(By.NAME, "element_name"…...

开疆智能Ethernet/IP转Modbus网关连接鸣志步进电机驱动器配置案例

在工业自动化控制系统中&#xff0c;常常会遇到不同品牌和通信协议的设备需要协同工作的情况。本案例中&#xff0c;客户现场采用了 罗克韦尔PLC&#xff0c;但需要控制的变频器仅支持 ModbusRTU 协议。为了实现PLC 对变频器的有效控制与监控&#xff0c;引入了开疆智能Etherne…...

Python打卡训练营学习记录Day49

知识点回顾&#xff1a; 通道注意力模块复习空间注意力模块CBAM的定义 作业&#xff1a;尝试对今天的模型检查参数数目&#xff0c;并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...

前端异步编程全场景解读

前端异步编程是现代Web开发的核心&#xff0c;它解决了浏览器单线程执行带来的UI阻塞问题。以下从多个维度进行深度解析&#xff1a; 一、异步编程的核心概念 JavaScript的执行环境是单线程的&#xff0c;这意味着在同一时间只能执行一个任务。为了不阻塞主线程&#xff0c;J…...

【Redis】Redis 的持久化策略

目录 一、RDB 定期备份 1.2 触发方式 1.2.1 手动触发 1.2.2.1 自动触发 RDB 持久化机制的场景 1.2.2.2 检查是否触发 1.2.2.3 线上运维配置 1.3 检索工具 1.4 RDB 备份实现原理 1.5 禁用 RDB 快照 1.6 RDB 优缺点分析 二、AOF 实时备份 2.1 配置文件解析 2.2 开启…...

AI短视频创富营

课程内容&#xff1a; 相关资料 【第一章】前期准备 001.【涨粉技巧】新账号如何快速涨粉?_ev(1).mp4 002.【带贷权限】如何开通账号带贷权限?(1).mp4 003.【费用缴纳】如何缴纳账号保证金?_ev(1).mp4 004.【账号检测】如何检测账号是否限流?(1).mp4 005.【风险规避…...