C++|多态(虚函数、抽象类、多态原理)

目录

一、多态的概念及构成

1.1概念

1.2多态的构成条件(继承+虚函数)

二、虚函数和虚函数重写

2.1虚函数和虚函数重写的概念

2.2虚函数的"异变"(协变+析构重写)

2.3虚函数的扩展(override+final)

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

三、抽象类 

3.1概念

3.2接口继承和实现继承

 四、多态的原理

4.1虚函数表及指针(原理层上的覆盖+虚表存储位置)

4.2多态调用的原理(动态绑定+静态绑定)

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

5.1单继承中的虚函数表

5.2多继承中的虚函数表


一、多态的概念及构成

1.1概念

从表面意思上来说,就是多种形态,具体来说就是去完成某个行为,当不同的对象去完成时会产生不同的状态。那么运用到类中,多态是在不同的继承关系的类对象去调用同一个函数,产生了不同的行为。 

例如:坐飞机,高铁等,买同一趟的班次,有的人花的价钱却不一样。

1.2多态的构成条件(继承+虚函数)

 在程序中,构成多态是在继承中发生,同时还有两个条件:

1.必须通过基类的指针或者引用调用虚函数

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

光看概念还是懵懂,那么还得来了解虚函数是啥。 

二、虚函数和虚函数重写

2.1虚函数和虚函数重写的概念

虚函数:在类中,被virtual修饰的函数就叫做虚函数 

虚函数的重写(覆盖):

①派生类有一个跟基类完全相同的虚函数

②完全相同指的是他们的返回值类型、函数名字、参数列表完全相同

满足这两点,就可以说子类虚函数构成重写。

直接上一个多态例子:

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void rice()//虚函数
	{
		cout << "吃两碗饭" << endl;
	}
};

class Student : public Person
{
public:
	virtual void rice()//虚函数,对基类虚函数重写
	{
		cout << "吃一碗饭" << endl;
	}
};



int main()
{
	Person p;
	Person& ps = p;//基类引用基类
	ps.rice();//基类引用调用基类虚函数
	
	Student s;
	ps = s;//ps 最开始引用了基类对象 p。无论后来如何操作,ps 指向的始终是基类对象 p。即使将派生类对象 s赋值给 ps;,
	//也只是将派生类对象的部分成员切片到了基类对象中,但 ps 本身依然是一个基类引用,
	ps.rice();//因此调用 ps.rice() 时会调用基类 Person 中的 rice() 函数,而不是派生类 Student 中的版本。

	Person& pp = s;
	pp.rice();
	pp = p;//同理pp 最开始引用了派生类对象s。无论后来如何操作,pp 指向的始终是派生类类对象 s
	pp.rice();//因此调用 pp.rice() 时会调用派生类 Student 中的rice版本。

	//指针就不一样了,指针是指向谁就调用谁的,因为指针指向的是对象的地址。
    Person* sp = &p;//基类指针指向基类
	sp->rice();//基类指针调用基类虚函数

	sp = &s;//基类指针指向子类中父类的成员
	sp->rice();//基类指针调用子类中父类的虚函数
	return 0;
}

输出结果: 

 由上述多态可以总结:

1.基类引用调用虚函数时,调用其初始化引用中的虚函数,无论后来如何操作,始终不变。

2.基类指针调用虚函数时,指向谁就调用谁的。

同时要注意的是多态调用和普通调用的区别,普通调用规则:

1.在多态中,子类调用子类自己的方法为普通调用

2.非多态中,就是普通调用

普通调用就只跟对象类型有关了,对象类型是属于谁的就调用谁的。

了解了多态,虚函数的概念使用,对此,虚函数还有一些扩展,来继续学习吧

2.2虚函数的"异变"(协变+析构重写)

子类虚函数重写时,不加关键字virtual也构成虚函数重写,其依然是虚函数,但是父类的关键字virtual不能省略注意:只要父类的virtual未省略,其所有直接继承、非直接继承中子类虚函数重写都可不加virtual,其依然是虚函数。例如:


class Person
{
public:
	virtual void rice()//虚函数
	{
		cout << "吃两碗饭" << endl;
	}
};

class Student : public Person
{
public:
	 void rice()//子类虚函数的重写不加virtual依然构成重写
	{
		cout << "吃一碗饭" << endl;
	}
};

