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

news2024/11/20 12:24:51

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
	{
		// D
		cout << 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.coloradmin.cn/o/1675163.html

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

相关文章

Docker 部署 Nginx 实现一个极简的 负载均衡

背景: Nginx是异步框架的网页服务器&#xff0c;其常用作反向代理(负载均衡器)。在一般的小项目中, 服务器不多, 如果不考虑使用服务注册与发现, 使用Nginx 可以容易实现负载均衡。 在特此写一个快速入门 Nginx 的技术贴, 使用 Docker 部署 Nginx, 实现一个极简的加权轮询负载均…

信息与未来2017真题笔记

T1. 龟兔赛跑 题目描述 兔子又来找乌龟赛跑啦&#xff01;同样的错误兔子不会犯两次&#xff0c;所以兔子提出赛跑的时候&#xff0c;乌龟就觉得这场比赛很不公平。于是兔子进一步放宽了条件&#xff0c;表示他可以在比赛开始以后先睡 t t t 分钟再开始追乌龟。 乌龟这下没…

瑞_Docker安装教程(超详细图文步骤,含卸载、离线安装)

文章目录 1 CentOS 在线安装 Docker1.1 卸载旧版Docker1.2 安装Docker1.2.1 安装Docker的yum库1.2.2 配置Docker的yum源1.2.3 安装Docker-CE1.2.4 启动和校验&#xff08;开机自启&#xff09;1.2.5 配置镜像加速 1.3 安装 Docker Compose1.4 启动和校验&#xff08;开机自启&a…

神级框架!!不要再封装各种 Util 工具类了【送源码】

这个工具类就比较厉害了&#xff0c;不过我在 Halo 当中用得最多的还是 HtmlUtil.encode&#xff0c;可以将一些字符转化为安全字符&#xff0c;防止 xss 注入和 SQL 注入&#xff0c;比如下面的评论提交。 comment.setCommentAuthor(HtmlUtil.encode(comment.getCommentAutho…

MQTT学习(一)

MQTT是一种与HTTP类似的应用层协议。 在某些物联网应用中&#xff0c;MQTT优于HTTP。 首先&#xff0c;HTTP是用于客户端服务器计算的以文档为中心的请求-响应协议。 HTTP是万维网的基础&#xff0c;但它不是专门为机器之间通信而设计的。 MQTT是一种机器对机器、以数据为中…

栅格地图、障碍物地图与膨胀地图(障碍物地图(三)写一张障碍物地图)

花了不少时间看完了障碍物地图的大致思路&#xff0c;这里简单根据前面的思路来写一个简易版的障碍物地图。 1.订阅一张地图 首先&#xff0c;我们需要一张静态地图作为原始数据&#xff0c;这个我们可以订阅当前的map来获取&#xff1a; void map_test1::MapCallback(const…

一文读懂 Pencil 积分,打开 Pencils Protocol 生态权益大门

近日&#xff0c;Scroll 生态项目 Penpad 近期将品牌全新升级为 Pencils Protocol &#xff0c;在升级后&#xff0c;其从一个 Scroll 生态的原生 LaunchPad 平台进一步拓展为集 Staking、Vault 以及 Shop 等功能于一体的全新生态。全新的品牌升级不仅让 Pencils Protocol 生态…

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

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

nginx配置域名与IP访问服务冲突问题

在最近的一次开发中遇到一个问题&#xff0c;我在云服务器上部署了两个服务&#xff0c;A服务和B服务&#xff0c; A服务在服务器中用的端口是80端口&#xff0c;所以我在浏览器访问的地址就是 B服务在服务器中用的是9818端口&#xff0c;所以我在浏览器访问的是 现在我给B服务…

Network Compression

听课&#xff08;李宏毅老师的&#xff09;笔记&#xff0c;方便梳理框架&#xff0c;以作复习之用。本节课主要讲了Network Compression&#xff0c;包括为什么要压缩&#xff0c;压缩的主要手段&#xff08;pruning,knowledge distillation,parameter quantization,architect…

Linux线程(四) 生产者消费者模型

目录 一、什么是生产者消费者模型 基本概念 优点以及应用场景 二、 基于阻塞队列的生产者消费者模型 三、POSIX信号量 四、基于环形队列的生产消费模型 一、什么是生产者消费者模型 Linux下的生产者消费者模型是一种经典的多线程或多进程编程设计模式&#xff0c;它用于解…

2024年3月 电子学会青少年等级考试机器人理论真题六级

202403 青少年等级考试机器人理论真题六级 第 1 题 下列选项中&#xff0c;属于URL的是&#xff1f;&#xff08; &#xff09; A&#xff1a;192.168.1.10 B&#xff1a;www.baidu.com C&#xff1a;http://www.kpcb.org.cn/h-col-147.html D&#xff1a;fe80::7998:ffc8…

【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 思路: 本实验完成的任务为用户级线程系统设计上下文切换机制…

x264 帧类型代价计算原理:slicetype_frame_cost 函数分析

slicetype_frame_cost 函数 函数功能 这个函数的核心是计算编码一系列帧(从 p0 到p1,以 b 为当前帧)的代价 cost,并根据这个代价 cost来辅助帧类型决策。它考虑了运动搜索的结果、帧间和帧内预测的成本,并且可以并行处理以提高效率。该函数在帧类型决策、MBtree 分析、场…

有一个21年的前端vue项目,死活安不上依赖

在公司开发的时候遇到的一个很玄幻的问题,这个项目是21年开发的,现在我是24年中途二开增加新功能 这个项目经过多人之手,现在已经出现了问题------项目依赖安不上,我能启动完全是因为在23年的时候写这个项目的时候将依赖费九牛二虎之力下载好后打成了压缩包发给另外一个安不上依…

【Java学习笔记10 Java Web 应用——JSP

JSP(Java Script Pages)技术是一种网站开发技术&#xff0c;可以让Web开发人员快速、高效的开发出易于维护的动态网页。使用JSP技术开发的Web应用程序具有跨平台性&#xff0c;不需要修改程序&#xff0c;发布后即可在Windows、Linux等不同的操作系统中运行。 10.1 JSP技术概述…

【JavaWeb】前后端分离SpringBoot项目快速排错指南

1 发起业务请求 打开浏览器开发者工具&#xff0c;同时显示网络&#xff08;Internet&#xff09;和控制台&#xff08;console&#xff09; 接着&#xff0c;清空控制台和网络的内容&#xff0c;如下图 然后&#xff0c;点击你的业务按钮&#xff0c;发起请求。 首先看控制台…

nginx 配置域名SSL证书HTTPS服务

下载 上传根目录 /home/wwwroot/xx.com/ssl 从新执行 添加域名命令 选择添加SSL SSL Certificate file: 填写 完整目录 PEM文件地址 SSL Certificate Key file:填写 完整目录 key文件地址

OmniDrive:具有 3D 感知推理和规划功能的自动驾驶整体 LLM-智体框架

24年5月北理工、Nvidia和华中科大的论文“OmniDrive&#xff1a;A Holistic LLM-Agent Framework for Autonomous Driving with 3D Perception Reasoning and Planning”。 多模态大语言模型&#xff08;MLLMs&#xff09;的进展导致了对基于LLM的自动驾驶的兴趣不断增长&…

QT状态机10-QKeyEventTransition和QMouseEventTransition的使用

1、QMouseEventTransition的使用 首先明白 QMouseEventTransition 继承自 QEventTransition类。 关于QEventTransition类的使用,可参考 QT状态机9-QEventTransition和QSignalTransition的使用 回顾 QT状态机9-QEventTransition和QSignalTransition的使用 中的状态切换代码,如…