【C++从0到王者】第二十四站:多态的底层原理

news2024/12/27 13:05:50

文章目录

  • 前言
  • 一、虚函数表
  • 二、一道经典的例题
  • 三、深度剖析多态的条件之一:为什么必须是父类的指针或引用
  • 四、深度剖析多态的条件之二:为什么是虚函数的重写/覆盖?
  • 五、虚函数表的一些总结
  • 六、关于Func3的验证
  • 七、动态绑定与静态绑定
  • 八、总结

前言

在前面,我们也了解了多态的定义、概念、实现。对于多态的使用,有很多需要注意的细节,可谓到处都是坑!了解了多态的使用,那么现在我们来了解一下多态的原理吧。


一、虚函数表

我们先来猜猜下面程序的运行结果是多少?

class Base
{
public:
	virtual void Func1()
	{
	cout << "Func1()" << endl;
	}
private:
	char _c = 1;
};
int main()
{
	cout << sizeof(Base) << endl;
	return 0;
}

我们可能会以为是1,实际上运行结果是8
在这里插入图片描述

那么为什么是8呢?

我们可以进入调试观察一下,我们会发现它里面似乎多了一个指针
在这里插入图片描述

这个指针是四字节的话,那么内存对齐一下,刚好是8个字节。

那么这个指针究竟是何方神圣呢?实际上这个指针是虚函数指针(v代表虚拟,f代表函数,ptr是指针)。从后面的vftable也可以看出来,它是一个虚函数表

从这里我们也可以知道,我们一般不使用多态的话,最好还是不要加上virtual,因为是有开销的。

这个虚表里面存储的就是虚函数的地址。而虚函数是存放在代码段的。

如果我们有两个虚函数的话,那么这个虚表里面就有两个虚函数的地址
在这里插入图片描述

以上是由于虚函数导致的对象中的一些变化,虚函数是应用于重写的。那么重写的时候会发生什么呢?

我们接下来使用如下代码

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	int _a = 1;
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	int _b = 1;
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p;
	Func(p);
	Student s;
	Func(s);
	return 0;
}

我们对其进行分析:下面是监视窗口里面的样子
在这里插入图片描述

我们不妨将他们用下图代替,即用下面的图更能清晰的表达他们的关系

在这里插入图片描述

根据上面的图中,我们可以注意到,所谓的重写,其实从原理层的角度来看,其实就是将虚表里面的地址给覆盖了。才导致调用不同的函数。所以重写是语法层的概念,覆盖是原理层的概念。

对于子类的虚表,我们也可以认为是将父类的虚表给拷贝下来了,然后在将重写的给覆盖上去。

这个时候我们就知道了如何实现的指向父类调父类,指向子类调子类了。

所以现在,我们知道了多态是如何实现的。如果是父类的对象,进行调用的时候那么自然就是调用它的虚表里面的函数,如果是将子类对象,使用指针或者引用进行切片的话,本质上还是指向子类,只不过是指向子类中的父类的那一部分罢了,而我们这里的虚表中的地址已经被替换了。所以当然可以实现调用不同的函数了。

其实如果是普通的调用的话,那么它在编译的时候地址已经被确定了。

如下就是普通调用的时候,在编译的时候地址早已被确定好了,所以它恒定的调用一个函数。这里也就解释了为什么必须是基类的指针和引用。如果是对象的切片的话,这里的虚表中的内容是不会被切片过去的,p调用函数的时候地址在编译时候早已被确定了。
在这里插入图片描述

如果符合多态的话,运行时到指向对象的虚函数表中找调用对象的地址。不是在编译时候就确定了地址了

我们也不管他是子类还是父类,即便是子类,经过切片后,也是一个父类。我们只需要找到对应的虚表中的地址就可以了。我们可以看到,同一个函数,多态调用的时候指令都变多了
在这里插入图片描述

二、一道经典的例题

我们来看下面这道题,猜猜它选什么呢?

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()
{
	B* p = new B;
	p->test();
	return 0;
}

这道题的运行结果为
在这里插入图片描述

看到这里,我们肯定已经蒙了,这是为什么呢?为什么是这么一个出乎意料的结果呢?

我们现在来分析一下代码

