【C++】多态|原理|override|final|抽象类|多继承虚函数表|对象模型|虚表打印|(万字详解版)

目录

​编辑

一.多态的概念

二.多态的构建

虚函数

重写

虚函数重写的例外 

协变

隐藏

析构函数的重写 

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

四.C++11新增的 override 和 final 

override

final

五.抽象类 

 六.多态的原理

虚函数表

总结:

引用和指针如何实现多态  

虚函数表存放位置 

七.单继承和多继承关系的虚函数表 

 单继承中的虚函数表

打印虚表 

总结:

多继承中的虚函数表 

打印虚表 

打印第二章虚表

多继承多个虚表 

​编辑 总结:

八.菱形继承和菱形虚拟继承 

菱形继承

菱形虚拟继承

区别和联系: 

九.例题


一.多态的概念

多态是面向对象编程中的一个核心概念,它允许不同类的对象对同一消息做出响应,但具体的行为会根据对象的实际类型而有所不同。在C++中,多态主要通过虚函数来实现。

二.多态的构建

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
// 声明一个基类Person,其中包含一个虚函数BuyTicket
class Person
{
public:// 虚函数声明,使用virtual关键字,使得通过基类指针或引用调用该函数时能够根据实际对象类型动态绑定virtual void BuyTicket()		//虚函数,是建立多态的条件{cout << "全票" << endl;	// Person类中BuyTicket函数的行为是打印“全票”}
};// Student类是从Person类公有继承而来
class Student : public Person
{
public:// 重写了基类中的虚函数BuyTicket,这是多态的表现virtual void BuyTicket()		// 重写基类的虚函数{cout << "半票" << endl;	// Student类中BuyTicket函数的行为是打印“半票”}
};// 函数func接受一个Person类的引用作为参数
void func(Person& p)
{p.BuyTicket();	// 调用传入对象的BuyTicket函数,由于BuyTicket是虚函数,这里会根据实际对象类型执行对应版本的函数
}int main()
{Person p;			// 创建Person类的对象pStudent s;			// 创建Student类的对象s// 将基类和派生类的对象分别传递给func函数func(p);			// 输出“全票”,因为p是Person类型的对象func(s);			// 输出“半票”,尽管传入的是Student对象的引用,但通过基类引用调用,由于虚函数机制,会正确调用到Student的BuyTicketreturn 0;
}

注:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写(

因为派生类继承的是虚函数的声明),但是这种写法不规范,不建议这样使用 。

虚函数

virtual修饰的类成员函数称为虚函数。

注:和虚继承没有任何关系,只是一个关键词两用。

    virtual void BuyTicket()	//携带关键字 virtual的函数{cout << "虚函数" << endl;	}

重写

派生类中有个跟基类相同的虚函数,则称为重写

        相同指 三同:函数名相同,返回值相同,参数列表相同

class Base {
public:virtual void print() { // 虚函数声明cout << "Base Class" <<endl;}
};class Derived : public Base {
public:void print() {// 使用override重写父类的虚函数cout << "Derived Class" << endl;}
};int main() {Base* basePtr = new Derived();      // 基类指针指向子类对象basePtr->print();                // 输出 "Derived Class",展示了多态性delete basePtr; //释放return 0;
}

虚函数重写的例外 

协变和析构是例外

协变

派生类重写基类虚函数时,与基类虚函数返回值类型不同。

基类虚函数返回基类对象的指 针或者引用派生类虚函数返回派生类对象的指针或者引用时,称为协变,简单来说,就是基类的返回值和派生类的返回值是同样的继承关系

class A {};
class B : public A {};  //继承关系class Base {
public:virtual A* f()  //基类虚函数返回值是 另外一个基类的指针/引用{return new A;}
};
class Derive : public Base
{
public:virtual B* f()  //派生类重写了基类的虚函数,返回派生类对象的指针/引用{ return new B;}
};
隐藏

在了解析构函数的重写前,先了解隐藏

指在派生类中定义了一个与基类中同名的成员(可以是函数、变量等),但并没有使用virtual关键字进行虚函数的重写。这种情况下,基类的成员在派生类的上下文中被隐藏了,而不是被重写。这意味着,如果你有一个派生类的对象,并尝试通过派生类的引用或指针访问这个同名成员,你将访问到派生类的版本;但是,如果通过基类的引用或指针访问,则仍然只能访问到基类的成员,即使该对象实际上是派生类类型的实例。 

详情点击【C++】继承|切片|菱形继承|虚继承-CSDN博客中有关于 隐藏/重定义的详解。

析构函数的重写 

首先了解到,编译器会将析构函数统一处理成destructor,而C++中又有隐藏这一处理。

先看,如果没有加virtual构成隐藏

class Base {
public:~Base(){cout << "~Base" << endl;}
};
class Derive : public Base
{
public:~Derive(){cout << "~Derive" << endl;}
};int main()
{//Base b;//Derive d;Base* ptrb1 = new Base;Base* ptrb2 = new Derive;delete ptrb1;delete ptrb2;return 0;
}

