【C++】从零开始认识多态

在这里插入图片描述

送给大家一句话:
一个犹豫不决的灵魂,奋起抗击无穷的忧患,而内心又矛盾重重,真实生活就是如此。 ​​​​ – 詹姆斯・乔伊斯 《尤利西斯》

_φ(* ̄ω ̄)ノ_φ(* ̄ω ̄)ノ_φ(* ̄ω ̄)ノ
_φ(* ̄ω ̄)ノ_φ(* ̄ω ̄)ノ_φ(* ̄ω ̄)ノ
_φ(* ̄ω ̄)ノ_φ(* ̄ω ̄)ノ_φ(* ̄ω ̄)ノ


从零开始认识多态

  • 1 前言
  • 2 什么是多态
  • 3 多态的构成
    • 3.1 协变
    • 3.2 析构函数的重写
    • 3.3 语法细节
    • 3.4 C++11 override 和 final
    • 3.5 重写(覆盖) - 重载 - 重定义(隐藏)
  • 4 多态的底层实现
    • 4.1 底层实现
    • 4.2 验证虚表
  • 5 抽象类
  • 6 多继承中的多态
    • 一般的多继承
    • 菱形继承和菱形虚拟继承
  • Thanks♪(・ω・)ノ谢谢阅读!!!
  • 下一篇文章见!!!

1 前言

面向对象技术(oop)的核心思想就是封装,继承和多态。通过之前的学习,我们了解了什么是封装,什么是继承。

封装就是对将一些属性装载到一个类对象中,不受外界的影响,比如:洗衣机就是对洗衣服功能,甩干功能,漂洗功能等的封装,其功能不会受到外界的微波炉影响。

继承就是可以将类对象进行继承,派生类会继承基类的功能与属性,类似父与子的关系。比如水果和苹果,苹果就有水果的特性。

接下来我们就来了解学习多态!

2 什么是多态

多态是面向对象技术(OOP)的核心思想,我们把具有继承关系的多个类型称为多态类型,通俗来讲:就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

举个例子:就拿刚刚结束的五一假期买票热为例,买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。同样一个行为在不同的对象上就有不同的显现。

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。


#include<iostream>

using namespace std;

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 p;
	Student s;
	//同一个函数对不同对象有不同效果
	Func(p);
	Func(s);

	return 0;
}

比如Student继承了Person。Person对象买票全价,Student对象买票半价。我们运行看看:
在这里插入图片描述

  • 多态调用:运行时,到指定对象的虚表中找虚函数来调用(指向基类调用基类的虚函数,指向子类调用子类的虚函数)
  • 普通调用:编译时,调用对象是哪个类型,就调用它的函数。

乍一看还挺复杂,接下来我们就来了解多态的构成。

3 多态的构成

继承的情况下才有虚函数,才有多态!!!

多态构成的条件

  1. 必须通过基类的指针或者引用调用虚函数(virtual修饰的类成员函数)
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(父子虚函数要求三同)
    • 虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数

看起来很是简单,当时其实有很多的坑!!!一不小心就会掉进去。

3.1 协变

上面我们说了多态的条件:父子虚函数要求三同。但是却有这样一个特殊情况:协变!
协变:派生类重写基类虚函数时,与基类虚函数返回值类型不同

  1. 基类虚函数返回基类对象的指针或者引用
  2. 派生类虚函数返回派生类对象的指针或者引用

这样的情况称为协变。

#include<iostream>

using namespace std;

class A {};
class B : public A {};
//这里明显返回类型不同但是结构仍然正常
class Person 
{
public:
	virtual A* BuyTicket() { cout << "买票->全价" << endl; return nullptr; }
};

class Student : public Person
{
public:
	virtual B* BuyTicket() { cout << "买票->半价" << endl; return nullptr; }
};

很明显派生类与基类的返回值不同(注意一定是:基类返回“基类”,派生类返回“派生类”):
但是结果确实正常的,依然构成多态,这样的情况就称为协变!!!

3.2 析构函数的重写

析构函数在编译阶段都会转换成:destructor(),所以表面析构函数名字不同,但是实质上是一致的。这样就会构成多态。

来看正常情况下的析构:

 
#include<iostream>
using namespace std;