首先我们定义了一个派生类的指针,指向了B对象。然后我们现在想用这个派生类的指针去调用test函数,这里是可以去调用的,因为子类继承了父类

在test函数里面,只有一个功能就是调用func函数,注意,这里的func函数是由this指针来进行调用的,只不过是this指针隐藏了。

现在我们来思考一下,这里调用func是不是多态调用呢?

我们知道,多态调用有两大条件:父类的指针或引用去调用虚函数,这个虚函数必须是重写的虚函数。

那么这里的this指针是父类的指针吗?答案是这里的this指针确实是父类的指针,而不是派生类的指针!

为什么是A*父类的指针呢?因为这里的func函数是继承下来的。这里的继承并不是单纯的将test函数在派生类生成了一份,编译器不会那样做的。

继承的对象模型是这样做的,它的对象模型分为两部分,一部分是将父类的整体当成一个成员给拿下来,这里父类会自己内存对其等操作,然后另一部分就是自己的本来的成员,经过与父类对象进行内存对齐以后,整个进行建模。然后这些成员函数它都是在代码段的,它并不会生成多份的。编译的时候是检查语法的,先去派生类里面去找,找不到再去父类里面去找。 所以test不会有两份,所以这里只能是A*指针了。这样就满足了多态的第一个条件了。或者说这里发生一个切片,B*指针切片给了A*类型的指针。

第二个条件是虚函数的重写,那么这个func满足虚函数的重写吗?其实是满足的,首先有基类和派生类里面都有func函数,这两个函数满足虚函数加三同的条件,注意形参的类型相同指的是类型的相同,有没有缺省参数,缺省参数是多少跟他们没有任何关系,即便是形参名字不同也是无所谓的。

所以现在满足了多条的条件,已经是多态的调用了。我们知道多态的调用看的是指向的对象是哪里。而这里我们的A*的指针是由B*的指针切片得到的,所以这里实际指向的是一个派生类,那么自然就调用的是派生类的func了

此时我们以为得到了正确答案B->0,实则不然,我们又调入了一个大坑里面,我们要注意,多态改变的是函数的实现,虚函数加上三同只是可以告诉我们说这个构成了多态。换言之,多态在调用的时候,前面的部分,即返回值形参函数名这些看的是基类的部分,而实现的部分看的是多态的调用,即指向的对象的那一部分。而在这里,形参使用基类中的1,实现打印B。

所以最终结果为B->1

甚至于我们还可以将派生类中的缺省参数给去掉也是没有任何问题的
在这里插入图片描述

甚至于我们可以直接换名
在这里插入图片描述

但是我们不可以连形参名字都不写了,因为我们在里面毕竟用到了val了,如果连val名字都不写的话是不行的
在这里插入图片描述

所以说,虚函数的重写,重写的只是实现,那一个壳用的还是基类的。这里也印证了为什么派生类可以不加virtual。因为只是重写的实现。

如果我们将上面的题稍作修改,如下所示:那么结果又会如何变化呢?

class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
};
class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
	virtual void test() { func(); }
};
int main()
{
	B* p = new B;
	p->test();
	return 0;
}

这里比较有意思的是,我们只是将test这个函数换在了B类里面,即派生类里面,这样的话我们p调用test的时候,this指针和p一样了都是派生类的指针类型,这已经不构成多态的条件了,所以是一个普通的调用,就直接看的是派生类中的这个函数了所以结果为B->0

在这里插入图片描述

三、深度剖析多态的条件之一:为什么必须是父类的指针或引用

我们在回过头来看一下多态的条件为什么是那两条:1.基类的指针或引用去调用虚函数2.被调用的函数必须是重写的虚函数

为什么必须是父类的指针或引用,子类的指针或引用为什么不可以呢?为什么不能是父类的对象呢?

先回答第一个问题:因为只有父类的指针才可以指向子类和父类,如果是子类的指针的话就只能指向子类了,不能指向多种形态了。这个问题还算比较容易理解

再来回答第二个问题:我们知道对象的切片和指针与引用的切片是有一些不同的,我们先要知道对象切片和指针切片的差异是什么。为了演示这个差异,我们使用如下代码

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void Func1() {};
	virtual void Func2() {};
