【C++】多态详解

💗个人主页💗
⭐个人专栏——C++学习⭐
💫点击关注🤩一起学习C语言💯💫

目录

一、多态概念

二、多态的定义及实现

1. 多态的构成条件

2. 虚函数

 2.1 什么是虚函数

 2.2 虚函数的重写

 2.3 虚函数重写的两个例外

2.3.1 协变

2.3.2 析构函数的重写

3. override 和 final

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

三、抽象类

1. 概念

2. 接口继承和实现继承

四、多态的原理

1. 虚函数表

2. 多态的原理

3. 动态绑定与静态绑定

五、单继承和多继承关系的虚函数表

1. 单继承中的虚函数表

2. 多继承中的虚函数表


一、多态概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。

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

多态的优势在于它增加了代码的灵活性和可扩展性。通过多态,我们可以编写通用的代码,处理多种类型的对象,使得程序更易于理解、扩展和维护。

二、多态的定义及实现

1. 多态的构成条件

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

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

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

class Person 
{
public:virtual void BuyTicket() {cout << "买票全价" << endl; }
};class Student : public Person 
{
public:virtual void BuyTicket() { cout << "买票半价" << endl; }
};
void Func(Person& people)
{people.BuyTicket();
}
int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}

2. 虚函数

 2.1 什么是虚函数

虚函数(virtual function)是面向对象编程中的一个概念,它是用来实现多态的机制之一。

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

虚函数是在基类中声明的一种特殊的成员函数,用关键字virtual进行声明。当在派生类中重写(override)这个函数时,可以使用关键字override来显式标识。

通过将函数声明为虚函数,可以实现动态绑定(dynamic binding),即在运行时根据对象的实际类型来调用相应的函数。这意味着当通过基类指针或引用调用虚函数时,实际调用的是对应的派生类中的函数。

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

2.2 虚函数的重写

虚函数的重写是指在派生类中重新定义(override)基类中已经声明的虚函数。通过重写虚函数,可以在派生类中提供自己的实现,以替代基类中的默认实现。

重写虚函数的规则如下:

  1. 基类中的虚函数必须使用关键字virtual进行声明。
  2. 派生类中的重写函数必须具有相同的名称、参数列表和返回类型。
  3. 派生类中的重写函数必须使用关键字override进行标识,以确保编译器进行正确的检查。
  4. 重写函数可以被声明为虚函数,但这不是必需的。