 发生隐藏后,调用的时候只看自身的类型,是Base就调用Base的函数,是Derive就调用Derive的函数,不构成多态,且派生类析构没有调用,内存泄漏

加上virtual的析构函数 ,就能正常析构了

注:析构函数加virtual是在new场景下才需要, 其他环境下可以不用

解释如下:

  1. 对象d(类型为Derive)首先被销毁,调用其析构函数,输出~Derive
  2. 紧接着,因为d的析构完成后,会自动调用其基类Base的析构函数,输出~Base
  3. 最后,对象b(类型为Base)被销毁,调用其析构函数,再次输出~Base

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

四.C++11新增的 override 和 final 

C++11 引入了overridefinal 两个关键字,它们增强了面向对象编程中的继承和多态特性,提高了代码的安全性和可读性。

override

关键字用于指示一个虚函数是其基类中虚函数的重写版本。

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

在派生类中定义虚函数时,在函数声明后紧跟 override关键字。

final

用途:

当用于类声明时,它表示该类不能被其他类继承,即禁止继承。

当用于虚函数声明时,它表示该虚函数在其派生类中不能被进一步重写,即锁定重写。

语法:

对于类,将 final 放在类声明的末尾;对于虚函数,在其声明后紧跟 final 关键字。

五.抽象类 

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

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

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

 六.多态的原理

虚函数表

  1. 每个包含虚函数的类都会有一个虚函数表。
  2. 每个对象的内存布局中,第一个位置通常是一个指向其类虚函数表的指针。
  3. 当通过基类指针或引用调用虚函数时,程序会先访问对象的虚函数表指针,然后根据虚函数在表中的索引找到正确的函数指针,并调用该函数。
  4. 在多重继承的情况下,每个类都有自己的虚函数表,对象的内存布局中可能包含多个虚函数表指针
class A
{
public:int i;virtual void Print() { } // 虚函数
};int main()
{A a;cout << sizeof(a) <<endl;        //输出结果是8  当然根据不同X86/X64来决定指针大小return 0;
}

 因为该类有虚函数,所以至少有一个虚函数表指针:虚函数的地址会放在这个虚函数表里面。

 

而如果有派生类,派生类会继承基类的虚函数表和成员变量。

class A
{
public:virtual void Print() { } // 虚函数virtual void Print1() { } // 虚函数virtual void Print2() { } // 虚函数virtual void Print3() { } // 虚函数void func() { }
protected:int _a = 1;
};class B :public A
{
public:virtual void Print() {}     //重写virtual void func() {}       //派生类自己特有的虚函数
protected:int _b = 0;
};int main()
{A a;B b;cout << sizeof(a)<<","<<sizeof(b) << endl;  //8,12return 0;
}

 类A:  包含一个虚函数表指针(通常占用4或8字节,具体取决于平台),因为类A中至少有一个虚函数。成员变量_a是一个 int 通常占用4字节。

类B: B会继承类A的所有成员,包括虚函数表指针和成员变量,自身也有成员变量,所以是4/8字节的虚函数表指针,4字节的类A成员变量,4字节自己的成员变量;

详解:

        类B还重写了虚函数Print(),并且新增了一个虚函数func(),

但是重写不会增加虚函数表的大小,新增的虚函数会使得虚函数表中增加一项,这不会直接影响类的大小,因为虚函数表本身只有一份,但是每个对象需要一个指向该表的指针。

 

总结:

  • 虚函数自身自带一个_vfptr的虚函数表
  • 子类虚函数继承父类虚函数时,不仅继承父类虚函数表,也可以对虚函数进行重写。
  • 子类虚函数继承父类虚函数时,非虚函数不会存放在_vfptr
  • 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
  • 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr

引用和指针如何实现多态  

为什么多态可以实现指向父类调用父类函数 ,指向子类调用子类函数?

传递父类,通过vftptr找到虚函数表的地址,再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数

传递子类,首先会进行切割 ;

将子类的内容切割掉,父类再去接受这个数据了,一样有vftptr(是子类的vftptr),再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数。

这样就完成了多态。

且记:是运行时才知道调用的是基类还是派生类的,这就是运行时多态;

同类对象的虚表一样。

如果子类没有重写父类的虚函数,那么他们的虚函数表指针不同,但里面的内容相同

虚函数表存放位置 

我们通过代码来打印各个区地地址,可以判断虚函数表存放位置

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};void func()
{}int main()
{Base b1;Base b2;static int a = 0;int b = 0;int* p1 = new int;const char* p2 = "hello world";printf("静态区:%p\n", &a);printf("栈:%p\n", &b);printf("堆:%p\n", p1);printf("代码段:%p\n", p2);printf("虚表:%p\n", *((int*)&b1));printf("虚函数地址:%p\n", &Base::func1);printf("普通函数:%p\n", func);
}

注意打印虚表这里,vs x86环境下的虚表的地址是存放在类对象的头4个字节上。因此我们可以通过强转来取得这头四个字节

b1是类对象,取地址取出类对象的地址,强转为(int*)代表我们只取4个字节,再解引用,就可以取到第一个元素的地址,也就是虚函数表指针的地址

七.单继承和多继承关系的虚函数表 

 单继承中的虚函数表

// 基类Base,包含一个虚函数func1和func2,以及一个私有成员变量a
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; } // 虚函数func1的实现virtual void func2() { cout << "Base::func2" << endl; } // 虚函数func2的实现
private:int a; // 私有成员变量a
};// 派生类Derive,公有继承类Base
class Derive : public Base {
public:// 重写基类的虚函数func1virtual void func1() { cout << "Derive::func1" << endl; }// 添加新的虚函数func3virtual void func3() { cout << "Derive::func3" << endl; }// 添加另一个新的虚函数func4virtual void func4() { cout << "Derive::func4" << endl; }
private:int b; // 私有成员变量b
};// 从Derive类进一步派生出的类MM
class MM : public Derive {
public:// 重写从Derive继承来的虚函数func3virtual void func3() { cout << "MM::func3" << endl; }
};int main()
{Base b; // 创建Base类的对象bDerive d; // 创建Derive类的对象dMM m; // 创建MM类的对象mreturn 0;
}

调试窗口有时候不可信,这时候要去看内存;

打印虚表 

虚函数表指针,其实就是一个函数数组指针,这个数组中的每个元素都是一个函数指针,指向类的虚函数实现

// 基类Base,包含虚函数func1和func2,以及一个私有成员变量a
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; } // 虚函数func1的实现virtual void func2() { cout << "Base::func2" << endl; } // 虚函数func2的实现
private:int a; // 私有成员变量a
};// 派生类Derive,公有继承自Base
class Derive : public Base {
public:// 重写基类的虚函数func1virtual void func1() { cout << "Derive::func1" << endl; }// 添加新的虚函数func3virtual void func3() { cout << "Derive::func3" << endl; }// 添加另一个新的虚函数func4virtual void func4() { cout << "Derive::func4" << endl; }
private:int b; // 私有成员变量b
};// 从Derive类进一步派生出的类X
class MM : public Derive {
public:// 重写从Derive继承来的虚函数func3virtual void func3() { cout << "MM::func3" << endl; }
};// 函数指针类型定义,用于指向无参数无返回值的函数
typedef void(*VFTPTR)();// 打印虚函数表的函数
void PrintVFPtr(VFTPTR a[])
{for (size_t i = 0; a[i] != 0; i++) // 遍历虚函数表,直到遇到空指针为止{printf("a[%d]:%p\n", i, a[i]); // 打印当前索引的函数指针地址}cout << endl;
}int main()
{Base b; // 创建Base类的对象bDerive d; // 创建Derive类的对象dMM m; // 创建MM类的对象m// 使用指针技巧获取虚函数表地址并打印其内容//  指针和指针之间可以进行强转//  整型家族可以跟指针进行强转PrintVFPtr((VFTPTR*)*(int*)&b); // 打印Base对象b的虚函数表PrintVFPtr((VFTPTR*)*(int*)&d); // 打印Derive对象d的虚函数表PrintVFPtr((VFTPTR*)*(int*)&m); // 打印MM对象m的虚函数表return 0;
}
  1. &b获取对象b的地址,它是一个指向Base对象的指针。
  2. (int*)&b将这个地址强制转换为一个指向 int 的指针。大部分编译器在对象的内存布局中,虚函数指针位于对象的起始位置,并且其大小与int相同。
  3. *(int*)&b解引用这个int指针,实际上得到的是对象b虚函数表指针的值
  4. (VFTPTR*)... 再次进行类型转换,这次是将前面得到的值(虚函数表的地址)转换为指向函数指针的指针,即(VFTPTR*)。这一步我们可以将这个地址传递给PrintVFPtr函数,进而访问虚函数表。