协变(基类与派生类虚函数返回值类型不同)

当派生类重写基类虚函数时,其返回值类型与基类的虚函数返回值类型可以不同,构成这样的条件是,有额外的继承关系,且当前基类虚函数的返回值类型为额外的基类对象的指针或者引用,派生类虚函数的返回值类型为额外的派生类对象的指针或者引用。

#include <iostream>
using namespace std;

//额外的继承关系
class A
{};

class B : public  A
{};


class Person
{
public:

	//virtual A* rice()//基类虚函数返回额外的派生类对象的引用指针
	//{
	//	cout << "吃两碗饭" << endl;
	//	return new A;
	//}
	virtual const A& rice()//基类虚函数返回额外的派生类对象的引用
	{
		cout << "吃两碗饭" << endl;
		return A();//构造匿名对象并返回,因为该匿名对象在函数结束时会销毁,
		//所以返回时会生成临时对象,匿名对象给给临时对象,而临时对象具有常属性,所以返回类型需要加const
	}
};

class Student : public Person
{
public:

	//B* rice()//派生类虚函数返回额外的派生类对象的指针
	//{
	//	cout << "吃一碗饭" << endl;
	//	return new B;
	//}

	const B& rice()//派生类虚函数返回额外的派生类对象的引用
	{
		cout << "吃一碗饭" << endl;
		return B();
	}
};


int main()
{
	Person p;
	Person& ps = p;
	ps.rice();

	Person* sp = &p;
	sp->rice();

	Student stu;
	sp = &stu;
	sp->rice();



	return 0;
}

 输出结果:

按照协变的规则,其明显违反了多态的规则,但就是能支持,没办法,C++的语法就是这么复杂,在这里协变又可以看做一个特例。

析构函数的重写(基类与派生类析构函数的名字不同)

如果基类析构函数是虚函数,那么子类的析构函数就构成对父类析构函数的重写。这是为什么呢,明明他们的函数名称不同为何构成重写?

实则,编译器会在编译后,对析构函数的名称统一处理成destructor,这样一来,他们的名称就相同了,从而构成了重写

#include <iostream>
using namespace std;

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

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





int main()
{
	Person* p = new Student;
	delete p;

	return 0;
}

 输出结果:

 由结果也可分析,子类析构函数构成了重写,多态调用了子类的析构函数,在继承中我们讲过,调用完子类的析构函数,会自动调用父类的析构函数,所以也会输出父类的结果。

在这里,有一个问题没解决的是,正因为编译器会在编译后,对析构函数的名称统一处理成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* ps = new Person;
	delete ps;//普通调用只跟对象类型有关,其类型为父类类型指针,调用的就是父类的析构

	Person* p = new Student;
	delete p;//普通调用只跟对象类型有关,其类型为父类类型指针,调用的就是父类的析构

	Student* s = new Student;
	delete s;//普通调用只跟对象类型有关,其类型为子类类型指针,调用的就是子类的析构
	//但调用完又会自动调用父类的析构

	return 0;
}

 输出结果:

除了上述虚函数的各种“异变”,那么虚函数还存在一些额外的修饰,具体是干嘛的,见下 

2.3虚函数的扩展(override+final)

C++11引入了两个新的关键字,override和final。其作用是为了检测函数重写是否出现漏洞,当函数重写出现了bug,其只有在运行时才能发现错误,为了提高检测错误效率,这两个关键字就可以派上用处。

①override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。其加在函数重写后面

例如(错误例子):

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void fun()
	{}
};

class Student : public  Person
{
public:
	void FUN() override//未重写父类虚函数,编译报错
	{}
};


int main()
{


	return 0;
}

编译报错:

②final:修饰虚函数,表示该虚函数不能再被重写。这就隔绝了函数重写可能带来的错误。 

 例如(错误例子):

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void fun() final//其子类虚函数不能构成重写,否则编译报错
	{}
};

class Student : public  Person
{
public:
	void fun()
	{}
};


int main()
{

	return 0;
}

编译报错:

 学习完了虚函数,接下来对虚函数的概念可能产生的冲突做一个总结。

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