class Person
{
public:
	~Person() { cout << "~Person()" << endl; }
};

class Student : public Person
{
public:
	~Student() { cout << "~Student()" << endl; }
};
int main()
{
	Person p;
	Student s;

	return 0;
}

这样会正常的调用析构函数(子类析构会自动调用父类析构->先子后父):
在这里插入图片描述
再来看:

int main()
{
	//Person p;
	//Student s;
	//基类可以指向基类 也可以指向派生类的基类部分
	Person* p1 = new Person ;
	//通过切片来指向对应内容
	Person* p2 = new Student;

	delete p1;
	delete p2;

	return 0;
}

如果是这样呢?
在这里插入图片描述
这样调用的析构不对啊!Student对象没有调用自身的析构函数,而是调用Person的,为什么会出现这样的现象呢???

这样就可能会引起一个十分严重的问题:内存泄漏

#include<iostream>

using namespace std;

class Person
{
public:
	~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
	Student() { int* a = new int[100000000]; }
	~Student() { cout << "~Student()" << endl; }
};
int main()
{
	for(int i = 0; i< 100000 ; i++)
	{
		Person* p2 = new Student;
		delete p2;
	}
	return 0;
}

如果我们在Student中申请一个空间,而析构的时候却不能调用其析构函数俩把申请的空间free这样就导致了内存泄漏!!!
在这里插入图片描述
这就十分危险了!!!
而我们希望的是指向谁就调用谁的析构:指向基类调用基类析构,指向派生类调用派生类析构。
那我们怎么做到呢 ----> 当然就是多态了!!!
那我们来看看现在满不满足多态的条件:

  1. 必须通过基类的指针或者引用调用虚函数(virtual修饰的类成员函数)
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(父子虚函数要求三同)

在编译的时候,析构函数都会变成destructor,这样满足三同!构成重写
那么我们就只需要将析构函数变为虚函数就可以了:

class Person
{
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

来运行看看:
在这里插入图片描述
老铁 OK了!!!应该释放的空间全都释放了!!!

所以建议析构函数设置为虚函数,避免出现上述的情况。

3.3 语法细节

  1. 派生类(基类必须写)的对应函数可以不写virtual(这个语法点非常奇怪!建议写上virtual
  2. “重写”的本质是重新写函数的实现,函数声明(包括缺省参数的值)与基类一致

来看一道面试题:
以下程序输出结果是什么()

#include<iostream>

using namespace std;

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 为什么呢?

  1. 首先:
    • A类与B类构成继承关系
    • func函数是虚函数(B类是派生类,可以不写virtual),并且AB 中满足三同。构成多态。
    • test函数的参数是基类指针(A* this 成员函数的默认参数),满足多态条件。
  2. 然后:
    • 主函数中调用test函数,因为B是子类,没有test函数,所以会在父类A中寻找。
    • test函数调用 func函数,参数this指向的是B类(指向谁调用谁),所以就会调用B类的func函数B->
    • 重写的本质是对函数的实现进行重写,函数的结构部分(包括参数,缺省值,函数名,返回值等)与基类一致。所以是 1

所以就可以判断是B选项。
当然实际中不能这么写代码奥!!!会有生命危险(Doge)

3.4 C++11 override 和 final

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

  1. final:
    • 修饰类(最终类),表示该类不能被继承。(C++98直接粗暴使用private来做到不能继承)
      class car final { };
    • 修饰虚函数,表示该虚函数不能再被继承
      virtual void func() final { }
  2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:
	virtual void Drive() {}
};

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

3.5 重写(覆盖) - 重载 - 重定义(隐藏)

我们来区分一下这三个类似的概念:

  1. 重载 :
    • 两个函数作用在同一作用域
    • 函数名相同,参数不同
  2. 重写(覆盖):
    • 两个函数分别在基类作用域好派生类作用域
    • 函数名、参数、返回值都一样(协变例外)
    • 两个函数必须是虚函数
  3. 重定义:
    • 两个函数分别在基类作用域好派生类作用域
    • 仅仅函数名相同
    • 两个基类和派生类的同名函数不是重写就是重定义

在这里插入图片描述
重定义包含重写!!!

4 多态的底层实现

4.1 底层实现

首先我们来看一下具有多态属性的类的大小:


#include<iostream>
using namespace std;

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

int main(int argc, char* argv[])
{
	cout << sizeof(Base) << endl;
	return 0;
}

Base的大小在x86环境下是12字节。这十二字节是怎么组成的呢?
首先类里面有一个虚函数表指针_vfptr
在这里插入图片描述
只要有虚函数就会有虚表指针,这个是实现多态的关键!!!
我们来探索一下:
通过VS的调试,我们可以发现:
在这里插入图片描述
那么如何实现传基类调用基类的虚函数,传派生类调用派生类的虚函数?
当然是使用切片了!

1. 首先每个实例化的类(如果有虚函数)会有一个虚函数表。
2. 传基类调用基类的虚函数,就正常在基类虚表中寻找其对应函数
3. 传派生类,因为多态函数时基类的指针,那么就会切片出来一个基类(虚函数表是派生类的),那么就会在派生类虚表调用对应虚函数。

这样就实现了执行谁就调用谁!!!
运行过程中去虚表中找对应的虚函数调用。具体的汇编语言实现还是比较直白的。

注意同类型的虚表是一样的!!!

  • 满足多态,那么运行时汇编指令会去指向对象的虚表中找对应虚函数进行调用!!!
  • 不满足多态,编译链接时直接根据对象类型,确定调用的函数,确定地址!!!

这里需要分辨一下两个概念:虚表与虚基表

  • 虚表:虚函数表,存的是虚函数,用来形成多态!!!
  • 虚基表:存的是距离虚基类的位置的偏移量,用来解决菱形继承的数据冗余和二义性!!!

注意:虚函数不是存在虚表中的 , 虚表中存的是虚函数的指针。那虚函数存在哪里呢?
来验证一下:

class Person
{
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};


int main()
{
	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);

	return 0;
}

运行可以看到:
在这里插入图片描述
虚表地址与常量区最接近,那可以推断出虚表储存在常量区!!!

4.2 验证虚表

我们来看:

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

梳理一下结构:

  1. Base作为基类 , Derive作为派生类!
  2. 派生类Derive重写了func1函数,构成多态
  3. 其余函数均不构成多态。

然后我们来探索一下:

int main()
{
	Base b;
	Derive d;
	return 0;
}

通过监视窗口可以查看一下虚表的内容:
在这里插入图片描述

这是VS调试的一点BUG,导致监视中派生类的虚表不能显示。在内存窗口里存在4个函数指针,接下来我们来验证一下他们是不是对应的虚函数。

虚函数表本质是一个函数指针数组!
那么如何定义一个函数指针和函数指针数组呢?

//这样定义 
//返回值是void 所以写void
void(*p)( //函数里面的参数 );
void(*p[10])( //函数里面的参数 )

当然可以使用typedef来简化(这个typedef也很特别)

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

那么如果我们想要打印出虚表,我们可以设置一个函数:

//因为是函数指针数组,所以传参是函数指针的指针(int arr[10] 传入 int*)。
void PrintVFT(VFPTR* vft )
{
	for(size_t i = 0 ; i < 4 ; i++)
	{
		printf("%p\n" , vft[i]);
	}
	
}

这样就可以打印了,那么现在就需要解决如何获取虚表的首地址。虚表首地址是类的头4个字节(x86环境),我们如何取出来了呢?
直接把类强转为int类型不就4个字节了吗!?但是没有联系的类型是不能强转的。那怎么办呢???
C/C++中指针可以直接互相强转(BUG级别的操作!!!),整型与指针也可以互相转换。

VFPTR* p =  (VFPTR*) *( (int*)&d );//这样就变成4个字节了!
  1. &d 是取类的指针
  2. (int*)&d将类指针强转为int*指针!
  3. *( (int*)&d )int * 解引用为int
  4. (VFPTR*) *( (int*)&d )int转换为VFPTR*,取到虚表首地址!!!

那么我们来验证一下:

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(*VFPTR)();

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

}

int main()
{
	Base b;
	Derive d;
	VFPTR* p = (VFPTR*)*((int*)&d);//这样就变成4个字节了!
	PrintVFT(p);
	return 0;
}

来看:
在这里插入图片描述
这样就成功获取到了虚标的内容,验证了虚表的内容中存在4个虚函数!!!

5 抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口类)