class Animal 
{
public:virtual void makeSound() {cout << "Animal is making a sound" << endl;}
};class Dog : public Animal 
{
public:void makeSound() override { // 重写虚函数cout << "Dog is barking" << endl;}
};class Cat : public Animal 
{
public:void makeSound() override { // 重写虚函数cout << "Cat is meowing" << endl;}
};int main() 
{Animal* animal = new Animal();Animal* dog = new Dog();Animal* cat = new Cat();animal->makeSound(); // 输出 "Animal is making a sound"dog->makeSound(); // 输出 "Dog is barking"cat->makeSound(); // 输出 "Cat is meowing"delete animal;delete dog;delete cat;return 0;
}

在上述代码中,Animal类中的makeSound()函数被声明为虚函数。派生类Dog和Cat分别重写了这个函数,并提供了自己的实现。

在main()函数中,通过基类指针调用makeSound()函数时,实际上会根据指针指向的对象类型来调用相应的派生类中的函数。这就是虚函数的重写实现多态性的一种方式。

 2.3 虚函数重写的两个例外

2.3.1 协变

虚函数的协变(covariant)是指派生类可以返回比基类更具体的类型。

换句话说,派生类可以重写基类中的虚函数,并返回一个派生类类型的指针或引用,而不仅仅是基类类型的指针或引用。

协变发生在满足以下条件时:

  1. 基类中的虚函数必须返回指针或引用类型。
  2. 派生类中重写的函数可以返回指向派生类的指针或引用类型,该派生类是基类中返回类型的派生类。
class Animal 
{
public:virtual Animal* create() {return new Animal();}
};class Dog : public Animal 
{
public:Dog* create() override { // 协变的重写函数return new Dog();}
};class Cat : public Animal 
{
public:Cat* create() override { // 协变的重写函数return new Cat();}
};int main() 
{Animal* animal = new Animal();Animal* dog = new Dog();Animal* cat = new Cat();Animal* newAnimal = animal->create();Animal* newDog = dog->create();Animal* newCat = cat->create();delete animal;delete dog;delete cat;delete newAnimal;delete newDog;delete newCat;return 0;
}

在上述代码中,Animal类中的create()函数返回一个指向Animal类型的指针。派生类Dog和Cat分别重写了这个函数,并返回指向Dog和Cat类型的指针。

在main()函数中,通过调用基类指针的create()函数,实际上会调用对应派生类中重写的create()函数。由于协变的存在,可以将返回的指针赋值给指向基类的指针,这样便保留了派生类的类型信息。

注意,为了实现协变,返回类型必须是指针或引用,而不能是值类型。否则无法满足派生类返回类型更具体的要求。

2.3.2 析构函数的重写

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

以下是一个示例,展示了虚函数中的析构函数重写是没有意义的:

class Animal 
{
public:Animal() {cout << "Animal constructor called." << endl;}virtual ~Animal() {cout << "Animal destructor called." << endl;}virtual void speak() {cout << "Animal speaks." << endl;}
};class Dog : public Animal 
{
public:Dog() {cout << "Dog constructor called." << endl;}~Dog() {cout << "Dog destructor called." << endl;}void speak() override {cout << "Dog barks." << endl;}
};int main() 
{Animal* animal = new Dog();animal->speak();delete animal;return 0;
}

Animal类定义了一个虚函数speak(),而派生类Dog中重写了该函数。在main()函数中,通过基类指针创建了一个Dog对象,然后调用speak()函数。

从结果可以看出,析构函数按照预期顺序调用,无需在派生类中显式重写。同时,虚函数speak()也可以正确地根据对象的实际类型调用对应的版本。

3. override 和 final

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

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

class Base 
{
public:virtual void foo() final {// 函数实现}
};class Derived : public Base 
{
public:void foo() override { // 编译错误,不能重写被声明为final的虚函数// 函数实现}
};

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

class Base 
{
public:virtual void foo() {// 函数实现}
};class Derived : public Base 
{
public:void foo() override { // 使用override关键字重写基类的虚函数// 函数实现}
};

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

三、抽象类

1. 概念

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

抽象类可以用于实现接口的一致性和多态性。通过基类指针或引用,可以在运行时动态地选择调用派生类的实现。

class Animal 
{
public:virtual void makeSound() = 0; // 纯虚函数void sleep() {cout << "Zzz..." << endl;}
};class Dog : public Animal 
{
public:void makeSound() override {cout << "Woof!" << endl;}
};class Cat : public Animal {
public:void makeSound() override {cout << "Meow!" << endl;}
};int main() 
{Animal* animalPtr = new Dog();animalPtr->makeSound(); // 输出 "Woof!"animalPtr->sleep(); // 输出 "Zzz..."delete animalPtr;animalPtr = new Cat();animalPtr->makeSound(); // 输出 "Meow!"animalPtr->sleep(); // 输出 "Zzz..."delete animalPtr;return 0;
}

 Animal是一个抽象类,包含一个纯虚函数makeSound()和一个普通的成员函数sleep()DogCatAnimal的派生类,必须实现makeSound()函数。通过基类指针,可以调用派生类的实现。

需要注意的是,无法直接实例化抽象类,因为它包含纯虚函数没有具体的实现。派生类必须实现所有纯虚函数,才能被实例化。抽象类可以作为基类来定义其他类,从而实现代码的复用和扩展。

2. 接口继承和实现继承

接口继承和实现继承是面向对象编程中的两种继承方式。

  1. 接口继承(Interface Inheritance)是指一个接口(interface)可以继承自另一个接口,继承接口的子接口会继承父接口的所有方法定义。接口继承主要用于定义一组共享的方法规范,子接口可以继续扩展这些方法规范,同时可以添加自己的方法规范。接口继承可以实现多继承的效果,一个类可以同时实现多个接口。

  2. 实现继承(Implementation Inheritance)是指一个类(class)可以继承自另一个类,继承类会继承父类的属性和方法。实现继承主要用于类之间的层级关系,子类可以继承父类的行为和状态,同时还可以添加自己特有的行为和状态。实现继承是一种单继承的方式,一个类只能直接继承一个父类。

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

四、多态的原理

1. 虚函数表

我们来看下面的代码,想一下sizeof(a)是多少。

class A
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
int main()
{A a;cout << sizeof(a) << endl;return 0;
}

第一眼看去,这不就是4吗,我们来看一下运行结果:

为什么呢?我们打开监视窗口来看一下:

我们发现,a里面不仅仅存在一个_b,还有一个_vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。

 那么派生类中这个表放了些什么呢?我们接着往下分析,针对上面的代码我们做出以下改造

  1. 我们增加一个派生类Derive去继承Base 
  2. Derive中重写Func1 
  3. Base再增加一个虚函数Func2和一个普通函数Func3
class A
{
public:virtual void Func1(){cout << "A::Func1()" << endl;}virtual void Func2(){cout << "A::Func2()" << endl;}void Func3(){cout << "A::Func3()" << endl;}
private:int _a = 1;
};
class B : public A
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _b = 2;
};
int main()
{A a;B b;cout << sizeof(a) << endl;cout << sizeof(b) << endl;return 0;
}

我们再来看下这个代码结果是多少呢:

这又是为什么呢?我们继续来看一下调试窗口:

通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象b中也有一个虚表指针,b对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 基类a对象和派生类b对象虚表是不一样的,这里我们发现Func1完成了重写,所以b的虚表中存的是重写的B::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

总结一下派生类的虚表生成:

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

这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的?

答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。

注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。

2. 多态的原理

上面分析了这个半天了那么多态的原理到底是什么?我们来继续探索。

class A
{
public:virtual void Func1(){cout << "A::Func1()" << endl;}
private:int _a = 1;
};
class B : public A
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _b = 2;
};
int main()
{A a;B b;return 0;
}

 我们会疑惑,为什么多态可以实现指向父类调用父类函数 ,指向子类调用子类函数?

我们来看看虚表里的指针内容:

在监视窗口里我们可以看到a对象和b对象里的内容,我们继续打开内存窗口,取地址a,

我们看到第一个内容就是_vfptr的地址,继续通过_vfptr的地址,我们就能看到虚表里的内容,这时我们又看到了虚函数的地址。

我们来取地址b,发现和上诉同理。

但是,这时我们发现怎么在B类里的A的虚函数地址,和它原本在A类里的地址不一样,这就是虚函数的重写覆盖。

当一个类声明了虚函数时,编译器会为该类生成一个虚函数表。虚函数表是一个存储了虚函数指针的表格,每个包含虚函数的类都会有自己的虚函数表。虚函数表的每个表项存储了相应虚函数的地址。对于派生类,虚函数表中的表项可能会被重写以指向派生类中的虚函数。

当使用基类指针或引用调用虚函数时,编译器会根据对象的实际类型来查找虚函数表,并调用相应的虚函数。这种通过虚函数表来实现动态绑定的机制使得C++中的对象能够在运行时根据实际类型来确定调用的函数,而不是在编译时就确定下来。

3. 动态绑定与静态绑定

动态绑定和静态绑定是两种不同的函数调用机制,它们在编程语言中起到的作用也不同。

  • 静态绑定(静态分派): 静态绑定是在编译时期完成的,也称为早期绑定。在静态绑定中,函数调用的目标函数是在编译时根据对象的静态类型确定的。当调用一个函数时,编译器会根据对象的声明类型来确定要调用的函数,而不管对象的实际类型。

静态绑定适用于非虚函数和静态函数,这些函数的调用目标不会发生改变。在静态绑定中,对象的方法调用是基于声明类型进行的,也就是编译期间根据对象的静态类型来确定调用哪个函数,无法体现多态性。

  • 动态绑定(动态分派): 动态绑定是在运行时期完成的,也称为晚期绑定。在动态绑定中,函数调用的目标函数是根据对象的实际类型来确定的。当调用一个虚函数时,编译器会根据对象的实际类型来确定要调用的函数。

动态绑定适用于虚函数,虚函数表(vtable)是实现动态绑定的一种机制。每个包含虚函数的类都会有一个虚函数表,该表存储了类中虚函数的地址。运行时系统通过指向基类的指针或引用,查找对象实际的类型,并根据其虚函数表来确定要调用的函数。

动态绑定允许程序在运行时根据对象的实际类型来选择调用的函数,实现了多态性。这样可以实现基于对象的具体行为,并提高代码的灵活性。

五、单继承和多继承关系的虚函数表

1. 单继承中的虚函数表

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

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这 两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印 出虚表中的函数。

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(*VFTPTR)();void PrintVFPtr(VFTPTR a[])
{for (size_t i = 0; a[i] != 0; i++){printf("a[%d]:%p\n", i, a[i]);}cout << endl;
}int main()
{Base b;Derive d;PrintVFPtr((VFTPTR*)*(int*)&b);PrintVFPtr((VFTPTR*)*(int*)&d);return 0;
}

 

2. 多继承中的虚函数表

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

观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

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

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

相关文章

实战项目——用Java实现图书管理系统

前言 首先既然是管理系统&#xff0c;那咱们就要实现以下这几个功能了--> 分析 1.首先是用户分为两种&#xff0c;一个是管理员&#xff0c;另一个是普通用户&#xff0c;既如此&#xff0c;可以定义一个用户类&#xff08;user&#xff09;&#xff0c;在定义管理员类&am…

哈哈看到这条消息感觉就像是打开了窗户

在这个信息爆炸的时代&#xff0c;每一条动态可能成为我们情绪的小小触发器。今天&#xff0c;当我无意间滑过那条由杜海涛亲自发布的“自曝式”消息时&#xff0c;不禁心头一颤——如果这是我的另一半&#xff0c;哎呀&#xff0c;那画面&#xff0c;简直比烧烤摊还要“热辣”…

多微信运营管理方案

微信作为一款社交通讯软件&#xff0c;已经成为人们日常生活中不可缺少的工具。不仅个人&#xff0c;很多企业都用微信来联系客户、维护客户和营销&#xff0c;这自然而然就会有很多微信账号、手机也多&#xff0c;那管理起来就会带来很多的不便&#xff0c;而多微信私域管理系…

K8s的基本使用和认识

目录 介绍 控制端 Node(节点) 控制端与节点的关系图 基本使用 创建和运行资源 查找和参看资源 修改和删除资源 介绍 控制端 api-server(api)是集群的核心是k8s中最重要的组件,因为它是实现声明式api的关键 kubernetes api-server的核心功能是提供了Kubernetes各类资…

7.6、指针和数组

代码 #include <iostream> using namespace std;int main() {//指针和数组//利用指针访问数组中的元素int arr[10] { 1,2,3,4,5,6,7,8,9,10 };cout << "第一个元素为&#xff1a;" << arr[0] << endl;int * p arr;//arr就是数组首地址co…

Python爬取国家医保平台公开数据

国家医保服务平台数据爬取python爬虫数据爬取医疗公开数据 定点医疗机构查询定点零售药店查询医保机构查询药品分类与代码查询 等等&#xff0c;数据都能爬 接口地址&#xff1a;/ebus/fuwu/api/nthl/api/CommQuery/queryFixedHospital 签名参数&#xff1a;signData {dat…

中国国产AI芯片的崛起

一、CUDA的垄断 当讨论半导体行业面临的挑战时&#xff0c;你首先想到的是什么&#xff1f;光刻机&#xff1f;3纳米或者5纳米技术&#xff1f;我们无法生产的完美方形芯片&#xff1f;是的&#xff0c;但也不完全是。 人们经常把半导体芯片归类为硬件产业&#xff0c;但实际上…

Vue3的模板语法插值表达式用法

在template中输入“5 3” &#xff0c;是没有运算能力的&#xff0c;只会把字符直接显示出来&#xff0c;代码如下&#xff1a; <template><view>这是demo</view><view>5 3</view><navigator open-type"navigateBack"><vi…

Opencv+python模板匹配

我们经常玩匹配图像或者找相似&#xff0c;opencv可以很好实现这个简单的小功能。 模板是被查找目标的图像&#xff0c;查找模板在原始图像中的哪个位置的过程就叫模板匹配。OpenCV提供的matchTemplate()方法就是模板匹配方法&#xff0c;其语法如下&#xff1a; result cv2.…

SerialportToTcp①

窗体 效果&#xff1a;串口和网口旁边的是panel当客户端或者服务器发送消息的时候会闪烁&#xff0c;下面的的textbox当接收到接受或者发送的数据会增加数量&#xff0c;心跳机制单选框可以开关&#xff0c;可设置心跳间隔和内容&#xff0c;重置按钮重置串口数据&#xff0c;…

轻度图像处理工具,匹敌photoshop

一、简介 1、一款功能强大的在线图片编辑工具,用户可以将其安装为渐进式网页应用(PWA)。它提供了与 Photoshop 相似的核心功能,能够满足大多数图像编辑需求,非常适合那些不愿或无法安装 Photoshop 的用户。即使使用免费版本,用户也能享受所有功能,是轻度图像处理的理想选…

python(基础语法,pandas,numpy,正则表达式,数据预处理)

python学习推荐网址&#xff1a; 白月黑羽 一、语法基础 目标&#xff1a; • list、tuple、set、dict的基本用法 • 内置函数 len&#xff08;&#xff09;&#xff0c; eval&#xff08;&#xff09;&#xff0c;range&#xff08;&#xff09;&#xff0c;sort&#xff08;…

Django 一对一关系

作用&#xff1a; 两个数据库表建立外键关系当外键表的数据被删除时&#xff0c;主表的数据也会一并删除。 1&#xff0c;添加表模型 Test/app8/views.pyfrom django.db import modelsclass User(models.Model):username models.CharField(max_length50, uniqueTrue)email …

【C语言】学生管理系统:完整模拟与实现

&#x1f308;个人主页&#xff1a;是店小二呀 &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;C笔记专栏&#xff1a; C笔记 &#x1f308;喜欢的诗句:无人扶我青云志 我自踏雪至山巅 &#x1f525;引言 本篇文章为修改了在校期间实训报告&#xff0c;使用C…

【网络架构】lvs集群

目录 一、集群与分布式 1.1 集群介绍 1.2 分布式系统 1.3 集群设计原则 二、LVS 2.1 lvs工作原理 2.2 lvs集群体系架构 ​编辑 2.3 lvs功能及组织架构 2.4 lvs集群类型中术语 三、LVS工作模式和命令 3.1 lvs集群的工作模式 3.1.1 lvs的nat模式 3.1.2 lvs的dr模式 …

为什么有些人思考得多,决策反而不好?避免过度拟合的终极指南:决策高手的秘密:灰度认知,黑白决策

在决策过程中&#xff0c;过度关注细节可能导致决策效果不佳&#xff0c;这被称为“过度拟合”。为了避免这种情况&#xff0c;我们需要进行“灰度认知&#xff0c;黑白决策”&#xff0c;即接受不确定性&#xff0c;关注整体趋势&#xff0c;设定明确目标&#xff0c;简化选择…

【JD-GUI】MacOS 中使用Java反编译工具JD-GUI

希望文章能给到你启发和灵感&#xff5e; 如果觉得文章对你有帮助的话&#xff0c;点赞 关注 收藏 支持一下博主吧&#xff5e; 阅读指南 开篇说明概念理解一、基础环境说明1.1 硬件环境1.2 软件环境 二、下载与安装2.1 选择对应版本2.2 解压运行排除异常&#xff1a;2.3 关于…

Geotools系列说明之LineString仿高德航路截取说明

需求分析 我们在做webgl的时候经常会遇到这样的需求&#xff0c;计算给定航路的拥堵情况&#xff0c;不同的拥堵显示不同的颜色&#xff0c;航路截取计算等等。基于这类问题统一都可以使用LineString进行处理 实现思路 如上图所示&#xff0c;航路是几个关键的点然后练成线&a…

05 docker 镜像

目录 1. 镜像 2. 联合文件系统 3. docker镜像加载原理 4. 镜像分层 镜像分层的优势 5. 容器层 1. 镜像 镜像是一种轻量级、可执行的独立软件包&#xff0c;它包含运行某个软件所需的所有内容&#xff0c;我们把应用程序和配置依赖打包好行程一个可交付的运行环境&#xf…

监控平台zabbix对接grafana

目录 1.安装grafana并启动 2.浏览器访问 3.导入zabbix数据&#xff0c;对接grafana 4.如何导入模板 5.使用zabbix监控nginx并发量连接数 5.1 修改nginx配置 5.2 编写监控数据脚本 5.3 设置键值 5.4 在zabbix web端完成自定义监控项 5.5 连接到grafana 以上一篇博客&l…