protected:
	int _a = 0;
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
protected:
	int _b = 1;
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;

	return 0;
}

如下是监视窗口中的样子
在这里插入图片描述

我们使用如下方式进行展现,这样方便我们进行观察
在这里插入图片描述

此时我们还是比较容易理解的,这里两个对象,分别有他们自己的虚函数表。这里的虚函数表严格来说应该是一个指针,指向着虚函数表。

我们知道下面的三种方式都是切片,那么他们的差异究竟在哪里呢?

ps = st;
Person* ptr = &st;
Person& ref = st;

首先毋庸置疑的是,如果是指向父类的指针,那么它指向的是一个父类的对象,看到的自然就是父类的虚表了。

如果是指向子类的指针或引用的话,那么它指向的是一个子类中的父类的那一部分,看到的其实是子类中的虚表,这个虚表是经过虚函数重写覆盖过的。
在这里插入图片描述

所以说指针和引用的切片他们是不存在任何的拷贝的问题的。

而对象的切片就存在拷贝的问题了。

当我们使用对象的切片的时候,子类中的父类部分的成员变量肯定是都会被拷贝过去的,但是虚表会被拷贝过去吗?我们可以测试一下

为了方便我们观察,我们可以提前先修改一下派生类中_a的值
在这里插入图片描述

然后我们在使用对象的切片,如下图所示,是未切片的时候

在这里插入图片描述

如下所示,是切片发生之后
在这里插入图片描述

我们已经发现,对象的切片,并不会改变虚表,所以虚表是不会进行拷贝的

那么为什么不拷贝虚表呢?拷贝虚表会带来什么问题呢?

我们可以这样思考一下,假如我们将派生类中的虚表给拷贝过去了,那么我们使用ps这个父类对象给取出它的地址以后,使用这个指针去调用它里面的函数的话,就会反而调用了派生类中的函数。这个明显有点奇怪。因为指向父类的居然调用了子类的函数。这明显不符合多态的要求。毕竟多态是指向什么类就调用什么类的。这样做显然就会乱套的。

所以我们得到一个结论:子类赋值给父类对象切片,不会拷贝虚表。如果拷贝虚表,那么父类对象虚表中的究竟是父类虚函数还是子类虚函数就不知道了,因为我们并不知道究竟这个对象有没有被赋值切片过。总之,就乱套了

上面这个结论,也就回答了我们前面的问题,为什么多态的条件不能是父类的对象。

四、深度剖析多态的条件之二:为什么是虚函数的重写/覆盖?

在前面我们也已经提到过,虚函数的重写/覆盖本质就是是什么?

在语法层面称之为重写,重写的是它的实现。所以有时候我们也会提出一个概念,普通的函数的继承称为实现继承,而多态,虚函数的重写,其实就是一个接口继承,然后重写它的实现

在原理上就是说将父类的虚函数表给拷贝下来,然后将子类中重写的部分给覆盖。

其次,因为只有完成了虚函数的重写,那派生类的虚表里面才能是派生类的虚函数。这样的话,这个基类指针才能做到指向父类调用父类,指向子类调用子类。

五、虚函数表的一些总结

  1. 派生类对象st中也有一个虚表指针,st对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在这一部分的,另一部分是自己的成员。

  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现BuyTicker完成了重写,所以d的虚表中存的是重写的Student::Buyticker,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法

  3. 另外Func1和Func2继承下来后是虚函数,所以放进了虚表,如果Func2不是虚函数,那么它也继承下来了,但是因为不是虚函数,所以不会放进虚表

  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr 。(注意不是所有的编译器都会给的,g++编译器就没有给,而且有时候vs的编译器有一些问题,不会给这个nullptr,这时候我们可以自己清理一下解决方案,然后重新编译一下就有了,这里算是一个编译器的bug)

    下面的就是给了nullptr的
    在这里插入图片描述

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

    a. 先将基类中的虚表内容拷贝一份到派生类虚表中

    b. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

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

    注意c这个小点中,虽然它会将这个添加到派生类虚表的最后,但是我们的监视窗口有时候是看不见的,如下所示,我们并没有看到Func3的虚函数表中的地址。
    在这里插入图片描述

