深度理解多态的底层实现


前言

首先先回顾一下上次的知识

一、多态的概念
多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态),这⾥我们重点讲运⾏时多态,编译时多态(静态多态)和运⾏时多态(动态多态)。编译时多态(静态多态)主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。

运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态。⽐如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。再⽐如,同样是动物叫的⼀个⾏为(函数),传猫对象过去,就是”(>ω<)喵“,传狗对象过去,就是"汪汪"。

二、多态的定义及实现

多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如Student继承了Person。Person对象买票全价,Student对象优惠买票。

2.实现多态的重要条件:
必须是基类的指针或者引⽤调⽤虚函数
被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖
多态必须存在于继承和派生类之间


一、多态的原理

1.1 虚函数表

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

在这里插入图片描述
在这里插入图片描述

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个_vftptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function,t代表table)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。


下面我们看一看基类和派生类的虚表里面有什么?


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

void Func(Person& p)
{
	p.BuyTicket();
}
	
void test()
{
	Person ps1;
	Student st1;
}

int main()
{
	Person ps;
	Student st;
	st._a = 10;
	
	ps = st;
	Person* ptr = &st; //指向父类对象看到的父类的虚表
	Person& ref = st;  //指向子类对象看到的子类中父类的那一部分的虚表
		
	test();
	
	return 0;
}

在这里插入图片描述
在这里插入图片描述

派生类的虚表是怎么生成的呢?
只要是虚函数就会被放入虚表,可以认为派生类的虚表是先把父类的虚表先拷贝过来然后再把派生类重写过的虚函数在父类的虚表上进行覆盖,而没有被重写的虚函数就被继承了下来保持不变。
派生类自己的虚函数写在后面(VS环境下在监视窗口不会显示,得去内存窗口查看)

总结一下派生类的虚表中包括:

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

  • 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表。

  • 在这里插入图片描述

  • 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。

  • 派⽣类中重写了基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。覆盖就是指虚表中虚函数
    的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

  • 派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,(3)派⽣类自己的虚函数地址三个部分。

  • 虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000标记,g++系列编译不会放) 。

  • 虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。

  • 虚函数表存在哪的? 这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以对⽐验证⼀下。vs下是存在代码段(常量区)

  • 从操作系统层面来说是代码段,而从语言的角度来说是叫常量区

那么虚表取的时候有涉及到大小端,判断大端小端怎么把低位的第一个字节取出来,如果低位的字节为1低位存低地址,int强转成char,取地址是int是四个字节,但是是指向第一个字节的开始,解引用看4个字节,类型决定看多大,强转成char就看第一个字节。具体看大小端博客讲解。

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


看一下虚表存在哪里

int main()
{
	Person ps;
	Student st;

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

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

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

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

	printf("虚表1:%p\n", *((int*)&ps));
	printf("虚表2:%p\n", *((int*)&st));


	return 0;
}

//宏定义
typedef void(*FUNC_PTR) ();

 打印函数指针数组   看一看Func到底在不在派生类虚函数表中
// void PrintVFT(FUNC_PTR table[])
void PrintVFT(FUNC_PTR* table)
{
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);

		FUNC_PTR f = table[i];
		//正常应该是通过对象去调用,这里直接通过取地址,
		f();
	}
	printf("\n");
}

int main()
{
	Person ps;
	Student st;

	int vft1 = *((int*)&ps);
	PrintVFT((FUNC_PTR*)vft1);

	int vft2 = *((int*)&st);
	PrintVFT((FUNC_PTR*)vft2);

	return 0;
}

1.2 静态绑定与动态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

编译时多态通过不同参数匹配实现调用不同的函数,函数模板编译时根据实际调用,对函数模板进行实例化出三个函数,通过重载去匹配对应的函数。这两个都是在编译时进行匹配和实例化
编译时通过参数确定的,达到不同的参数调用不同的函数,形成多种形态
在语法层完成某个行为就是调某个函数,之前都是通过参数匹配

满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到指定对象的中虚表去找的。不满足多态的函数调用时编译时确认好的。

运行起来到指定对象的虚表里去找对应的地址call进行调用

 多态
 静态(编译时)的多态,函数重载,函数模板
 动态(运行时)的多态, 通过继承,虚函数重写实现多态。核心机制是虚函数表和虚函数表指针
int main()
{
	int i = 1;
	double d = 1.1;
	cout << i << endl;
	cout << d << endl;

	Person ps;
	Person* ptr = &ps;

	ps.BuyTicket();
	ptr->BuyTicket();

	return 0;
}

