【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.mzph.cn/web/12121.shtml

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

相关文章

鸿蒙内核源码分析 (内核启动篇) | 从汇编到 main ()

这应该是系列篇最难写的一篇&#xff0c;全是汇编代码&#xff0c;需大量的底层知识&#xff0c;涉及协处理器&#xff0c;内核镜像重定位&#xff0c;创建内核映射表&#xff0c;初始化 CPU 模式栈&#xff0c;热启动&#xff0c;到最后熟悉的 main() 。 内核入口 在链接文件…

在k8s中安装Grafana并对接Prometheus,实现k8s集群监控数据的展示

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《Grafana&#xff1a;让数据说话的魔术师》 &#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、引言 1、Grafana简介 2、Grafana的重要性与影响力 …

强化训练:day9(添加逗号、跳台阶、扑克牌顺子)

文章目录 前言1. 添加逗号1.1 题目描述2.2 解题思路2.3 代码实现 2. 跳台阶2.1 题目描述2.2 解题思路2.3 代码实现 3. 扑克牌顺子3.1 题目描述3.2 解题思路3.3 代码实现 总结 前言 1. 添加逗号   2. 跳台阶   3. 扑克牌顺子 1. 添加逗号 1.1 题目描述 2.2 解题思路 我的写…

STM32学习和实践笔记(28):printf重定向实验

1.printf重定向简介 在C语言中printf函数里&#xff0c;默认输出设备是显示器&#xff0c;如果想要用这个函数将输出结果到串口或者LCD上显示&#xff0c;就必须重定义标准库函数里中printf函数调用的与输出设备相关的函数。 比如要使用printf输出到串口&#xff0c;需要先将f…

linux 任务管理(临时任务定时任务) 实验

目录 任务管理临时任务管理周期任务管理 任务管理 临时任务管理 执行如下命令添加单次任务&#xff0c;输入完成后按组合键Ctrl-D。 [rootopenEuler ~]# at now5min warning: commands will be executed using /bin/sh at> echo "aaa" >> /tmp/at.log at&g…

J-STAGE (日本电子科学与技术信息集成)数据库介绍及文献下载

J-STAGE (日本电子科学与技术信息集成)是日本学术出版物的平台。它由日本科学技术振兴机构&#xff08;JST&#xff09;开发和管理。该系统不仅包括期刊&#xff0c;还有论文集&#xff0c;研究报告、技术报告等。文献多为英文&#xff0c;少数为日文。目前网站上所发布的内容来…

使用Vue调用ColaAI Plus大模型,实现聊天(简陋版)

首先去百度文心注册申请自己的api 官网地址&#xff1a;LuckyCola 注册点开个人中心 查看这个文档自己申请一个ColaAI Plus定制增强大模型API | LuckyColahttps://luckycola.com.cn/public/docs/shares/api/colaAi.html来到vue的页面 写个样式 <template><Header …

ICode国际青少年编程竞赛- Python-5级训练场-综合练习6

ICode国际青少年编程竞赛- Python-5级训练场-综合练习6 1、 for i in range(3):Dev.step(2 * (i 1))Dev.turnLeft()while Flyer[2 - i].disappear():wait()Dev.step(2 * (i 1))Dev.turnRight()while Dev.x ! Item[i].x:wait()2、 for i in range(3):Dev.step(2 * i 1)while …

用Python的pynput库成为按键记录高手

哈喽&#xff0c;大家好&#xff0c;我是木头左&#xff01; 揭秘键盘输入&#xff1a;pynput库的基本介绍 无论是为了安全审计、数据分析还是创建热键操作&#xff0c;能够记录和处理键盘事件都显得尤为关键。这就是pynput库发挥作用的地方。pynput是一个Python库&#xff0c…

Java 对象序列化

序列化&#xff1a;把对象转化为可传输的字节序列过程称为序列化。 反序列化&#xff1a;把字节序列还原为对象的过程称为反序列化 序列化的作用是方便存储和传输&#xff0c;细节可参考如下文章&#xff1a; 序列化理解起来很简单 - 知乎序列化的定义 序列化&#xff1a;把对…