但是我们是可以从内存窗口看到有一个地址的,这个地址就是Func3的虚表中的地址。这里算是一个监视窗口的一个bug
在这里插入图片描述

所以说监视窗口和内存窗口有时候还是有一些不一样的。准确的来说,这里我们也不能断言说这里一定是Func3函数的地址。因为我们并没有给出证明,所以后面我们会给出一个证明。

  1. 还有一点是虚表是存储在哪里的呢?是栈区or堆区or数据段(静态区)or代码段(常量区)这四个中的哪一个呢?

首先我们就可以排除的是堆区,因为堆区还需要new,delete一下,编译器大概率是不会这样做的

然后我们还可以排除的是栈区,因为如果是存在栈区的话,那如果是两个栈帧的话,里面的虚表的地址肯定是不一样的,而我们经过下面的测试,发现地址是一样的,也就是说他们共用虚表,所以可以排除栈区。当然其实也不能百分之百排除掉栈区,因为万一存储在main函数的栈帧中呢?但是大概率还是不会存储在main中的。

在这里插入图片描述

同时上面的情形还说明了一件事,同类型的对象共用虚表

然后我们就可能会去猜测是静态区中存储着虚表,实际上不是的,虽然说网上的很多答案都是静态区,不过这个答案其实是错误的。

我们可以使用如下代码去验证:

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

	int _a = 0;
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	virtual void Func3() {};
protected:
	int _b = 1;
};

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

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

	static int b = 0;
	printf("静态区(数据段): %p\n", &b);

	const char* str = "hello world";
	printf("常量区(代码段): %p\n", str);

	Person ps;
	printf("Person: %p\n", *(int*)&ps);
	Student st;
	printf("Student: %p\n", *(int*)&st);

	return 0;
}

我们先来解释一下这段代码,前面都很简单,最后两个打印的时候,由于对象里面是没有虚表的,但是有一个虚表指针,并且这个指针就是第一个成员变量,所以我们ps的地址就是虚表指针的地址,然后我们为了可以直接用这个虚表指针的地址去打印出来虚表所在的地址,于是我们就对其进行强制类型转换为int*,因为我们的指针是四字节的。然后我们直接解引用,就可以拿到这个虚表指针所指向的值了。由于这个虚表指针本身就是一个二级指针,里面存储的就是一个地址,这个地址所指向的就是虚函数所存储的地址了。

或许你已经被绕晕了,不要紧,我们来画个图来直观的感受一下:

在这里插入图片描述

而我们上面所进行的操作,正好取出来的就是绿色方块里面的值,也就是一个地址,这个地址就是虚函数表的地址。相信大家这会儿已经听懂了吧

而我们最终的运行结果是这样的
在这里插入图片描述

我们对比后发现,与常量区,即代码段的数值最为接近。所以虚表应该存储在常量区/代码段

那么虚函数存储在哪里呢?

如果直接打印地址的话,恐怕并不好打印,有点繁琐,我们不如直接在监视窗口里面观察

在这里插入图片描述

可以注意到,虚函数显然距离常量区更近一些。所以也是存储在常量区的

六、关于Func3的验证

我们在前面中提到了,监视窗口中的虚表少了一个func3的地址,但是当我们进入内存查看的时候,存在一个指针。那么这个指针究竟是不是func3我们还需要进行验证。

我们想要验证这个东西,我们得先将虚表里面的地址看能否给拿出来。只要能拿到虚表里面的函数地址,我们就可以去调用这些函数从而判断是不是该函数

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

	int _a = 0;
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	virtual void Func3()
	{
		cout << "Student::Func3()" << endl;
	};
protected:
	int _b = 1;
};
typedef void (*Func_Ptr)();
void PrintVFT(Func_Ptr* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p\n", i, table[i]);
	}
	cout << endl;
}
int main()
{
	Person ps;
	Student st;
	int vft1 = *(int*)&ps;
	PrintVFT((Func_Ptr*)vft1);
	int vft2 = *(int*)&st;
	PrintVFT((Func_Ptr*)vft2);
	return 0;
}

