C++(多态)


目录

前言:

1.多态的概念

2.多态的定义及实现 

 2.1多态的构成条件

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

2.3虚函数重写 

2.4C++ override 和final

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

3.多态的原理

3.1虚表与续表指针 

3.2动态绑定与静态绑定

4单继承与多继承 

4.1单继承中虚表

4.2多继承中虚表 

4.2.1子类新增虚表归属问题 

 4.2.2多继承虚函数调用问题

4.3菱形继承多态与菱形虚拟继承多态 



前言:

上一章节对面向对象三大特性的继承做了知识复盘,本章节对最后一个特性多态做一个知识梳理和总结。

1.多态的概念

         通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态。可以举个现实中车站买票的例子,同一个窗口,不同年龄多,不同职业的对象去买票价格是不同的(这就是多种形态也就是多态)

2.多态的定义及实现 

 2.1多态的构成条件

        实现多态需要借助虚表,这里的虚表指的是虚函数表,虽然也是借助关键字:virtual, 这里需要和继承里面的虚拟继承区分开,两个概念不能搞混。有了虚函数表,就可以使用父类指针进行不同对象调用实现不同形态(接下来会仔细介绍)

        继承中构成多态的两个条件:

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

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

        我们通过一个买票的demo 理清楚多态的流程

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

 注意:除了上面提到的构成多态的两个必要条件,有两个例外是需要注意的

  • 除父类外,其他子类中的函数不必使用 virtual 修饰,此时仍然能构成多态(注意三同,需要构成重写)
  • 父子类中的虚函数返回值可以不相同,但此时需要返回对应的父类指针或子类指针,确保构成多态,这一现象称为 协变(了解)
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;}
};

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

1-析构函数可以是虚函数吗?为什么需要是虚函数?
2-析构函数加virtual,是不是虚函数重写? 

        答案是肯定的,如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

      为什么要这么处理呢?是要要让他们构成重写吗 那为什么要让他们构成重写呢?我们可以用一个demo 来解释为什么要这么做:

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }~Person() { cout << "~Person()" << endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }~Student() {cout << "~Student()" << endl;delete[] ptr;}protected:int* ptr = new int[10];
};int main()
{//Person p;//Student s;Person* p = new Person;p->BuyTicket();delete p;p = new Student;p->BuyTicket();delete p; // p->destructor() + operator delete(p)// 这里我们期望p->destructor()是一个多态调用,而不是普通调用return 0;
}

        此时 只有BuyTicket函数和父类构成了虚函数重写,且都是由父类指针进行的调用,所以我们会看到买票的多态,但是析构函数并未构成虚函数重写(既不是虚函数也不是重写)再调用delete p的时候,他只是一个普通对象,当前类型为Person* 所以只会去调用父类的析构,从而造成内存泄漏。

        虽然编译器对析构函数名称做了特殊处理,编译后嘻哈猴函数的名称统一处理成 destructor 

我们还是希望p->destrctor()能够是一个多态调用,而不是普通调用,那就是构成虚函数重写,所以我们就能理解,为什么父类成员必须加上virtual。修改完成后就不会造成内存泄漏了,代码如下:

总结: 

如何快速判断是否构成多态?

  • 首先观察父类的函数中是否出现了 virtual 关键字
  • 其次观察是否出现虚函数重写现象,三同:返回值、函数名、参数(协变例外)
  • 最后再看调用虚函数时,是否为【父类指针】或【父类引用】

父类指针或引用调用函数时,如何判断函数调用关系?

  • 若满足多态:看其指向对象的类型,调用这个类型的成员函数
  • 不满足多态:看具体调用者的类型,进行对应的成员函数调用

2.3虚函数重写 

        通过上面的介绍,我们知道虚函数是构成多态的必要条件,我们也知道想要构成多态,还需要实现重写,可是重写具体怎么实现,我们好像一笔带过,下面我将用代码,虚函数表的具体演示,派生类如何实现覆盖,重写,以及重写了什么

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

 