抽象类不能实例化出对象。派生类继承后也不能实例化出对象。只有重写纯虚函数,派生类才能实例化出对象

纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承

//抽象类
class Car
{
public: 
	//纯虚函数
	virtual void Drive() = 0;
};

int main()
{
	Car c;
	return 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;
	}
};
void Test()
{
	//各种调用自身的虚函数
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}
int main()
{
	Test();
	return 0;
}

抽象类与override关键字的区别:

  1. 抽象类间接强制了派生类必须进行虚函数重写
  2. override是在已经重写的情况下,帮助进行重写的语法检查

6 多继承中的多态

多继承我们讲过,是一种很危险的继承,很容易导致菱形继承,引起数据冗余和二义性。那么我们再来看看多态在多继承中是然如何实现的 。

一般的多继承

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

分析一下继承关系:

  1. 有两个基类:Base1类与Base2类
  2. Derive继承两个基类,对func1函数进行了重写构成多态

来看看Derive类的大小是多大:
我们分析一下:Base1类应该有一个虚表指针和一个int类型数据,所以应该为8字节。Base2同理8字节。
那么Derive由于多继承的缘故会包含两个基类,所以应该为16 + 4 = 20字节
在这里插入图片描述
运行一下,看来我们的分析没有问题!也就是有两张虚表,func1重写会改变两个虚表(因为两个基类都有func1函数),func3是放在Base1的虚表中的,通过虚表验证:

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);
	//通过切片获取
	Base2 b2 = d;
	VFPTR* vTableb2 = (VFPTR*)(*(int*)&b2);
	PrintVTable(vTableb2);

	return 0;
}

运行看看:
在这里插入图片描述
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

菱形继承和菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。这里简单叙述一下:
先看菱形继承:

class A
{
public:
	virtual void func1() { cout << "A::func1" << endl; }
	int _a;
};

class B : public A
{
public:
	virtual void func2() { cout << "B::func2" << endl; }
	int _b;
};

 class C : 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;
}

先来看一下这个类有多大:
在这里插入图片描述

28 字节,这个是怎么得到的,来分析一下:

  1. A类有一个虚函数表指针和一个整型 ,应该是8字节
  2. B类继承于A类 ,包含A类的内容,B的虚函数储存在A的虚表中,所以B类一个为8 + 4 = 12
  3. C类同理
  4. D类继承于B类和C类,那么就包含B类与C类,D类的虚函数储存在B类的虚表中(A的虚表)

在这里插入图片描述
通过内存来验证一下:
在这里插入图片描述
可以看到只有两个虚表指针。所以菱形继承和多继承类似!

再来看菱形虚拟继承:
在这里插入图片描述
这个36字节是怎么得到的???

  1. 首先菱形虚拟继承会把共同的基类提取出来(也就是A被提出来了),那么B类就会有一个虚基表指针来指向这个提前出来的A类。所以B类大小为4 (虚表指针) + 4(虚基表指针) + 4(int数据) = 12
  2. C类同理,那么现在就有12 (B类) + 12(C类) + 4(A类的int)+ 4(D类的int) = 32
  3. 啊???这才32字节,剩下的4字节是什么?难不成还有一个虚表指针?!是的,A 里面还有一个虚表指针!!!

来看内存:
在这里插入图片描述
很明显,在A类中还有一个虚表指针!!!真滴复杂!
所以应该是:
12 (B类) + 12(C类) + 8(A类的int)+ 4(D类的int) = 36
那为什么A会有一个虚表指针,而不是D类有!?

  1. 首先派生类的成员是不会有虚表指针的,虚表指针都在基类的部分中!!!

  2. 我们这四个类都有自身的虚函数

    • 菱形继承中,B类与C类都继承于A类,所以BC是派生类,就不需要有独立的虚表指针,而是与A类共用。父类有了就与父类共用,父类没有才会独立创建。
    • 菱形虚拟继承中,B类与C类都虚拟继承于A类,A类被单独出去了,那么B类与C类的虚函数就不能放在A类里,因为A类是共享的,放进去就会产出问题!所以BC会独立创建一个虚表指针
  3. 总结: 子类有虚函数,继承的父类有虚函数就有虚表,子类不需要单独建立虚表!!!如果父类是共享的,无论如何都有创建独立的虚表!!!