1.3 多继承中的虚函数表

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

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

派生类没必要单独产生虚表,因为派生类继承父类,派生类里面的父类就包含虚表。这样就够了就可以实现多态。
多态是父类的指针或引用,指向父类对象时找的时父类的虚函数,指向子类时切片切出子类当中父类的那一部分,去这部分找出被子类重写的虚函数覆盖的那部分即可。

这个虚表也可以算是子类自己的,子类中的父类也算是子类的成员,并且虚表也不是和父类共用的是把父类的拷贝下来自己再进行覆盖等等。


这里还有一个为什么重写func1,但Base1和Base2的虚表中Func1的地址不一样?
这里就涉及到this指针,ecx,call eax地址,jmp 反汇编
因为Derive和Base1起始地址一样,因为Derive先继承的Base1,而Base2调用Func1的时候需要sub ecx,8;找到func1的实际内存地址进行调用;

地址也可以一样,再提前修正一步
在这里插入图片描述


二、注意事项

1 、虚函数重写中参数列表相同这里注意有坑

注意虚函数重写的坑(参数列表)

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();
	因为子类继承父类的成员函数test, 只是子类可以访问test函数,
	但是这个test函数本身还是父类对象的,
	这个时候这个test函数中的this指针依旧还是父类的
	是A*调用test()中的func();
	return 0;
	
	/*不构成多态就看它的类型,构成多态就看其指向的对象*/
}

B能调用test()是因为继承,  this->func()
                       A*
 p->test(); 传一个B*给A*,切片,看的是A对象,调用的是A的func  B*转A*  例如:B bb; A* p = &bb;
 本质是让A*指向了B的对象

子类继承父类的时候,并不会把父类的成员变量/函数拷贝下来,两个类还是独立的只是说在生成一个B的对象的时候里面由两部分构成,一部分父类的,一部分子类的;

隐藏:
成员函数名相同且在两个不同的作用域就构成隐藏
会先在子类里面找,找到了子类就对父类形成隐藏
( 如果需要直接调用父类的需要加上域作用限定符指定访问;)
找不到就会去父类找;

因为隐藏的问题,在继承体系下有一个隐藏的说法,就是子类和父类同名函数,子类会隐藏父类,
所以直接用子类指针找,访问的是子类的,如果子类没有,那么子类就会继承父类的,去父类哪里寻找访问。


2、虚析构函数

为什么基类中的析构函数建议设计为虚函数?
因为多态的原因析构函数要统一名字。
某种情况下派生类可以不加virtual,也是为了这里析构函数。
如果要设计一个类,这个类想要被继承,就把基类的析构函数加上virtual写成虚函数;

class A
{
	public :
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能
构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
void func(A* ptr)
{
	//ptr->f();
	delete ptr;
	// ptr->调用各自的析构函数() //不构成多态时,就是普通对象会根据当前的类型调用两次A的析构函数
	// ptr->destructor()      //构成多态
	// operator delete(ptr)
}
int main()
{
	//这种情况没问题
	A* aa1;
	B* bb1;
	//这个OK
	//A* p1 = new A;
	//B* p2 = new B;
	//delete p1;
	//delete p2;

    这就情况就过不了
	func(new A);
	func(new B);
	//
	这个同理
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
	
	return 0;
}

析构函数的重写:
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以只要基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。

下⾯的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调⽤的A的析构函数,没有调⽤B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。(使用父类指针或者引用去操作子类对象,只会调用父类中的析构函数,并不会调用子类的析构函数)。

涉及到一个知识点:
派生类的析构函数不需要显示调用父类的析构函数,会自动调用


3、构成多态的必要条件思考

构成多态的条件(为什么不能是派生类的指针或引用?为什么不能用父类的对象)
1.必须是基类的指针或引用调用虚函数
2.虚函数的重写(接口继承)

一、必须是基类的指针或引用调用虚函数:
1、为什么不能是派生类的指针或引用?
答:
因为只有父类才可以即可以指向父类的对象又可以指向子类的对象,实现指向父类调父类指向子类调父类。
如果是子类的指针,只能指向子类的对象,到虚表里找的时候只有子类的虚函数,不能实现出多态行为。

2、为什么不能用父类的对象?
答:
对象的切片和指针或引用是不同的,对象会发生拷贝。
子类赋值给父类对象切片时,不会拷贝虚表。如果拷贝虚表,把子类的虚表拷贝到父类那么再用父类的指针或引用指向父类的对象反而会调用子类。那么父类对象虚表中是父类的虚函数还是子类的虚函数就不确定了。

二、虚函数的重写(接口继承)
虚函数的重写重写的派生类函数的实现,用的还是父类的声明;
在这里插入图片描述
只有实现了虚函数的重写才能实现指向派生类调用派生类的虚函数,指向基类调用基类的虚函数
因为没实现虚函数重写的话派生类的虚表里只会是基类虚表的拷贝,就算指向派生类也只会调用基类


总结