结果解析:初始化两个对象的时候,子类继承父类且都是是虚函数,会创建两张虚函数表,我们发现虚表指针的地址不一样,所以第一步,是两张虚表,当使用父类指针调用的时候,就会去完成重写,将父类的虚表复制,将自己的虚函数函数进行覆盖,但是重写的是实现方法,也就是外壳,内容是不会改变的,所以最后的结果就是 B:1

补充:

         我们已知多态的条件之一就是父类的指针或者引用去调用,那为什么不能子类的指针或者引用去调用呢?为啥不能是父类对象呢?

        答:1因为是复制父类的虚表进行重写,如果是父类调用父类就不用重写,父类调用子类就重写子类属于自己的那部分,如果用子类指针 永远无法调用到父类;

               2 子类赋值给父类对象切片,不会拷贝虚表,如果拷贝虚表,那么父类对象虚表中是父类虚函数还是子列就不确定,会乱套。

2.4C++ override 和final

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

  • final:修饰虚函数,表示该虚函数不能再被重写,对于父类的虚函数,如果加上final就不能被重写,也就无法实现多态

  • override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

 

 

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

         

截至目前为止,我们已经学习了三个 “重” 相关函数知识:重载、重写、重定义

这三兄弟不止名字很像,而是功能也都差不多,很多面试题中也喜欢考这三者的区别

重载:即函数重载,函数参数 不同而触发,不同的 函数参数 最终修饰结果不同,确保链接时不会出错,构成重载

重写(覆盖):发生在类中,当出现虚函数且符合重写的三同原则时,则会发生重写(覆盖)行为,具体表现为 父类虚函数接口 + 子类虚函数体,是实现多态的基础

重定义(隐藏):发生在类中,当子类中的函数名与父类中的函数名起冲突时,会隐藏父类同名函数,默认调用子类的函数,可以通过 :: 指定调用

重写和重定义比较容易记混,简言之 先看看是否为虚函数,如果是虚函数且三同,则为重写;若不是虚函数且函数名相同,则为重定义

   


3.多态的原理

         之前提到过多态需要虚函数表,以及指向虚函数标的指针,我们可以写一个空类,通过测试大小验证一下:

class Parent
{virtual void func() {};
};int main()
{Parent p;	cout << "Parent : " << sizeof(p) << endl;return 0;
}

       

        通过验证我们发现,一个带有虚函数的类的大小在64位平台下是8,因此也就验证了我们猜想,虚函数的类中包含一个虚表指针 。虚表指针->虚表 实现多态。

3.1虚表与续表指针 

        虚函数表(虚表)即 virtual function table -> vft,指向虚表的指针称为 虚表指针 virtual function pointer -> vfptr,在 vs 的监视窗口中,可以看到涉及虚函数类的对象中都有属性 __vfptr(虚表指针),可以通过虚表指针所指向的地址,找到对应的虚表。虚函数表中存储的是虚函数地址,可以在调用函数时根据不同的地址调用不同的方法。

        在下面这段代码中,父类 Person 有两个虚函数(func3 不是虚函数),子类 Student 重写了 func1 这个虚函数,同时新增了一个 func4 虚函数

#include <iostream>using namespace std;class Person
{
public:virtual void func1() { cout << "Person::fun1()" << endl; };virtual void func2() { cout << "Person::fun2()" << endl; };void func3() { cout << "Person::fun3()" << endl; };	//fun3 不是虚函数
};class Student : public Person
{
public:virtual void func1() { cout << "Student::fun1()" << endl; };virtual void func4() { cout << "Student::fun4()" << endl; };
};int main()
{Person p;Student s;return 0;
}

如何通过程序验证虚表的真实性?

  • 虚表指针指向虚表,虚表中存储的是虚函数地址,而 64 位平台中指针大小为 8字节
  • 因此可以先将虚表指针强转为 指向首个虚函数 的指针,然后遍历虚表打印各个虚函数地址验证即可。
  • vs 中对虚表做了特殊处理:在虚表的结尾处放了一个 nullptr,因此下面这段代码可能在其他平台中跑不了。
     

 