如上所示的代码就是可以打印出虚表,上面代码的原理是这样的,由于虚表是一个函数指针数组,每一个函数指针都是void(*)()类型的指针。所以我们直接使用typedef一下方便我们使用这种类型的指针,然后我们在想办法取出虚表的地址。这个取法在前文中已经提及了。然后我们就可以直接去打印这个虚表了。注意:我们这里使用的vs2022 ,x86环境的,我们的指针都是4字节的,其次vs在虚表结束的时候是会添加一个nullptr的,如果是Linux环境的话,首先默认是x64环境的,所以指针是八字节的,在取地址的时候就要小心了。我们不能用int类型了,可以使用long long类型的。其次Linux环境下最后是不会在虚表的结尾补一个nullptr的,所以就不能像我们上面那样使用了。必须得写死了才能打印出虚表。

如下就是我们此时打印出来的虚表

在这里插入图片描述

现在我们已经有了虚表中的每一个函数的地址了,那么有了函数的地址了,再去调用这个函数就非常之简单了,我们对前面的代码稍作修改,得到如下代码,可以去正常访问每一个函数

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

	int _a = 0;
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	virtual void Func3()
	{
		cout << "Student::Func3()" << endl;
	};
protected:
	int _b = 1;
};

typedef void (*Func_Ptr)();
void PrintVFT(Func_Ptr* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		table[i]();
	}
	cout << endl;
}

int main()
{
	Person ps;
	Student st;
	int vft1 = *(int*)&ps;
	PrintVFT((Func_Ptr*)vft1);
	int vft2 = *(int*)&st;
	PrintVFT((Func_Ptr*)vft2);
	return 0;
}

对于上面的代码,我们简要的分析一下,我们的目的是为了打印虚表中的每一个函数,虚表本质是一个函数指针数组,注意它与虚基表是不一样的,虚基表是菱形虚拟继承中用来存储偏移量的。虚表是虚函数表,简称虚表,它本质是就是一个函数指针数组。我们可以给每个虚函数都加上打印。因为我们在前面已经取出来了每一个函数的地址,这里就有点类似于回调函数中的做法。我们有了函数地址,它就可以当作函数名直接调用这个函数,然后观察打印结果就可以验证。

在这里插入图片描述

注意在这里我们有时候可能会遇到程序崩溃的情况,这是因为vs的一个bug,本来在虚表后面是要补一个nullptr的,但是我们有时候生成完解决方案以后去修改了代码,可能就不会添加这个空指针了。从而导致程序调用野指针,程序崩溃。这时候我们只需要重新生成一下解决方案即可解决这个问题。

从上面的运行结果来看,是由Func3的,那么这里就已经验证了Func3的存在是在虚表中的,也就说明了那个指针确实是Func3。至于监视窗口没有显示Func3函数的地址,可能是由于编译器的bug

在这里插入图片描述

这里其实也说明了一件事,我们不要太过于相信监视窗口,只有内存窗口里面的才是最真实的

不过需要注意的是,上面的代码其实是被精心设计过的,它并不是正常的访问方式,首先我们的虚表中每一个函数我们的类型都设置成了一模一样的,否则的话在调用函数的时候必然因为指针的类型不同而出现问题。

其次我们的函数都是没有访问成员变量的,一旦函数里面存在访问成员变量的话,可能会出现很多问题。毕竟我们的是非正常访问,是没有this指针的。这里的非正常访问方式是无视类域的限制的,即便是私有的照样可以访问。因为他们都只是语法层面的限制,我们这里直接从内存中去找到对应的地址去调用的。

七、动态绑定与静态绑定

我们有时候又将多态分为静态的多态与动态的多态

所谓静态的多态,一般是指编译时的多态,也就是函数重载

比如下面的例子:

int main()
{
	int i = 1;
	double d = 1.1;
	cout << i << endl;
	cout << d << endl;
	return 0;
}

即不同的对象调用不同的函数,这些是在编译时候就确定好了的。通过函数名修饰规则等,来匹配不同的函数。我们也将之称为静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态 。它与普通的调用是一样的,在编译时就确定了地址

如下所示,现在所处的就是一个普通的调用。它在编译时就确定好了地址。
在这里插入图片描述

而下面这个则是多态的调用,编译器也不知道调用的到底是谁,反正就是通过一系列方法将这里面的函数给取出来去调用

在这里插入图片描述

这里也就是动态的多态,即运行时的多态,他是通过继承,虚函数重写实现的多态。