  • 多态的核心:通过虚函数重写和继承来实现运行时动态绑定。

  • 多态的核心意义:“通过继承和虚函数实现统一接口,多样实现”: 通过基类接口屏蔽不同子类的差异,实现一对多的调用逻辑,不同子类通过基类接口调用。

  • 关键点:虚函数重写、虚函数表、虚析构函数、final / override关键字。

  • 面向对象编程:只关注"做什么"(接口),而不是"怎么做"(具体实现)。

优势:

  • 提高代码灵活性:新增加子类时,无需修改已有代码,只需扩展新的子类即可(符合"开闭原则")。
  • 简化代码逻辑:通过统一的的接口处理多种对象类型,减少条件分支(如if / else或 switch)
  • 可维护性和扩展性:代码耦合度减低,不同子类的实现相互独立。新增子类不影响现有代码。
  • 工厂模式、策略模式等依赖多态实现,是设计复杂系统(框架,库)的基础。

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

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

相关文章

python中的异常-模块-包

文章目录 异常异常的定义异常捕获语法捕获常规异常捕获指定异常捕获多个异常捕获所有异常异常else异常finally 异常传递总结 模块概念导入自定义模块及导入main方法all变量 总结 包自定义包定义pycharm中建包的基本步骤导入方式 第三方包 异常 异常的定义 当检测到一个错误时…

亲测Windows部署Ollama+WebUI可视化

一. Ollama下载 登录Ollama官网(Ollama)点击Download进行下载 如果下载很慢可用以下地址下载&#xff1a; https://github.com/ollama/ollama/releases/download/v0.5.7/OllamaSetup.exe 在DeepSeek官网上&#xff0c;你可以直接点击【model】 到达这个界面之后&#xff0c;…

ios UICollectionView使用自定义UICollectionViewCell

和UITableView用法类似&#xff0c;UITableView主要是显示按行排列的数据&#xff0c;UICollectionView则用在显示多行多列的数据&#xff0c;今天我们继续来实现app下载页面的效果。 1.先自定义UICollectionViewCell&#xff0c;一个cell就相当于列表中的一项了。 记得勾上&a…

【个人开源】——从零开始在高通手机上部署sd(二)

代码&#xff1a;https://github.com/chenjun2hao/qualcomm.ai 推理耗时统计 单位/ms 硬件qnncpu_clipqnncpu_unetqnncpu_vaehtp_cliphtp_unethtp_vae骁龙8 gen124716.994133440.39723.215411.097696.327 1. 下载依赖 下载opencv_x64.tar,提取码: rrbp下载opencv_aarch64.t…

R 语言科研绘图第 27 期 --- 密度图-分组

在发表科研论文的过程中&#xff0c;科研绘图是必不可少的&#xff0c;一张好看的图形会是文章很大的加分项。 为了便于使用&#xff0c;本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中&#xff0c;获取方式&#xff1a; R 语言科研绘图模板 --- sciRplothttps://mp.…

REACT学习DAY02(恨连接不上服务器)

受控表单绑定 概念&#xff1a;使用React组件的状态&#xff08;useState&#xff09;控制表单的状态 1. 准备一个React状态值 const [value,setValue] useState() 2. 通过value属性绑定状态&#xff0c;通过onChange属性绑定状态同步的函数 <input type"text&quo…

一周学会Flask3 Python Web开发-response响应格式

锋哥原创的Flask3 Python Web开发 Flask3视频教程&#xff1a; 2025版 Flask3 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 在HTTP响应中&#xff0c;数据可以通过多种格式传输。大多数情况下&#xff0c;我们会使用HTML格式&#xff0c;这也是Flask中…

深度学习基础--ResNet网络的讲解,ResNet50的复现(pytorch)以及用复现的ResNet50做鸟类图像分类

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 前言 如果说最经典的神经网络&#xff0c;ResNet肯定是一个&#xff0c;这篇文章是本人学习ResNet的学习笔记&#xff0c;并且用pytorch复现了ResNet50&…

linux 驱动编程配置(minis3c2440)

1.介绍 1. 启动过程&#xff1a;启动u-boot------>>启动linux内核----->>挂载根文件系统 2. uboot是一个裸机程序&#xff0c;是一个bootloader&#xff0c;用于启动linux系统以及系统初始化 ubootloader主要完成了哪些任务&#xff1a;1. 初始化异常向量表&a…

【Excel】【VBA】根据内容调整打印区域

Excel VBA&#xff1a;自动调整打印区域的实用代码解析 在Excel中&#xff0c;我们经常需要调整打印区域。今天介绍一段VBA代码&#xff0c;它可以根据C列的内容自动调整打印区域。 Dim ws As Worksheet Dim lastRow As Long Dim r As Long 设置当前工作表 Set ws ActiveSh…

wps中zotero插件消失,解决每次都需要重新开问题

参考 查看zotero目录 D:\zotero\integration\word-for-windows 加载项点击 dotm即可 长期解决 把dom 复制到 C:\Users\89735\AppData\Roaming\kingsoft\office6\templates\wps\zh_CN还是每次都需要重新开的话 重新加载一下

【Java八股文】09-计算机操作系统面试篇

文章目录 计算机操作系统面试篇用户态和内核态的区别&#xff1f;用户态和内核态的区别&#xff1f; 进程管理线程和进程的区别是什么&#xff1f;进程&#xff0c;线程&#xff0c;协程的区别是什么&#xff1f;创建一个协程的过程线程运行过程中申请到的东西在切换时是否全部…

CPU安装pytorch(别点进来)

终于&#xff01; 深度学习环境配置5——windows下的torch-cpu1.2.0环境配置_requirement怎么写torch cu-CSDN博客

echarts找不到了?echarts社区最新地址

前言&#xff1a;在之前使用echarts的时候&#xff0c;还可以通过上边的导航栏找到echarts社区&#xff0c;但是如今的echarts变更之后&#xff0c;就找不到echarts社区了。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客 如今…

什么是事务?并发事务引发的问题?什么是MVCC?

文章目录 什么是事务&#xff1f;并发事务引发的问题&#xff1f;什么是MVCC&#xff1f;1.事务的四大特性2.并发事务下产生的问题&#xff1a;脏读、不可重复读、幻读3.如何应对并发事务引发的问题&#xff1f;4.什么是MVCC&#xff1f;5.可见性规则&#xff1f;参考资料 什么…

【算法基础】--前缀和

前缀和 一、一维前缀和示例模板[寻找数组的中心下标 ](https://leetcode.cn/problems/tvdfij/description/)除自身以外的数组乘积和可被k整除的子数组 一、一维前缀和 前缀和就是快速求出数组某一个连续区间内所有元素的和。 示例模板 已知一个数组arr&#xff0c;求前缀和 …

【含文档+PPT+源码】基于Django的新闻推荐系统的设计与实现

项目介绍 本课程演示的是一款基于Django的新闻推荐系统的设计与实现&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的 Python学习者。 1.包含&#xff1a;项目源码、项目文档、数据库脚本、软件工具等所有资料 2.带你从零开始部署运行本套系统 3.…

内容中台架构下智能推荐系统的算法优化与分发策略

内容概要 在数字化内容生态中&#xff0c;智能推荐系统作为内容中台的核心引擎&#xff0c;承担着用户需求与内容资源精准匹配的关键任务。其算法架构的优化路径围绕动态特征建模与多模态数据融合展开&#xff0c;通过深度强化学习技术实现用户行为特征的实时捕捉与动态更新&a…

如何在WPS打开的word、excel文件中,使用AI?

1、百度搜索&#xff1a;Office AI官方下载 或者直接打开网址&#xff1a;https://www.office-ai.cn/static/introductions/officeai/smartdownload.html 打开后会直接提示开始下载中&#xff0c;下载完成后会让其选择下载存放位置&#xff1a; 选择位置&#xff0c;然后命名文…

QML 实现一个动态的启动界面

QML 实现一个动态的启动界面 一、效果查看二、源码分享三、所用到的资源下载 一、效果查看 二、源码分享 工程结构 main.qml import QtQuick import QtQuick.Controls import QtQuick.Dialogs import Qt.labs.platformWindow {id:windowwidth: 640height: 400visible: truetit…