注意:虚基表中储存两个值:第一个是距离虚表位置的偏移量,第二个是距离基类位置的偏移量

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

xv6源码分析 017

xv6源码分析 017 在buffer cache上面的就是logging层了&#xff0c;这一层主要的工作是维持每一个文件系统写入的操作的原子性。什么是原子性&#xff1f;通俗地来讲&#xff0c;原子性可以这样理解&#xff0c;如果一组操作&#xff08;或者一个操作&#xff09;在执行的时候…

【busybox记录】【shell指令】expand

目录 内容来源&#xff1a; 【GUN】【expand】指令介绍 【busybox】【expand】指令介绍 【linux】【expand】指令介绍 使用示例&#xff1a; 把制表符转化为空格 - 默认输出 把制表符转化为空格 - 修改制表符转空格的个数 把制表符转化为空格 - 修改制表符转空格的个数…

HackMyVM-Animetronic

目录 信息收集 arp nmap nikto whatweb WEB web信息收集 feroxbuster steghide exiftool hydra ssh连接 提权 系统信息收集 socat提权 信息收集 arp ┌──(root㉿0x00)-[~/HackMyVM] └─# arp-scan -l Interface: eth0, type: EN10MB, MAC: 08:00:27:9d:6d:7…

C语言——每日一题(轮转数组)

一.前言 前不久学习了时间复杂度的概念&#xff0c;便在力扣上刷了一道需要参考时间复杂度的题——轮转数组 https://leetcode.cn/problems/rotate-array/submissions这道题不能使用暴力算法&#xff0c;因为这道题对时间复杂度的要求不能为O&#xff08;N^2&#xff09;。因…

【数据库表的约束】

文章目录 一、NULL vs &#xff08;空字符串&#xff09;二、not null 和default三、列描述字段comment四、zerofill五、primary key 主键总结 一、NULL vs ‘’&#xff08;空字符串&#xff09; NULL和空字符串’’ NULL代表什么都没有。 空字符串’代表有&#xff0c;但串…

CI/CD 上云为何如此重要

近年来&#xff0c;敏捷度和速度日渐成为产品开发的关键。市场高速运行&#xff0c;时间就是金钱&#xff0c;也是企业发展的关键。游戏、金融、自动化产业等软件开发企业更像卷入了一场无休止的时间竞赛。 这也难怪 DevOps 备受欢迎。企业借助 DevOps 不断加速优质软件的交付…

​分享1.36G全国村名点数据

数据是GIS的血液&#xff01; 我们在《2015年中国电子地图数据》一文中&#xff0c;为大家有偿分享了一份图层丰富&#xff0c;且有26.8G大小的全国电子地图。 这里再为大家分享一份有1.36G大小的全国村名数据&#xff0c;本数据来自网友分享&#xff0c;据说为2023年的村名数…

VMware 替代专题|14 个常见问题,解读 VMware 替代的方方面面

随着 VMware by Broadcom 调整订阅模式和产品组合&#xff0c;不少用户也将 VMware 替代提上日程。为了帮助用户顺利完成从 VMware 替代方案评估到产品落地的一系列环节&#xff0c;我们通过这篇博客&#xff0c;对 VMware 替代场景下用户经常遇到的问题进行了梳理和解答。 更…

【工作记录】openjdk-22基础镜像的构建

背景 近期使用到的框架底层都用的是springboot3.0&#xff0c;要求jdk版本在17甚至更高。 于是决定制作一个基于openjdk22的基础镜像&#xff0c;本文对这一过程进行记录。 作为记录的同时也希望能够帮助到需要的朋友。 期望效果 容器内可以正常使用java相关命令且版本是2…

再议大模型微调之Zero策略

1. 引言 尽管关于使用Deepspeed的Zero策略的博客已经满天飞了&#xff0c;特别是有许多经典的结论都已经阐述了&#xff0c;今天仍然被问到说&#xff0c;如果我只有4块40G的A100&#xff0c;能否进行全量的7B的大模型微调呢&#xff1f; 正所谓“纸上得来终觉浅&#xff0c;…