我们可以将代码修改一下,得到该地址具体是什么

因为a[i]里面存放的就是函数指针,因此我们可以选择直接调用。 

// 函数声明,接受一个VFTPTR类型的指针数组,VFTPTR是一个 无返回值无参数函数指针
void PrintVFPtr(VFTPTR a[]) {       //VFTPTR a[] 是一个函数指针数组,元素类型为 VFTPTR for (size_t i = 0;a[i] != 0;    // 循环条件是当前指针不为空(通常虚函数表以NULL(00000000)结尾)i++)            // 每次循环迭代,i递增,指向下一个虚函数{// 打印当前虚函数指针在数组中的索引及其地址printf("a[%d]:%p->", i, a[i]);// 创建函数指针变量    临时保存当前索引处的虚函数指针 就是指向的虚函数地址VFTPTR p = a[i];// 调用当前索引处的虚函数(通过函数指针调用)/*调用 p() 使用函数指针时,通过在指针后面加上括号 () 就可以实现对指针所指向函数的调用。这与直接调用一个函数的方式相似,例如 func(),区别在于这里是通过一个指向该函数地址的指针来实现调用。*/p();    }

详解:VFTPTR p = a[i];

                        p();

  • p:是一个函数指针,来自数组 a 中索引为 i 的函数地址赋给了它。这个地址指向某个具体的函数实现。
  • p() :意味着“调用 p 所指向的函数”。编译器会解析 p 指针所存储的地址,然后跳转到该地址执行函数代码。

 

总结:

每个类都有一个自己的虚函数表,其中包含了该类所有虚函数的地址。在多重继承或更复杂的继承结构中,虚函数表的布局和内容可能会更加复杂,但基本原理相同。 

多继承中的虚函数表 

两个基类,都有各自的func1,func2。派生类继承了类Base1,类Base2,且重写了func1,增加了自己的func3

// 定义一个函数指针类型,用于指向无参数无返回值的函数
typedef void(*VFTPTR)();// 打印虚函数表的函数,接受一个函数指针数组作为参数
void PrintVFPtr(VFTPTR a[]) {// 遍历数组直到遇到空指针for (size_t i = 0; a[i] != 0; i++) {// 打印当前函数指针的索引和地址printf("a[%d]:%p->", i, a[i]);// 调用当前函数指针指向的函数VFTPTR p = a[i];p();}cout << endl; // 换行
}// 定义基础类Base1,包含两个虚函数
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; } // 虚函数func1virtual void func2() { cout << "Base1::func2" << endl; } // 虚函数func2
private:int b1; // 私有成员变量
};// 定义另一个基础类Base2,同样包含两个虚函数
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; } // 虚函数func1virtual void func2() { cout << "Base2::func2" << endl; } // 虚函数func2
private:int b2; // 私有成员变量
};// 派生类Derive从Base1和Base2继承,添加自己的虚函数和成员变量
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; } // 重写func1virtual void func3() { cout << "Derive::func3" << endl; } // 添加新的虚函数func3
private:int d1; // 私有成员变量
};int main() {// 创建Derive类的对象dDerive d;// 通过强制类型转换和指针偏移访问Derive对象的虚函数表PrintVFPtr((VFTPTR*)(*(int*)&d));// 打印Derive对象的大小,用于观察多继承对对象大小的影响// cout << sizeof(d) << endl;return 0; // 程序正常结束
}

 可以看到有两个虚表,但是自身特有的虚函数放在哪张表呢?

打印虚表 

当一个类通过多重继承从多个基类继承时,派生类可能会有多个虚函数表(vtable)和虚基类表。