八、总结

本次主要讲解了多态的底层原理。深入浅出的讲解了虚函数表,深度剖析了多态的条件,以及虚表的很多细节。希望能对大家带来帮助

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

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

相关文章

数据分析作业2

中国在 2020 年开展第七次全国人口普查&#xff0c;截止 2021 年 5 月 11 日普查结果公布&#xff0c;全国人口共1411778724人。单从数据表格看相关数据不够直观&#xff0c;需要进行数据可视化展示&#xff0c;方便查看数据结果。 任务一&#xff1a;链接 MySQL 数据库&#x…

LeetCode 周赛上分之旅 #42 当 LeetCode 考树上倍增,出题的趋势在变化吗

⭐️ 本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。 学习数据结构与算法的关键在于掌握问题背后的算法思维框架&#xff0c;你的思考越抽象&#xff0c;它能覆盖的问题域就越广&#xff0c;理解难度…

webassembly003 ggml GGML Tensor Library part-2 官方使用说明

https://github.com/ggerganov/whisper.cpp/tree/1.0.3 GGML Tensor Library 官方有一个函数使用说明&#xff0c;但是从初始版本就没修改过 : https://github1s.com/ggerganov/ggml/blob/master/include/ggml/ggml.h#L3-L173 This documentation is still a work in progres…

【档案专题】七、电子档案利用与开发

导读&#xff1a;主要针对电子档案利用与开发相关内容介绍。对从事电子档案管理信息化的职业而言&#xff0c;不断夯实电子档案管理相关理论基础是十分重要。只有通过不断梳理相关知识体系和在实际工作当中应用实践&#xff0c;才能走出一条专业化加职业化的道路&#xff0c;从…

8.缓冲区管理

第五章 I/O管理 缓冲区管理 双缓冲区&#xff1a;T<CM 假设初始状态缓冲区1满&#xff0c;缓冲区2空&#xff0c;工作区为空。 刚开始缓冲区2为空&#xff0c;所以设备可以向缓冲区2中冲入数据耗时T&#xff0c;另一方面刚开始缓冲区1中是满的&#xff0c;所以刚开始就可…

免费OCR图像识别文字识别API

免费OCR图像识别文字识别API 一、OCR图像识别文字识别二、使用步骤1、接口2、请求参数3、请求参数示例4、接口 返回示例 三、温馨提示 一、OCR图像识别文字识别 光学字符识别&#xff08;Optical Character Recognition, OCR&#xff09;是指对文本资料的图像文件进行分析识别…

[管理与领导-55]:IT基层管理者 - 扩展技能 - 1 - 时间管理 -2- 自律与自身作则,管理者管好自己时间的五步法

前言&#xff1a; 管理好自己的时间&#xff0c;不仅仅是理念&#xff0c;也是方法和流程。 步骤1&#xff1a;理清各种待办事项 当提到工作事项时&#xff0c;这通常指的是要完成或处理的工作任务或事务。这些事项可以包括以下内容&#xff1a; 任务分配&#xff1a;根据工作…

2024年天津市大学软件学院专升本专业课考试大纲

天津市大学软件学院2024年“高职升本科”联合招生专业考试大纲 一、考试性质 天津市大学软件学院“高职升本科”联合招生专业考试是由合格的高职高专毕业生参加的选拔性考试。学校根据考生的成绩&#xff0c;按照已确定的招生计划&#xff0c;德、智、体全面衡量&#xff0c;…

fat32 文件系统 误删除文件数据恢复 SDK 介绍

fat32 文件系统 误删除文件数据恢复 SDK 介绍 fat32_analyze.dll 是一个专门用于恢复 fat32 文件系统误删除文件的标准的动态链接库(DLL)&#xff0c; 可被任何其他程序直接加载调用。 下载地址&#xff1a; https://gitee.com/tankaishuai/powerful_sdks/tree/master/fat32_a…

利用多种机器学习方法对爬取到的谷歌趋势某个关键词的每日搜索次数进行学习

大家好&#xff0c;我是带我去滑雪&#xff01; 前一期利用python爬取了谷歌趋势某个关键词的每日搜索次数&#xff0c;本期利用爬取的数据进行多种机器学习方法进行学习&#xff0c;其中方法包括&#xff1a;随机森林、XGBOOST、决策树、支持向量机、神经网络、K邻近等方法&am…