这里有一点注意的是,重写虽然也叫覆盖,但虚函数重写是语法层的概念,覆盖是原理层(虚基表)的概念,这里先打个预防针,在后面会讲解。 

OK,你以为虚函数就全部了解完了吗,你错了,纯虚函数才是法外狂徒。 

三、抽象类 

3.1概念

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

 例子:

class Person
{
public:
	virtual void fun() = 0
	{
		cout << "谁都不能改变我" << endl;
	}
};

class Student : public  Person
{
public:
	 void fun()//重写纯虚函数
	{
		cout << "我要回家" << endl;
	}
};
class Assistant : public  Person
{
public:
	void fun()//重写纯虚函数
	{
		cout << "我要回家找妈妈" << endl;
	}
};

int main() 
{
	//Person sp;抽象类不能实例化出对象

	Person* p = new Student;
	p->fun();

	Person* ps = new Assistant;
	ps->fun();

	return 0;
}

输出结果:

那么什么又是接口继承呢?

3.2接口继承和实现继承

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

现在可以说是虚函数的完结了,但是对于多态的调用究竟是如何实现只了解了片面,对于其底层的挖掘还有一段距离,那么现在就来揭秘。 

 四、多态的原理

4.1虚函数表及指针(原理层上的覆盖+虚表存储位置)

在虚拟继承中,我们学习了虚基表指针和虚基表。对于多态呢就有这样一个概念,虚函数指针和虚函数表。他们是干啥的呢?先看 一段代码

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void fun1()
	{
		cout << "Person::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Person::fun2()" << endl;
	}
private:
	int _num = 0;
};

int main()
{
	Person p;
	cout << sizeof(p) << endl;

	return 0;
}

输出结果:

根据往常的理解,其大小应该是4,计算的是_num成员的大小呀,那为什么是16呢, 实则不然,其还有其他成员那就是虚函数表:

调试监口:

通过调试监口发现对象p不只有_num成员还有_vfptr。那么其中_vfptr就是虚函数表,虚函数表又简称虚表,虚函数表存放的是虚函数表指针,指针的值是虚函数地址,虚函数表指针指向的是虚函数的地址,其次,每个含有虚函数的类都有一个虚函数表。所以我们在计算对象p大小的时候还得计算虚表的大小。虚表的对齐数为8,_num的对齐数为4,大小为12,再根据对齐的最后一条规则取最大对齐数的倍数,总大小为16。

原理层上的覆盖

OK,了解了虚函数表和虚函数表指针的概念之后,接着就来延续上面提到的预防针,即原理层上的覆盖。当子类重写虚函数,则在子类虚表中,就会对子类中原先的父类虚函数进行覆盖。

例如: 

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void fun1()
	{
		cout << "Person::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Person::fun2()" << endl;
	}
private:
	int _num = 0;
};


class Student : public  Person
{
public:
	virtual void fun1()//重写纯虚函数
	{
		cout << "Student::fun1()" << endl;
	}


private:
	int _id = 1;
};


int main()
{
	Person p;

	Student s;
	return 0;
}

调试监口:

 内存角度观察虚函数表成员:

对此可以对虚函数表做一个总结拓展:

1.虚函数表本质是一个存虚函数指针的指针数组,不是虚函数的其地址就不会存放到虚函数表中。一般情况这个数组最后面放了一个nullptr,在调试中我们看不到这个空指针,但看虚函数表的成员个数也可明确,同时也可通过内存观察出。

2. 派生类的虚表生成是先将基类中的虚表内容拷贝一份到派生类虚表中,如果派生类重写基类虚函数,则用派生类自己的虚函数覆盖派生类虚表中基类虚函数。

3.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void fun1()
	{
		cout << "Person::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Person::fun2()" << endl;
	}
private:
	int _num = 0;

};


class Student : public  Person
{
public:
	virtual void fun1()//重写纯虚函数
	{
		cout << "Student::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "Student::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Student::fun4()" << endl;
	}
private:
	int _id = 1;
};

int main()
{
    Student s;
    return 0;
}

 

4.同一个类实例化出的对象共用一张虚表 

 

对于虚函数表大家可能还会存在一个误区:

我们知道,成员函数是存放在代码区,那么虚函数也是如此,因为虚函数也是成员函数,有疑问的是虚表是存放在哪的?在调试结果中可以看到虚表似乎是在一个对象当中的,当真如此吗?实则不然,在VS下,通过验证虚表其实也是存放在代码区的。通过打印虚表地址进行验证。

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void fun1()
	{
		cout << "Person::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Person::fun2()" << endl;
	}
private:
	int _num = 0;

};


class Student : public  Person
{
public:
	virtual void fun1()//重写纯虚函数
	{
		cout << "Student::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "Student::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Student::fun4()" << endl;
	}
private:
	int _id = 1;
};

int main()
{
	int a;
	printf("栈区:%p\n", &a);

	int* p = (int*)malloc(sizeof(int)*1);
	printf("堆区:%p\n", p);

	static int b;
	printf("静态区:%p\n", &b);

	const char* q = "bit";
	printf("常量区:%p\n", q);

	Person pp;
	void** ptr = ((void**)*(int*)(&pp));//取pp对象的地址,强转为int*,说明只能访问pp对象大小为4个字节,
	//取到pp对象的头四个字节,即取到虚函数表
	//虚函数表存的又是虚函数指针,虚函数的类型为void,所以虚函数指针的类型也为void*
	//那么虚函数表的类型就相当于void**类型了,所以还要将取到的头四个字节强转成void**类型
	printf("虚表地址: %p\n", ptr);
	return 0;
}

pp对象空间抽象图: 

 

输出结果:

 

调试结果: 

 

通过打印,调试对比,可以看出虚表地址与常量区的地址最接近,而内存的地址划分大小变化是常量区/代码区往上增长,栈区往下增长的,由此可见虚表是存储在常量区/代码区的。

有了虚函数的理解,现在可以开展多态调用的原理了。

4.2多态调用的原理(动态绑定+静态绑定)

多态调用正是通过虚函数指针去调用对应的虚函数,父类指针或引用指向父类则通过父类中的虚函数指针调用父类的虚函数,父类指针或引用指向子类则通过子类中的虚函数指针调用子类的虚函数。

且多态调用是在运行时确定函数的调用,当父类指针或引用指向子类对象并调用方法时,会根据实际对象的类型来决定调用哪个方法的实现,这也体现了动态绑定的特性。而在编译时,编译器只知道引用变量的类型是父类类型,它无法确定该引用变量实际指向的是哪个子类对象,以及子类中是否有覆盖(重写)了父类方法的实现,因此,编译时只能根据引用变量的类型来决定调用哪些方法

而普通调用则是在编译时就确定了函数的地址并调用该函数,体现了静态绑定的特性。

动态绑定:又称为后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

静态绑定:又称为前期绑定(早绑定),在程序变压器间确定了程序的行为,也称为静态多态 。

还是这个例子: 

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void fun1()
	{
		cout << "Person::fun1()" << endl;
	}
private:
	int _num = 0;

};


class Student : public  Person
{
public:
	virtual void fun1()//重写纯虚函数
	{
		cout << "Student::fun1()" << endl;
	}
private:
	int _id = 1;
};


int main()
{
    //多态调用
	Person pp;
	Person* p = &pp;
	p->fun1();
    
    //静态调用
	Person ps;
	Student s;
	ps = s;
	ps.fun1();

	return 0;
}

 下面通过汇编来进行浅浅的观察:

多态调用:

静态调用:

了解完多态原理,就要来进行更深层次的研究,那就是对于多继承关系而言中的虚函数表。 

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

5.1单继承中的虚函数表

在前面就已经知道在监视窗口中,并不能查看虚函数表的所有成员,这影响了对代码的理解性,这可以认为是一个小bug,但是可以通过内存的角度来观察其成员,那么这里还有另一种方式来进行演示,就是打印虚函函数表成员的地址,进行对比 

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void fun1()
	{
		cout << "Person::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Person::fun2()" << endl;
	}
private:
	int _num = 0;

};


class Student : public  Person
{
public:
	virtual void fun1()//重写纯虚函数
	{
		cout << "Student::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "Student::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Student::fun4()" << endl;
	}
private:
	int _id = 1;
};


int main()
{
	Person p;

	Student s;

	return 0;
}

调试监口只能观察两个成员:

通过打印虚表成员地址:

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void fun1()
	{
		cout << "Person::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Person::fun2()" << endl;
	}
private:
	int _num = 0;

};


class Student : public  Person
{
public:
	virtual void fun1()//重写纯虚函数
	{
		cout << "Student::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "Student::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Student::fun4()" << endl;
	}
private:
	int _id = 1;
};

typedef void(*VFPTR)();//定义函数指针,并重命名
void PrintVTable(VFPTR arr[])//void(*)() arr[] ==  void**
{
	cout << " 虚表地址>" << arr << endl;
	for (int i = 0; arr[i] != nullptr; i++)
	{
		printf("第%d个虚函数地址:0x%x,->", i, arr[i]);//打印虚函数表中的内容
		VFPTR f = arr[i];//取到虚函数地址
		f();//回调虚函数,例如:(&fun1)();调用虚函数
	}
}
int main()
{
	
	Person p;

	PrintVTable((VFPTR*)(*(int*)(&p)));//(*(int*)(&P))//取p对象的地址,强转为int*,说明只能访问p对象大小为4个字节,
	//取到P对象的头四个字节,即取到虚函数表
	//虚函数表存的又是虚函数指针,虚函数的类型为void,所以虚函数指针的类型也为void*
	//那么虚函数表的类型就相当于void**类型了,所以还要将取到的头四个字节强转成void**类型
	cout << endl;
	Student s;
	PrintVTable((VFPTR*)(*(int*)(&s)));

	return 0;
}

输出结果: 

 

通过打印成员地址,就可以观察出虚表中的所有成员。 也观察出了,子类fun1对父类中的fun1进行了覆盖。有一个问题需注意的是编译器有时对虚表的处理并不干净,导致虚表最后面没有放nullptr,打印时崩溃,此时重新生成解决方案即可。

5.2多继承中的虚函数表

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void fun1()
	{
		cout << "Person::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Person::fun2()" << endl;
	}
private:
	int _num = 0;

};


class Student 
{
public:
	virtual void fun1()
	{
		cout << "Student::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Student::fun2()" << endl;
	}

private:
	int _id = 1;
};

class Assistant : public Person, public Student
{
public:
	virtual void fun1()//重写纯虚函数
	{
		cout << "Assistant::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "Assistant::fun3()" << endl;
	}
private:
	int _a = 0;
};

typedef void(*VFPTR)();//定义函数指针,并重命名
void PrintVTable(VFPTR arr[])//void(*)() arr[] ==  void**
{
	cout << " 虚表地址>" << arr << endl;
	for (int i = 0; arr[i] != nullptr; i++)
	{
		printf("第%d个虚函数地址:0x%x,->", i, arr[i]);//打印虚函数表中的内容
		VFPTR f = arr[i];//取到虚函数地址
		f();//回调虚函数,例如:(&fun1)();调用虚函数
	}
}
int main()
{

	
	Assistant a;
	PrintVTable((VFPTR*)(*(int*)(&a)));

	cout << endl;
	PrintVTable((VFPTR*)(*(int*)((char*)&a + sizeof(Person))));
	//(char*)&a的意思是可以访问a对象的大小为一个字节,而a对象中的头四个字节是父类虚表,
	//则该表达式的意思是指访问的大小为a对象中父类虚表中的第一个元素
	//sizeof(Person)计算Person类的大小,包括虚表大小和成员大小,即(char*)&a + sizeof(Person)表示跳过了一个Person大小
	//即跳到了Student的虚表,所以该整个表达式在打印Student中虚表成员
    //结合下面的图解可以更好理解

	return 0;
}

调试结果:

输出结果: 

 

抽象图: 

 

结合以上,可以观察出,在多继承中,派生类重写虚函数fun1、fun2,则派生类中父类中的虚函数表对应被覆盖。未重写的虚函数fun3放在了第一个继承父类部分的虚函数表中。

对于多继承中的菱形继承和菱形虚拟继承在实际中并不建议去使用,一般也不用过多的去研究。

end~

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

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

相关文章

43.乐理基础-拍号-常见的拍号与强弱关系