每个非虚基类的虚函数表

  • 如果派生类从每个非虚基类继承了虚函数,那么派生类将为每个基类拥有一个独立的虚函数表。

虚基类的虚基类表

  • 如果派生类通过继承引入了虚基类,那么将为每个虚基类有一个虚基类表,用于管理虚基类的布局信息。

派生类自己的虚函数表

  • 如果派生类本身声明了新的虚函数或重写了继承的虚函数,它将拥有自己的虚函数表。

 

打印出来的结果只有第一张的虚表,且自身的func3虚函数在该表中。可以推论出该对象模型; 

打印第二章虚表

 由上可见,

方法1:

&d强转为Base1*,这样+1就会跳过整个Base1,就刚好到达了Base2类的开始,再进行之前的强转便可以打印了。

方法2:

直接将&d赋值给Base2* ptr;这样Base2会进行切片操作,于是ptr就直接指向了Base2的虚函数表,依然就行之前的强转操作便可以打印了。

int main()
{Derive d; // 创建一个Derive类的对象d// 直接通过Derive对象d访问虚函数表PrintVFPtr((VFTPTR*)(*(int*)&d));// 这里首先取d的地址(&d),然后强制转换为int*,接着解引用(*)得到虚函数表指针的地址,// 最后再次强制转换为VFTPTR*来打印Derive类的虚函数表。// 方法1: 通过将d转换为Base1指针并偏移1来尝试访问虚函数表PrintVFPtr((VFTPTR*)(*(int*)((Base1*)&d +1)));// 这里首先将d的地址强制转换为Base1*,为了访问Base1的虚函数表,// (Base1*)&d +1操作是访问紧随Base1之后的内存区域,// 方法2: 通过Base2指针访问Derive对象的虚函数表Base2* ptr = &d; // 将Derive对象d的地址赋给Base2指针ptr  切片PrintVFPtr((VFTPTR*)(*(int*)ptr));// 首先将ptr转换为int*,然后解引用得到Base2的虚函数表指针地址,// 再次转换为VFTPTR*以打印虚函数表。但是,会打印Base2的虚函数表,// 而非Derive的,因为ptr是指向Derive对象中Base2子对象的首地址。return 0; 
}

总结:Derive类对象的虚函数会放在多继承中继承的第一个类的虚函数表里(即Base1类虚函数表)  

多继承多个虚表 

如果没有多个虚表,那么p1去调用func1(),p2也去调用func1(),

如若d没有重写func1(),那么这个多态就会紊乱,调用的都是那一个func1()了,而不是p1调用Base1的func1(),p2调用Base2的func1()了。因此多继承就需要多个虚表才不会出现紊乱的问题

 

 总结:

在多继承的情况下,如果基类中有同名的虚函数(如func1()),每个基类都会维护自己的虚函数表,即使这些函数在名字和签名上完全相同。

        即使Base1和Base2中都有名为func1()的虚函数,它们在各自类的虚函数表中会有不同的地址,因为它们实际上是两个不同的函数实现。 当一个派生类(如Derive)继承了这些基类,它会继承这些虚函数表。

        如果派生类重写了这些同名的虚函数,则派生类的虚函数表中会包含这个重写后的函数地址。如果没有重写,派生类对象在调用这些函数时,会根据调用上下文(即通过哪个基类的指针或引用调用)来决定使用哪一个基类的函数实现。 

        在多继承体系中,每个基类的虚函数表都是独立的,即使它们包含同名函数,这些函数也是基类定义范围内的独立实现。这保证了多态性,即派生类对象可以正确响应通过基类指针或引用调用的虚函数,无论这些函数在多少个基类中被重载。

八.菱形继承和菱形虚拟继承 

两种不同的继承结构,它们都涉及到基类和派生类之间的关系

菱形继承

菱形继承是一种继承结构,其中两个或多个派生类继承自同一个基类,然后一个更深层次的派生类继承自这两个派生类。这种结构在类图上形成了一个菱形的样子;

class Base {
public:int value;
};class Derive1 : public Base {
};class Derive2 : public Base {
};class MostDerived : public Derive1, public Derive2 {// MostDerived 有两个 Base 的拷贝,一个来自 Derive1,一个来自 Derive2
};

 

菱形虚拟继承

 是解决菱形继承问题的一种方法。通过将基类的继承方式改为虚继承(在继承时使用 virtual 关键字),可以确保所有派生类共享同一个基类子对象的拷贝。