导入excel数据给前端Echarts实现中国地图-类似热力图可视化

导入excel数据给前端Echarts实现中国地图-类似热力图可视化 程序文件&#xff1a; XinqiDaily/frontUtils-showSomeDatabaseonMapAboutChina/finalproject xin麒/XinQiUtilsOrDemo - 码云 - 开源中国 (gitee.com) https://gitee.com/flowers-bloom-is-the-sea/XinQiUtilsOr…

第五章 树与二叉树 二、二叉树的定义和常考考点

一、定义 二叉树可以用以下方式详细定义&#xff1a; 二叉树是由节点构成的树形结构&#xff0c;每个节点最多可以有两个子节点。每个节点有以下几个属性&#xff1a; 值&#xff1a;存储该节点的数据。左子节点&#xff1a;有一个左子节点&#xff0c;如果没有则为空。右子节…

大数据(四)主流大数据技术

大数据&#xff08;四&#xff09;主流大数据技术 一、写在前面的话 To 那些被折磨打击的好女孩&#xff08;好男孩&#xff09;&#xff1a; 有些事情我们无法选择&#xff0c;也无法逃避伤害。 但请你在任何时候都记住&#xff1a; 你可能在一些人面前&#xff0c;一文不值&a…

基于内存池的 简单高效的数据库 SDK简介

基于内存池的 简单高效的数据库 SDK简介 下载地址&#xff1a; https://gitee.com/tankaishuai/powerful_sdks/tree/master/shm_alloc_db_heap shm_alloc_db_heap 是一个基于内存池实现的简单高效的文件型数据存储引擎&#xff0c;利用它可以轻松地像访问内存块一样读、写、增…

国产系统下开发QT程序总结

国产系统下开发QT程序总结 1. 国产系统简介 开发国产系统客户端的过程中&#xff0c;会出现兼容性问题。以下介绍Kylin和UOS环境下开发QT程序&#xff0c; 首先麒麟和统信这两个系统基于Ubuntu开发的。所以在Ubuntu开发理论上在国产系统上也能运行。芯片架构又分为amd,arm,mi…

谷歌翻译国内使用

谷歌已经退出中国市场&#xff0c;如果正常想使用的谷歌翻译的话&#xff0c;需要科学上网才可以 一些涉及到谷歌翻译的软件工具软件也无法正常使用&#xff0c;如chrome浏览器右键翻译&#xff0c;potplayer在线字幕实时翻译等等 目前最有效的解决方法就是通过修改hosts文件来…

C语言链表梳理-2

链表头使用结构体&#xff1a;struct Class 链表中的每一项使用结构体&#xff1a;struct Student#include <stdio.h>struct Student {char * StudentName;int StudentAge;int StudentSex;struct Student * NextStudent; };struct Class {char *ClassName;struct Stude…

web、HTTP协议

目录 一、Web基础 1.1 HTML概述 1.1.1 HTML的文件结构 1.2 HTML中的部分基本标签 二.HTTP协议 2.1.http概念 2.2.HTTP协议版本 2.3.http请求方法 2.4.HTTP请求访问的完整过程 2.5.http状态码 2.6.http请求报文和响应报文 2.7.HTTP连接优化 三.httpd介绍 3.1.http…

前端基础(Element、vxe-table组件库的使用)

前言&#xff1a;在前端项目中&#xff0c;实际上&#xff0c;会用到组件库里的很多组件&#xff0c;本博客主要介绍Element、vxe-table这两个组件如何使用。 目录 Element 引入element 使用组件的步骤 使用对话框的示例代码 效果展示 vxe-table 引入vxe-table 成果展…

不使用ip和port如何进行网络通讯(raw socket应用例子)

主要应用方向是上位机和嵌软(如stm32单片机)通讯&#xff0c;不在单片机中嵌入web server&#xff0c;即mac层通讯。 一、下面先了解网络数据包组成。 常见数据包的包头长度: EtherHeader Length: 14 BytesTCP Header Length : 20 BytesUDP Header Length : 8 BytesIP Heade…