首先拍号的定义&#xff1a;39.认识音符、40.什么是一拍、41.小节、小节线、终止线、42.看懂拍号的意义 通过 39.认识音符、40.什么是一拍、41.小节、小节线、终止线、42.看懂拍号的意义 应该可以知道 Y的取值只能是2、4、8、16、32、64。。。。因为Y指的是Y分音符&#xff0c;…

树莓派4b测量PM2.5

1.GP2Y1010AU0F粉尘传感器连接图 2. GP2Y1010AU0F工作原理 工作原理 传感器中心有个洞可以让空气自由流过,定向发射LED光,通过检测经过空气中灰尘折射过后的光线来判断灰尘的含量。 3.源代码 main.py # coding=UTF-8 import RPi.GPIO as GPIO from ADC import ADS1015…

【进程等待】是什么 | 为什么 | 怎么办 | wait阻塞等待

目录 进程等待是什么&#xff1f; 为什么要进程等待&#xff1f; 如何进程等待&#xff1f; wait 阻塞等待 进程等待是什么&#xff1f; 进程终止会把进程退出的数据&#xff08;退出码和退出信号&#xff09;存放到进程的PCB中保存下来&#xff0c;让父进程进行等待。…

OpenHarmony 实战开发 - 如何在源码中编译复杂应用(4.0-Release)

文档环境 开发环境&#xff1a;Windows 11 编译环境&#xff1a;Ubuntu 22.04 开发板型号&#xff1a;DAYU 200&#xff08;RK3568&#xff09; 系统版本&#xff1a;OpenHarmony-4.0-Release 功能简介 在 OpenHarmony 系统中预安装应用的 hap 包会随系统编译打包到镜像中&a…

Java毕业设计 基于SpringBoot vue社区智慧养老监护管理平台

Java毕业设计 基于SpringBoot vue社区智慧养老监护管理平台 SpringBoot 社区智慧养老监护管理平台 功能介绍 登录注册 个人中心 修改密码 个人信息 房间信息管理 房间入住信息管理 反馈信息管理 留言管理 老人信息管理 公告管理 物资申请管理 管理员管理 护工管理 体检员管理…

浅谈Windows 上的线程亲和性(Thread affinity)

​ 前言 线程属性包括是否分离、亲和性、调度策略和优先级等。Linux默认的调度策略是CFS(完全公平调度算法),而 Windows 是基于优先级抢占式的策略。 在这些方面,Windows 和 Linux 差异巨大。本文仅针对 Windows 系统的线程亲和性进行探讨。 线程亲和性(Thread affinity) 什…

解锁AI的神秘力量:LangChain4j带你步入智能化实践之门

关注微信公众号 “程序员小胖” 每日技术干货&#xff0c;第一时间送达&#xff01; 引言 在数字化转型的浪潮中&#xff0c;人工智能&#xff08;AI&#xff09;正逐渐成为推动企业创新和增长的关键力量。然而&#xff0c;将AI技术融入到日常业务流程并非易事&#xff0c;它…

谷歌月球模型

收费产品&#xff0c;白嫖党勿扰 收费金额500元 1 概述 前些时间&#xff0c;有个客户&#xff0c;想fight TAIWAN&#xff0c;于是乎&#xff0c;我把谷歌地球整个台湾的模型都下载下来了&#xff0c;大约300GB。今天&#xff0c;又有个客户&#xff0c;提出一个过分要求&…

【第14章】spring-mvc之ajax

文章目录 前言一、准备二、单个值1.前端2.后端3. 结果 三、对象1.前端2.后端3. 结果 四、JSON对象1.前端2.后端3. 结果 五、JSON数组1.前端2.后端3. 结果 总结 前言 AJAX&#xff08;Asynchronous JavaScript and XML&#xff09;是一种用于创建快速动态网页的技术&#xff0c…

STM32:GPIO输入输出

文章目录 1、GPIO介绍1.1 GPIO的基本结构1.1 GPIO的位结构 2、 GPIO工作模式3、GPIO标准外设库接口函数3.1 RCC接口函数3.2 GPIO接口函数3.2.1 GPIO的读取函数3.2.1 GPIO的写入函数 4、GPIO的初始化 1、GPIO介绍 GPIO&#xff08;General Purpose Input Output&#xff09;通用…

深入大模型量化技术,大模型端侧落地已Ready?