echarts map地图添加背景图

给map地图添加了一个阴影3d的效果&#xff0c;添加一张背景图&#xff0c;给人感觉有3d的效果 具体配置如下&#xff1a; html代码模块&#xff1a; <div class"echart_img" style"position: fixed; visibility: hidden;"></div><div id&q…

Autoware内容学习与初步探索(一)

0. 简介 之前作者主要是基于ROS2&#xff0c;CyberRT还有AutoSar等中间件完成搭建的。有一说一&#xff0c;这种从头开发当然有从头开发的好处&#xff0c;但是如果说绝大多数的公司还是基于现成的Apollo以及Autoware来完成的。这些现成的框架中也有很多非常好的方法。目前作者…

【Java的抽象类和接口】

1. 抽象类 1.1 抽象类概念 在面向对象的概念中&#xff0c;所有的对象都是通过类来描绘的&#xff0c;但是反过来&#xff0c;并不是所有的类都是用来描绘对象的&#xff0c;如果 一个类中没有包含足够的信息来描绘一个具体的对象&#xff0c;这样的类就是抽象类。 以上代码中…

Leaflet系列——【一】初识Leaflet与Leaflet视图操作

初识Leaflet&#xff08;vue3 &#xff09; 前言&#xff1a;当你熟悉了openlayer、mapbox、cesium等一些GIS框架之后&#xff0c;对于我们开发来说其实他们的本质就是往瓦片上面叠加图层、【点、线、面、瓦片、geoJson、热力图、图片、svg等等】都是一层层的Layer图层&#xf…

Ceph集群扩容及数据再均衡原理分析

用户文件在Ceph RADOS中存储、定位过程大概包括&#xff1a;用户文件切割成对象、对象映射到PG、PG分组PGP、PG映射到OSD。这些过程中&#xff0c;可能涉及了大量概念和变量&#xff0c;而其实它们大部分是通过HASH、CRUSH等算法计算出来的&#xff0c;初始参数可能也就只有这么…

sql实践

1.从excel导入数据 在excel导入数据时要先在数据库中创建对应的数据库表 CREATE TABLE your_table_name (crawl_datetime DATE,url CHAR(255),company_name CHAR(255),company_size CHAR(255),company_type CHAR(255),job_type CHAR(255),job_name CHAR(255),edu CHAR(255),e…

暗区突围TWITCH掉宝关联帐号不了 无法关联帐号 关联不上

Twitch&#xff0c;作为全球知名的游戏直播平台&#xff0c;常常携手热门游戏如《暗区突围》举办互动活动&#xff0c;为玩家带来独特的参与体验。在这个过程中&#xff0c;“绑定关联”成为了连接直播观众与游戏世界的桥梁。简单来说&#xff0c;Twitch绑定关联《暗区突围》指…

leetcode——链表的中间节点

876. 链表的中间结点 - 力扣&#xff08;LeetCode&#xff09; 链表的中间节点是一个简单的链表OJ。我们要返回中间节点有两种情况&#xff1a;节点数为奇数和节点数是偶数。如果是奇数则直接返回中间节点&#xff0c;如果是偶数则返回第二个中间节点。 这道题的解题思路是&a…

OpenAI 发布了免费的 GPT-4o,国内大模型还有哪些机会?

大家好&#xff0c;我是程序员X小鹿&#xff0c;前互联网大厂程序员&#xff0c;自由职业2年&#xff0c;也一名 AIGC 爱好者&#xff0c;持续分享更多前沿的「AI 工具」和「AI副业玩法」&#xff0c;欢迎一起交流~ 这是今天在某乎看到一个问题&#xff1a;OpenAI 发完 GPT-4o&…

关闭 Visual Studio Code 项目中 的eslint的语法校验 lintOnSave: false;; 项目运行起来之后 自动打开浏览器 端口

1、在 vue.config.js 配置 一个属性 lintOnSave: false 2、配置两个属性 open: true, // 自动打开浏览器 port: 3000 // 端口 port 端口号根据自己的项目实际开发来 配置