typedef void (*VF_T)();//函数指针 为下面函数指针数组做铺垫class Person
{
public:virtual void func1() { cout << "Person::fun1()" << endl; };virtual void func2() { cout << "Person::fun2()" << endl; };void func3() { cout << "Person::fun3()" << endl; };	//fun3 不是虚函数
};class Student : public Person
{
public:virtual void func1() { cout << "Student::fun1()" << endl; };virtual void func4() { cout << "Student::fun4()" << endl; };
};
void test(VF_T table[])
{int i = 0;while(table[i]){printf(" [%d]:%p->", i, table[i]);//虚函数表里面存的是虚函数地址 直接解引用就是该虚函数VF_T f = table[i];f();i++;}cout << endl;
}
int main()
{Person p;Student s;test((VF_T*)(*(int*)&p));test((VF_T*)(*(int*)&s));return 0;
}

        

 因为平台不同指针大小不同,因此上述传递参数的方式(VF_T*)(*(int*)&p 具有一定的局限性
假设在 64 位平台下,需要更改为 (VF_T*)(*(long long*)&p

综上所述,虚表是真实存在的,只要当前类中涉及了虚函数,那么编译器就会为其构建相应的虚表体系

虚表相关知识补充:

  • 虚表是在 编译 阶段生成的
  • 虚表指针是在构造函数的 初始化列表 中初始化的
  • 虚表一般存储在 常量区(代码段),有的平台中可能存储在 静态区(数据段)

 

int main()
{//验证虚表的存储位置Person p;Student s;int a = 10;	//栈int* b = new int;	//堆static int c = 0;	//静态区(数据段)const char* d = "xxx";	//常量区(代码段)printf("a-栈地址:%p\n", &a);printf("b-堆地址:%p\n", b);printf("c-静态区地址:%p\n", &c);printf("d-常量区地址:%p\n", d);printf("p 对象虚表地址:%p\n", *(VF_T**)&p);printf("s 对象虚表地址:%p\n", *(VF_T**)&s);return 0;
}

 

显然,虚表地址与常量区的地址十分接近,因此可以推测 虚表位于常量区中,因为它需要被同一类中的不同对象共享,同时不能被修改(如同代码一样)

函数代码也是位于 常量区(代码段),可以在监视窗口中观察两者的差异

 

3.2动态绑定与静态绑定

 静态绑定(前期绑定/早绑定)

  • 在编译时确定程序的行为,也称为静态多态

动态绑定(后期绑定/晚绑定)

  • 在程序运行期间调用具体的函数,也称为动态多态

 

p1->func1();
p2->func1();add(1, 2);
add(1.1, 2.2);

        简单来说,静态绑定就像函数重载,在编译阶段就确定了不同函数的调用;而动态绑定是虚函数的调用过程,需要 虚表指针+虚表,在程序运行时,根据不同的对象调用不同的函数

 


4单继承与多继承 

         需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类 的虚表模型前面我们已经看过了,没什么需要特别研究的。

4.1单继承中虚表

我们上面研究的基本都是子类继承父类,对父类中的虚函数进行覆盖重写。 

 

向父类中新增虚函数:父类的虚表中会新增,同时子类会继承,并纳入自己的虚表之中

向子类中新增虚函数:只有子类能看到,因此只会纳入子类的虚表中,父类是看不到并且无法调用的

向父类/子类中添加非虚函数时:不属于虚函数,不进入虚表,仅当作普通的类成员函数处理

4.2多继承中虚表 

   C++ 中支持多继承,这也就意味着可能出现 多个虚函数重写 的情况,当父类指针面临 不同虚表中的相同虚函数重写 时,该如何处理呢?  

#include <iostream>
using namespace std;//父类1
class Base1
{
public:virtual void func1() { cout << "Base1::func1()" << endl; }virtual void func2() { cout << "Base1::func2()" << endl; }
};//父类2
class Base2
{
public:virtual void func1() { cout << "Base2::func1()" << endl; }virtual void func2() { cout << "Base2::func2()" << endl; }
};//多继承子类
class Derive : public Base1, public Base2
{
public:virtual void func1() { cout << "Derive::func1()" << endl; }virtual void func3() { cout << "Derive::func3()" << endl; }	//子类新增虚函数
};int main()
{Derive d;return 0;
}

此时,derive继承了base1和base2,所以derive有两张虚表,分别为 Base1 + Derive::func1 构成的虚表Base2 + Derive::func1 构成的虚表 

 

此时出现了两个问题:

  1. 子类 Derive 中新增的虚函数 func3 位于哪张虚表中?
  2. 为什么重写的同一个 func1 函数,在两张虚表中的地址不相同?

下面我们对这两个问题做一个深度解析。

4.2.1子类新增虚表归属问题 

         在单继承中,子类中新增的虚函数会放到子类的虚表中,因为只有一张表我们没有疑问,多继承中,子类中新增的虚函数默认添加至第一张虚表中,我们可以通过test打印进行验证,因为此时有两张虚表,所以需要分别打印;第一张虚表的地址和子类的首地址重合,只需要取地址+类型强转;第二张虚表就比较麻烦,需要在第一张虚表的起始地址处,跳过第一张虚表的大小,然后才能获取第二张虚表的起始地址。

 

//打印虚表
typedef void(*VF_T)();void test(VF_T table[])
{//vs中在虚表的结尾处添加了 nullptrint i = 0;while (table[i]){printf("[%d]:%p->", i, table[i]);VF_T f = table[i];f();	//调用函数,相当于 func()i++;}cout << endl;
}int main()
{Derive d;test(*(VF_T**)&d);	//第一张虚表test(*(VF_T**)((char*)&d + sizeof(Base1)));	//第二张虚表return 0;
}

 

        可以看出新增的 func3 函数确实在第一张虚表中;可能有的人觉得取第二张虚表的起始地址很麻烦,那么可以试试利用 切片 机制,天然的取出第二张虚表的地址切片行为是天然的,可以完美取到目标地址.

Base2* table2 = &d;	//切片
PrintVFTable(*(VF_T**)table2);	//第二张虚表

 4.2.2多继承虚函数调用问题

        在上面的多继承多态代码中,子类分别重写了两个父类中的 func1 函数,但最终通过监视窗口发现:同一个函数在两张虚表中的地址不相同;因此可以推测:编译器在调用时,根据不同的地址寻找到同一函数,解决冗余虚函数的调用问题至于实际调用链路,还得通过汇编代码展现:

 

 

        ptr2 在调用时的关键语句 sub ecx 4;sub 表示减法,ecx 通常存储 this 指针4 表示 Base1 的大小;这条语句表示将当前的 this 指针向前偏移 sizeof(Base1),后续再 jmp 时,调用的就是同一个 func1;这一过程称为 this 指针修正,用于解决冗余虚函数的调用问题

        为什么是 Base2 修正?因为先继承了 Base1,后继承了 Base2,假设先继承的是 Base2,那么修正的就是 Base1这种设计很大胆也很巧妙,完美解决了多继承多态带来的问题因此回答问题二:两张虚表中同一个函数的地址不同,是因为调用方式不同,后继承类中的虚表需要通过 this 指针修正的方式调用虚函数。

4.3菱形继承多态与菱形虚拟继承多态 

        菱形继承问题是 C++ 多继承中的大坑,为了解决菱形继承问题,提出了 虚继承 + 虚基表 的相关概念,那么在多态的加持之下,菱形继承多态变得更加复杂:需要函数调用链路设计的更加复杂菱形虚拟继承多态就更不得了:需要同时考虑两张表:虚表、虚基表

  • 虚基表中空余出来的那一行是用来存储偏移量的:表示当前虚基表距离虚表有多远
     

 

 

 

 

 

 

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

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

相关文章

css 设备背景图片 宽高总是不能平铺

宽高总是宽大了 高就挤出去了&#xff1b;高设置了 宽度就变小了&#xff1b;疯掉的节奏。。。。。。 .center-bottom{background: url(/img/newpic/leftbg.png);background-repeat: no-repeat;width: 98%;height: 60%;background-position: center center;background-size: 1…

基于YOLOv8深度学习的智能玉米害虫检测识别系统【python源码+Pyqt5界面+数据集+训练代码】目标检测、深度学习实战

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

自定义Taro上传图片hooks(useUploadImg)

有两个方法需要提前引入 FileUtil(上传文件的方法)、to&#xff08;对请求接口返回做了二次处理&#xff0c;数据和错误提示等&#xff09; //FileUtil export namespace FileUtil {const env {timeout: 10000,uploadImageUrl: "阿里云的地址",};const genPolicy …

微软的word文档中内置背景音乐步骤(打开自动播放)

目录 一、前言 二、操作步骤 一、前言 有时候需要在word文档里面打开的时候就自动播放音乐或者音频&#xff0c;那么可以用微软的word来按照操作步骤去这样完成。 如果没有微软office的&#xff0c;可以下载这个是2021专业版的。因为office只能免费使用一段时间&#xff0c…

融资项目——vue之事件监听

vue通过v-on进行事件监听&#xff0c;在标签中使用v-on:xxx&#xff08;事件名称&#xff09;进行监听&#xff0c;事件触发的相应方法定义在Vue对象中的methods中。如下图所示&#xff1a; 上述代码对按钮进行监听&#xff0c;点击按钮后就会触发solve函数。

如何将图片(matlab、python)无损放入word论文

许多论文对插图有要求&#xff0c;直接插入png、jpg一般是不行的&#xff0c;这是一篇顶刊文章&#xff08;pdf&#xff09;的插图&#xff0c;放大2400%后依旧清晰&#xff0c;搜罗了网上的方法&#xff0c;总结了一下如何将图片无损放入论文中。 这里主要讨论的是数据生成的图…

LTO-3 磁带机种草终于是用上了

跑来跑去&#xff0c;买了不少配件&#xff0c;终于是把这磁带机给用上了&#xff0c;已经备份好了300 多 GB 的数据。 我们用了 NAS 的数据压缩功能&#xff0c;把需要备份的文件用 NAS 压缩成一个 Zip 文件&#xff0c;如果你可以 tar 的话也行。 这样传输速度更快&#xf…

Linux系统中的地址映射

一. 简介 在前面的裸机开发实验 LED灯实验中 &#xff0c;其实就是操作 IMX6ULL芯片的寄存器。 Linux 驱动开发也可以操作寄存器&#xff0c;但是&#xff0c;Linux不能直接对寄存器物理地址进行读写操作&#xff0c;例如&#xff0c;寄存器 A的物理地址为 0X01010101。 裸机…

基于STM32的HC-SR501红外感应模块驱动与应用

一、 简介 HC-SR501红外感应模块是一种常用的人体红外感应模块&#xff0c;常用于安防监控、智能家居等领域。本文将介绍如何在STM32单片机上驱动和应用HC-SR501红外感应模块&#xff0c;实现基本的人体检测功能。 二、 模块原理 HC-SR501红外感应模块基于红外热释电传感器&am…

C++设计模式 #3策略模式(Strategy Method)

动机 在软件构建过程中&#xff0c;某些对象使用的的算法可能多种多样&#xff0c;经常改变。如果将这些算法都写在类中&#xff0c;会使得类变得异常复杂&#xff1b;而且有时候支持不频繁使用的算法也是性能负担。 如何在运行时根据需求透明地更改对象的算法&#xff1f;将…

DRF之引入

目录 一、web应用模式 【1】前后端混合开发 【2】前后端分离 二、API接口 三、接口测试工具&#xff1a;Postman 四、RESTful API规范 【1】什么是RESTful 【2】RESTful API的规范 2.1 数据的安全保障 2.2 接口特征表现 2.3 多数据版本共存 2.4 数据即是资源&#…

Python并行计算和分布式任务全面指南

更多Python学习内容&#xff1a;ipengtao.com 大家好&#xff0c;我是彭涛&#xff0c;今天为大家分享 Python并行计算和分布式任务全面指南。全文2900字&#xff0c;阅读大约8分钟 并发编程是现代软件开发中不可或缺的一部分&#xff0c;它允许程序同时执行多个任务&#xff0…

Win10纯净版edge浏览器改为ie浏览器的方法

在Win10纯净版系统中&#xff0c;用户喜欢使用ie浏览器来浏览页面&#xff0c;但是电脑默认浏览器为edge浏览器&#xff0c;所以想把edge浏览器改成ie浏览器&#xff0c;但不知道更改的操作方法。下面小编给大家详细介绍将Win10电脑默认浏览器edge浏览器改为ie浏览器的方法步骤…

文献速递:生成对抗网络医学影像中的应用——基于CycleGAN的图像到图像转换,用于逼真的外科手术训练模型

文献速递&#xff1a;生成对抗网络医学影像中的应用——基于CycleGAN的图像到图像转换&#xff0c;用于逼真的外科手术训练模型 本周给大家分享文献的主题是生成对抗网络&#xff08;Generative adversarial networks, GANs&#xff09;在医学影像中的应用。文献的研究内容包括…

一文掌握分布式锁:Mysql/Redis/Zookeeper实现

目录 一、项目准备spring项目数据库 二、传统锁演示超卖现象使用JVM锁解决超卖解决方案JVM失效场景 使用一个SQL解决超卖使用mysql悲观锁解决超卖使用mysql乐观锁解决超卖四种锁比较Redis乐观锁集成Redis超卖现象redis乐观锁解决超卖 三、分布式锁概述四、Redis分布式锁实现方案…

人大金仓Kingbase数据库备份和还原

前言 最近在项目开发过程中&#xff0c;使用了国产数据库人大金仓&#xff08;即Kingbase数据库&#xff09;&#xff0c;在使用过过程中需要对数据库进行备份与还原&#xff0c;在此对相关的命令进行简单介绍&#xff0c;以备不时之需。 Linux环境下安装人大金仓可参考此篇文…

ECMAScript基础入门:猫头虎博主的技术分享

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

Spring中你一定要知道的afterPropertiesSet()

文章目录 功能源码 功能 初始化bean执行的回调方法其一&#xff0c;它不像PostConstruct一样可以有多个&#xff0c;只能调用一次&#xff1b;它执行的时机是在PostConstruct之后&#xff0c;从它的名称也可以看出&#xff0c;他是在属性填充完&#xff0c;也就是bean初始化完…

实力强的大模型都有哪些超能力?

实力强的大模型都有哪些超能力&#xff1f; 前几日&#xff0c;人工智能研究公司OpenAI CEO山姆奥特曼&#xff08;Sam Altman&#xff09;在谈及人工智能这项技术的潜力以及人们对它的担忧时&#xff0c;曾表示“AI发展速度快得吓人&#xff0c;就像停不下来的龙卷风。”可见&…

如何使用 NFTScan NFT API 在 Base 网络上开发 Web3 应用

Base 是 Coinbase 使用 OP Stack 开发的最新以太坊第 2 层&#xff08;L2&#xff09;网络&#xff0c;用于解决以太坊等主要区块链面临的可扩展性和成本挑战。Coinbase 将其描述为“安全、低成本、对开发人员友好的以太坊 L2&#xff0c;旨在将下一个 10 亿用户带入 Web3”。B…