优点

  • 避免了内存浪费,因为所有派生类共享同一个基类实例。
  • 保持了数据一致性,因为所有对基类成员的修改都作用于同一个实例。
class Base {
public:int value;
};class Derive1 : virtual public Base {
};class Derive2 : virtual public Base {
};class MostDerived : public Derive1, public Derive2 {// MostDerived 只有一个 Base 的拷贝,所有 Derived 共享
};

区别和联系: 

  • 菱形继承不使用 virtual 关键字,导致基类在派生类中被多次实例化。
  • 菱形虚拟继承使用 virtual 关键字来避免重复实例化基类,确保所有派生类共享同一个基类实例。
  • 菱形虚拟继承解决了菱形继承中的数据一致性和内存浪费问题。

九.例题

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

 

 

是不是结果很匪夷所思

B类型的对象p去调用test();

test()是B类继承的,但是里面默认存放的this指针依然是A*,B类里面的func()是重写了A类的func()  (A类func()为虚函数,B类重写了可以不写virtual)。

重写的关键点:

        仅仅是重写了基类的实现,函数的声明,依然是A类的声明,因此给到的val默认值是1,调用了B类的函数实现!!! 所以输出B->1

 

 

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

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

相关文章

非常好用的7个Vue3组件库!!【送源码】

说到Vue&#xff0c;怎能不提Vue3呢&#xff1f; 它的大名鼎鼎主要归功于一项革命性的创新——Composition API。 这个新功能为逻辑复用带来了前所未有的友好性和灵活性&#xff0c;让开发者们在构建应用时如鱼得水。 如果你现在正在使用Vue3&#xff0c;或者在新的一年考虑…

浏览器上直接运行近 1000个 AI 模型!

今天推荐的开源项目叫做 tansformers.js&#xff0c;这是一个不需要服务器端&#xff0c;能让你在浏览器上使用到自然语言处理、计算机视觉等 AI 能力的开源项目。由 xenova 开源&#xff0c;transformers.js 已经在 GitHub 上获得了超过 9.2K 颗星星。 项目简介 transformers.…

JVM如何确定方法调用

方法调用并不等同于方法执行&#xff0c;方法调用阶段唯一的任务就是确定调用哪一个方法&#xff0c;不涉及方法内部的具体运行过程。在程序运行时&#xff0c;进行方法调用是最普遍、最频繁的操作&#xff0c;但Class文件的编译过程中不包含传统编译中的连接步骤&#xff0c;一…

医学人工智能项目如何申请基金?

小罗碎碎念 本期推文面向的群体 青年教师有志硕博/博后 尤其适合一直认真追小罗推文的老师/同学&#xff0c;你们会发现自己在看这篇推文的时候&#xff0c;遇到自己领域的项目时&#xff0c;文思如泉涌&#xff0c;仿佛马上就能把本子写好&#xff0c;哈哈。&#xff08;运用…

命令词:引导行动的语言工具

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

App UI 风格打造独特体验

App UI 风格打造独特体验

通过Stream流对集合进行操作

Stream Api是JDK8提供的新特性&#xff0c;可以更为方便地对集合进行操作&#xff0c;比如我今天遇到的一个场景&#xff1a; 将本地的一个视频文件分成多块上传到Minio服务器&#xff0c;现在上传功能已经完成&#xff0c;需要调用minioClient对已经上传的文件重新合并成一个新…

8086汇编 add指令学习

ADD&#xff0c;是Intel x86平台的汇编加法指令&#xff0c;MEM代指操作数为内存或寄存器&#xff0c;REG代指操作数为寄存器&#xff0c;IMM代指立即数&#xff0c;SEG代指操作数为段寄存器。 形式和示例如下&#xff1b; ADD MEM8,REG8 ADD DS:[BXSI],AL ADD MEM16,R…

【ARM Coresight Debug 系列 -- ARMv8/v9 Watchpoint 软件实现地址监控详细介绍】

请阅读【嵌入式开发学习必备专栏 】 文章目录 ARMv8/v9 Watchpoint exceptionsWatchpoint 配置信息读取Execution conditionsWatchpoint data address comparisonsSize of the data accessWatchpoint 软件配置流程Watchpoint Type 使用介绍WT, Bit [20]: Watchpoint TypeLBN, B…