华为OD机试【路灯照明问题】(java)(100分)

1、题目描述 在一条笔直的公路上安装了N个路灯&#xff0c;从位置0开始安装&#xff0c;路灯之间间距固定为100米。 每个路灯都有自己的照明半径&#xff0c;请计算第一个路灯和最后一个路灯之间&#xff0c;无法照明的区间的长度和。 2、输入描述 第一行为一个数N&#xff…

一文了解什么是SSL证书?——值得收藏

SSL证书&#xff0c;全称Secure Sockets Layer证书&#xff0c;是一种网络安全协议的实现方式&#xff0c;现在通常指的是其继任者TLS&#xff08;Transport Layer Security&#xff09;证书&#xff0c;不过习惯上仍称为SSL证书。它的主要作用是确保互联网上的数据传输安全&am…

ESP32 IDF linux下开发环境搭建

文章目录 介绍升级Python环境下载Python包配置编译环境及安装Python设置环境变量 ESPIDF环境搭建下载esp-idf 代码编译等待下载烧录成功查看串口打印 介绍 esp32 官方文档给的不是特别详细 参考多方资料 最后才完成开发 主要问题在于github下载的很慢本教程适用于ubuntu deban…

HarmonyOS实战开发-如何实现查询当前城市实时天气功能

先来看一下效果 本项目界面搭建基于ArkUI中TS扩展的声明式开发范式&#xff0c; 数据接口是和风&#xff08;天气预报&#xff09;&#xff0c; 使用ArkUI自带的网络请求调用接口。 我想要实现的一个功能是&#xff0c;查询当前城市的实时天气&#xff0c; 目前已实现的功能…

5.合并两个有序数组

文章目录 题目简介题目解答解法一 &#xff1a;合并后排序解法二&#xff1a;双指针排序 题目链接 大家好&#xff0c;我是晓星航。今天为大家带来的是 合并两个有序数组 相关的讲解&#xff01;&#x1f600; 题目简介 题目解答 解法一 &#xff1a;合并后排序 假设我们要合…

每日OJ题_贪心算法三⑤_力扣134. 加油站

目录 力扣134. 加油站 解析代码 力扣134. 加油站 134. 加油站 难度 中等 在一条环路上有 n 个加油站&#xff0c;其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车&#xff0c;从第 i 个加油站开往第 i1 个加油站需要消耗汽油 cost[i] 升。你从其中的一…

Redis学习(十)|使用消息队列的重试机制实现 MySQL 和 Redis 的数据一致性

文章目录 介绍原理整体方案实现步骤示例代码总结其他&#xff1a;Kafka 重试策略配置1. 生产者重试策略配置2. 消费者重试策略配置 介绍 在分布式系统中&#xff0c;保持 MySQL 和 Redis 之间的数据一致性是至关重要的。为了确保数据的一致性&#xff0c;我们通常采取先更新数…

红外与可见光图像融合评价指标(cddfusion中的代码Evaluator.py)

一、Evaluator.py全部代码&#xff08;可正常调用&#xff09; import numpy as np import cv2 import sklearn.metrics as skm from scipy.signal import convolve2d import math from skimage.metrics import structural_similarity as ssimdef image_read_cv2(path, modeRGB…

minio上传文件失败如何解决

1. 做了什么操作 通过接口上传excel文件&#xff0c;返回响应值 2. 错误如图 2. 如何解决 根据错误描述定位到了部署minio的地方minio通过docker部署&#xff0c;找到docker - compose发现配置文件中minio有两个端口&#xff0c;一个是用于api的&#xff0c;一个是用于管理界面…

AI模型:windows本地运行下载安装ollama运行Google CodeGemma可离线运行数据模型【自留记录】

AI模型&#xff1a;windows本地运行下载安装ollama运行Google CodeGemma可离线运行数据模型【自留记录】 CodeGemma 没法直接运行&#xff0c;需要中间软件。下载安装ollama后&#xff0c;使用ollama运行CodeGemma。 类似 前端本地需要安装 node.js 才可能跑vue、react项目 1…