揭秘未来&#xff1a;大模型量化技术如何革新移动AI应用 ©作者|饮水机 来源|神州问学 前言 最近&#xff0c;苹果发布了OpenELM系列模型&#xff0c;参数规模分别为270M、450M、1.1B和3B。与此同时&#xff0c;微软也推出了Phi-3系列模型&#xff0c;其中mini版本的参数…

支付时,中国网联结算与中国银联结算的区别与联系

随着电子商务和互联网支付的快速发展&#xff0c;中国的支付清算市场也呈现出前所未有的繁荣景象。在这个大背景下&#xff0c;中国网联与中国银联作为两大支付清算机构&#xff0c;各自扮演着重要的角色。本文将对两者的区别和联系进行深入探讨&#xff0c;以期对读者有更全面…

【北京迅为】《iTOP-3588开发板快速烧写手册》-第9章ubuntu系统下升级固件

RK3588是一款低功耗、高性能的处理器&#xff0c;适用于基于arm的PC和Edge计算设备、个人移动互联网设备等数字多媒体应用&#xff0c;RK3588支持8K视频编解码&#xff0c;内置GPU可以完全兼容OpenGLES 1.1、2.0和3.2。RK3588引入了新一代完全基于硬件的最大4800万像素ISP&…

智慧公厕,城市现代化公卫变革的关键节点

随着城市的快速发展&#xff0c;公共厕所作为社会基础民生设施&#xff0c;正越来越受到精细化管理的重视。智慧公厕的出现&#xff0c;为传统公共厕所的脏乱臭提供了解决方案&#xff0c;通过物联网、大数据、云计算、自动化控制等先进技术的贡献&#xff0c;使公厕管理更加高…

怎么编辑百度百科个人词条

辑百度百科个人词条是一个相对复杂的过程&#xff0c;需要遵循一定的步骤和规则。以下是百科优化网整理的编辑百度百科个人词条的步骤和注意事项。 1. 确定编辑资格 百度百科个人词条的编辑权主要赋予那些具有一定影响力的公众人物&#xff0c;或者是有一定“身份”的人物&…

大模型驱动的新一代 BI 平台,Sugar BI 开启智慧决策新模式

本文整理自 2024 年 4 月 16 日的 2024 百度 Create 大会上的《大模型驱动的新一代 BI 平台如何开启智慧决策》分享。 全文包括了可视化 BI 分析技术架构、智能图表推荐策略与规则设计、Sugar Bot 智能问数的技术实现流程&#xff0c;以及目前的场景应用等。 1 Sugar BI 产…

【C语言】路漫漫其修远兮,深入[指针]正当下

一. 指针初步 1.概念定义 地址&#xff1a;我们在内存中开辟空间时&#xff0c;为了方便后续访问&#xff0c;每个数据有确切的地址。 指针&#xff1a;指向数据的地址&#xff0c;并将其地址储存在指针变量中。 2.基本运算符 • 取地址操作符&#xff08;&&#xff09; …

OpenHarmony 4.0 实战开发——分布式软总线解析:设备发现与传输

OpenHarmony 的分布式软总线子系统为 OpenHarmony 系统提供的通信相关的能力&#xff0c;包括&#xff1a;WLAN 服务能力、蓝牙服务能力、软总线、进程间通信 RPC&#xff08;Remote Procedure Call&#xff09;等通信能力。 其中主要包括&#xff1a; WLAN 服务&#xff1a;…

【Java开发的我出书啦,各位同仁快过来围观】!!!

文章目录 &#x1f50a;博主介绍&#x1f964;本文内容出书的目的出书的过程书籍的内容 &#x1f4e5;博主的话 &#x1f50a;博主介绍 文章目录 &#x1f50a;博主介绍&#x1f964;本文内容出书的目的出书的过程书籍的内容 &#x1f4e5;博主的话 &#x1f33e;阅读前&#x…

机器学习 | 时间序列预测中的AR模型及应用

自回归模型&#xff0c;通常缩写为AR模型&#xff0c;是时间序列分析和预测中的一个基本概念。它们在金融、经济、气候科学等各个领域都有广泛的应用。在本文中&#xff0c;我们将探索自回归模型&#xff0c;它们如何工作&#xff0c;它们的类型和实际例子。 自回归模型 自回…
最新文章