vue技巧(十)全局配置使用(打包后可修改配置文件)

1、背景 vue打包目前主流用的有webpack和vite两种&#xff0c;默认用的webpack。&#xff08;二者的区别大家可以各自上网查&#xff0c;我没用过vite&#xff0c;所以不过多介绍&#xff09;vue通过webpack打包后&#xff0c;源码会被压缩&#xff0c;但一些关键配置可…

【新课程】PICO VR 交互开发指南

从PICO开始&#xff0c;迈向XR跨平台开发 Unity XR Interaction Toolkit &#xff08;简称XRI&#xff09;是一套跨平台的 XR 交互开发工具包&#xff0c;随着版本的更新与完善&#xff0c;逐渐获得了开发者的青睐。各 XR 平台逐步推荐开发者采用 XRI 作为首选的交互开发工具为…

Pytest框架中fixture功能详解

文章目录 1 定义 Fixture函数 2 Fixture 的函数参数 2.1 传入其他fixture函数作为参数 2.2 传入request对象参数 示例1&#xff1a;访问fixture的调用者 示例2&#xff1a;使用fixture的参数 3 Fixture 的作用域参数scope 3.1 scopeclass场景 3.2 scopesession场景 4…

SwiftUI 6.0(iOS 18)新容器视图修改器漫谈

概览 本届 WWDC 2024 观影正如火如荼的进行中&#xff0c;一片鸟语花香、枝繁叶茂的苹果树上不时结出几颗令人垂涎欲滴的美味苹果让秃头码农们欲罢不能。 如您所愿&#xff0c;在界面布局“利器” SwiftUI 这根蔓藤也长出不少喜人的果实&#xff0c;其中在 iOS 18.0 中新添加的…

rabbitMQ的简单使用

rabbitMQ的介绍 RabbitMQ是一个开源的消息代理和队列服务器&#xff0c;主要用于在不同的应用程序之间传递消息。它基于AMQP&#xff08;Advanced Message Queuing Protocol&#xff09;协议&#xff0c;提供了一种可靠的方式来处理异步通信。RabbitMQ使用Erlang语言编写&…

springboot 整合redis问题,缓存击穿,穿透,雪崩,分布式锁

boot整合redis 压力测试出现失败 解决方案 排除lettuce 使用jedis <!-- 引入redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclus…

内存泄漏 内存溢出

概念 内存泄漏&#xff1a;是程序没有正确的释放已分配的内存&#xff0c;造成系统内存的浪费。内存泄漏很难发现&#xff0c;因为他不会直接导致程序崩溃&#xff0c;而是会慢慢降低程序的性能。 内存溢出&#xff1a;系统中存在无法回收的内存或使用的内存过多&#xff0c;…

【linux-imx6ull-定时器与中断】

目录 1. 前言2. Linux软件定时器2.1 内核频率选择2.2 重要的API函数2.3 Linux软件定时器的使用配置流程 4. Linux中断4.1 简单中断使用4.1.1 简要说明4.1.2 重要的API函数4.1.3 中断的简要配置流程 4.2. 中断的上半部和下半部4.2.1 tasklet实现下半部4.2.2 work实现下半部 1. 前…

MySQL数据操作与查询- 聚合函数和分组查询

一、聚合函数 聚合函数主要用来进行数据 汇总 。 1、sum 返回选取的某列的总和。 语法&#xff1a; select sum(字段名) from 表名 where 条件表达式 2、max 返回选取的某列的最大值。 语法&#xff1a; select max(字段名) from 表名 where 条件表达式 3、min 返…

【网络安全的神秘世界】AppScan安装及使用指南

&#x1f31d;博客主页&#xff1a;泥菩萨 &#x1f496;专栏&#xff1a;Linux探索之旅 | 网络安全的神秘世界 | 专接本 https://www.hcl-software.com/appscan AppScan是一种综合型漏洞扫描工具&#xff0c;采用SaaS解决方案&#xff0c;它将所以测试功能整合到一个服务中&a…

样式的双向绑定的2种方式,实现样式交互效果

与样式标签实现双向绑定 通过布尔值来决定样式是出现还是消失 show代表着布尔值&#xff0c;show的初始值是false所以文本不会有高亮的效果&#xff0c;当用户点击了按钮&#xff0c;就会调用shows这个函数&#xff0c;并将show的相反值true赋值并覆盖给show,此时show的值为tru…