C++ 多态

devtools/2025/2/27 8:25:53/

文章目录

  • (一)多态的概念
  • (二)多态的定义及实现
    • 1.多态的构成条件
    • 2.虚函数的重写/覆盖
    • 3.多态场景的一道选择题
    • 4.协变(了解)
    • 5.析构函数的重写
    • 6.override和final关键字
    • 7.重载/重写/隐藏的对比
  • (三)纯虚函数和抽象类
  • (四)多态的原理
    • 1.虚函数表指针
    • 2.多态是如何实现的
    • 3.动态绑定和静态绑定
    • 4.虚函数表

(一)多态的概念

概念:不同的对象在完成相同函数时产生不同的形态,也就是多种形态。多态又分为静态多态动态多态

  • 静态多态:也叫作编译时多态,主要就是函数重载和函数模板,它们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,就叫作静态多态
    例如:
int a;
double b;
cout << a;
cout << b;

这里通过传不同的参数,调用函数重载,达到不同的形态

  • 动态多态:也叫作运行时多态,就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,即可达到多种形态。例如:买票这个行为,普通人买票就是全价购买;学生买票就是半价或打折购买;军人买票可以优先购买。

该文章主要学习动态的多态

(二)多态的定义及实现

1.多态的构成条件

  1. 多态是一个继承关系下的类对象,去调用同一函数,产生不同的行为。如:Student继承了Person,Person买票全价,Student买票打折
  2. 必须是基类的指针或引用调用虚函数
  3. 被调用的函数必须是虚函数

说明:必须是基类的指针或引用,是因为只有基类的指针或引用才可既指向基类对象也可指向派生类对象;派生类必须对基类的虚函数进行重写/覆盖,这样派生类才能有不同的函数,才可达到多态效果

举例代码如下:

#include <iostream>
using namespace std;class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; } //virtual关键字就代表函数是虚函数
};
class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
void func(Person* ptr) //基类的指针
{ptr->BuyTicket(); //基类的指针调用虚函数
}
int main()
{Person ps;Student sd;func(&ps);func(&sd);return 0;
}

输出结果:
在这里插入图片描述
通过代码和结果我们可以看到虽然Person指针ptr都在调用BuyTicket函数,但是产生了不同的结果,所以说明在调用BuyTicket函数时,跟ptr是没有关系的,而是与ptr指向的对象有关。当ptr指向的是父类对象时,调用的是父类的BuyTicket函数,指向的是子类对象时,调用的是子类的BuyTicket函数

注:成员函数前面加上virtual关键字就是虚函数,非成员函数不能加virtual修饰

2.虚函数的重写/覆盖

概念:在派生类中有一个虚函数,该虚函数的返回类型与基类的相同、与基类的函数名相同、与基类的参数列表相同,只有函数体不同,则称该派生类的虚函数重写/覆盖了基类的虚函数

通过下面的代码来进行理解:

class Animal
{
public:virtual void Talk() { };
};
class Cat : public Animal
{
public :virtual void Talk() { cout << "喵喵~" << endl; }
};

Cat类中的虚函数对Animal类中的虚函数进行了重写/覆盖,重写可以理解为对基类的虚函数的函数体进行了重写;覆盖可以理解为将基类的虚函数的返回值、函数名、参数列表,这三样覆盖到了派生类中

注意:在重写基类虚函数时,派生类的虚函数可以不加virtual关键字,因为基类与派生类是继承的关系,继承后,基类的虚函数被继承了下来,派生类只是对被继承下来的虚函数进行函数体的重写,所以被继承下来的虚函数依旧保持着虚函数的属性,但前提是基类的虚函数必须加上virtual关键字,但不建议这样使用。

如下基类不加virtual关键字的代码以及结果:

class Animal
{
public:void Talk() { }; //基类不加virtual关键字
};
class Cat : public Animal
{
public :virtual void Talk() { cout << "喵喵~" << endl; }
};
class Dog : public Animal
{virtual void Talk() { cout << "汪汪~" << endl; }
};void func(Animal& ptr)
{ptr.Talk();
}
int main()
{Cat cat;Dog dog;func(cat);func(dog);
}

在这里插入图片描述
运行是没有问题的,结果是空,就是因为基类的虚函数没有加上virtual关键字,所以该函数就不是虚函数,只有虚函数才能完成重写,所以当不同对象在调用Talk函数时,调用的都是基类的Talk函数,因为基类的函数没有被重写,代码中基类的函数的函数体是空,则输出的结果就为空。语法规定只有虚函数才能被重写,该基类的函数没能被重写,就无法达到多态的效果。为什么只有虚函数才能被重写,结合下面的多态的底层才能知晓。

3.多态场景的一道选择题

在这里插入图片描述
结果:
在这里插入图片描述
解析:
当P调用test()后,函数体里又调用了func()函数,即this指针调用了func()函数,this->func(),那么第一个问题来了this调用的func()函数是否构成多态?简单来说就是this是基类指针还是派生类指针呢?第二个问题调用的函数是否是虚函数?
在这里插入图片描述
第一个问题,this代表的是A*,因为A与B是继承的关系,所以这里调用的test()函数是复用了A类的test()函数,P指向的是B,所以当P在B类里找test()函数找不到时,就会去A类里面找,所以this代表的是基类的指针。这里大家可能会有一个错误的认知,B继承了A就说明,所以在B类里也拷贝了一份A类的成员函数/变量在B类中生成,这是错误的,继承的本质是在派生类中找不到就去基类找。

第二个问题,调用的函数是虚函数,若B类中的func()函数没有加上virtual关键字也是可以的,因为在继承下来的过程中保留了基类虚函数的属性。代码中对虚函数也进行了重写。

以上这两个问题就证明了调用的func()函数构成了多态。P指向的是子类,所以调用的是子类的func()函数,但还有一个要注意的是这里的缺省值是1还是0?在文章的前面我提到过,重写是对虚函数的函数体进行重写,覆盖可以理解为将基类的虚函数的返回值、函数名、参数列表,这三样覆盖到了派生类中,所以这里的缺省值是1。

4.协变(了解)

概念:派生类在重写/覆盖基类虚函数时,返回值的类型可以与基类不同。前提是,基类的虚函数的返回值是基类的对象的指针或者引用,那么,派生类的虚函数的返回值可以是派生类的对象的指针或者引用,称为协变。(返回的类型也要构成继承的关系)

代码如下:

class A{};
class B : public A{};
class Person
{
public:virtual A* BuyTicket() { cout << "全价购买" << endl; return nullptr; }//将A换成Person也行
};
class Student : public Person
{
public:virtual B* BuyTicket() { cout << "半价购买" << endl; return nullptr; }//将B换成Student也行
};
void func(Person& ptr)
{ptr.BuyTicket();
}
int main()
{Person a;Student b;func(a);func(b);return 0;
}

5.析构函数的重写

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

代码如下:

class A
{
public:virtual ~A()//编译器会将其变成~destroctor(){cout << "~A()" << endl;}
};
class B : public A
{
public:~B() //编译器会将其变成~destroctor(){cout << "~B()"<<":"<< _p << endl;delete _p;}
protected:int* _p = new int[10];
};
int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

结果:
在这里插入图片描述
从代码及结果来看,先释放A的资源,再释放B的资源,又因为在继承关系中,释放的顺序是先子后父,所以又释放了一次A的资源。所以我们可以看到,构成多态的析构函数,函数名不同也没有关系,编译器会对其进行统一处理,只需在基类的析构函数中加上virtual关键字即可。

若将代码改一改,将基类的析构函数的virtual关键字去掉,会出现怎样的结果呢?

在这里插入图片描述
从结果可以看到,B的资源没有得到释放,导致内存泄漏。P2指向的是B对象,当deleteP2时,本应该调用B的析构函数,但是没有调到B的析构函数,导致B对象所指向的空间没有被释放掉。核心的原因即没有调用对应的合适的析构函数,当指向父类时,调用的父类的析构函数;指向子类时,调用的还是父类的析构函,这是错误的。本质的原因就是调用的析构函数没有构成多态,没有构成多态就会根据指针类型来调用,父类的指针就调父类,子类的指针就调用子类。构成多态就必须要达成两个条件,必须是基类的指针或引用来调用虚函数;调用的函数必须是虚函数,且虚函数构成重写。我们来看deleteP2这个指令,编译器会先执行P2->destructor()这个函数然后再执行operator delete(P2)这个函数,P2是基类指针,满足多态的第一个条件,且P2调用的destructor()函数也构成了重写,但是该函数不是虚函数,不满足第二个条件,以上就是导致内存泄漏的原因。所以基类的析构函数必须要用virtual修饰

总结:构成多态,才能指向谁调用谁的析构函数,才能正确释放资源。

6.override和final关键字

用法:C++对函数重写的要求比较严格,如函数名写错或者参数写错等就会导致无法构成重载,而这种错误在编译期间是不会报出来的,只有在运行时没有得到预期结果才来debug就得不偿失了,因此C++11提供了override,可以帮助用户检测是否构成重写。如果我们不想让派生类重写这个虚函数,那么我们可以用final修饰。

使用如下:

class Car
{
public:virtual void Dirve() { }
};
class Benz : public Car
{
public:virtual void Deirve() override { cout << "Benz-舒适" << endl; }//用override关键字来检测该语句是否构成重写
};
int main()
{return 0;
}

结果如下:
在这里插入图片描述
override检测到重写的函数有错误,就是因为上述代码重写的函数名有错。

final关键字可以修饰一个类使这个类不能被继承,也可以用来修饰虚函数,使虚函数不能被派生类重写。

代码如下:

class Car
{
public:virtual void Dirve() final { } //被final关键字修饰
};
class Benz : public Car
{
public:virtual void Dirve() { cout << "Benz-舒适" << endl; }
};
int main()
{return 0;
}

结果如下:
在这里插入图片描述
以上就是override和final的用法

7.重载/重写/隐藏的对比

  • 重载:两个函数在同一作用域;函数名相同,参数的类型或数目不同,返回值可同可不同。
  • 重写/覆盖:两个函数分别在继承体系的父类和子类不同作用域;函数名相同、返回值相同、参数列表相同,协变例外;两个函数都是虚函数
  • 隐藏:两个函数分别在继承体系的父类和子类不同作用域;函数名相同;两个函数只要不构成重写,就是隐藏;父子类的成员变量相同也叫隐藏。

(三)纯虚函数和抽象类

概念:在虚函数的后面写上=0,则这个函数就是纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只需要声明即可。包含纯虚函数的类叫作抽象类,抽象类不能实例化出对象,如果派生类继承了抽象类,但不重写纯虚函数的话,那么该派生类也是抽象类。

为了方便理解,代码如下:

class Car
{
public:virtual void Dirve() = 0; // 纯虚函数
};
class Benz : public Car
{
};
int main()
{Car c;Benz b;return 0;
}

结果如下:
在这里插入图片描述
Car类中有纯虚函数是抽象类,所以不能实例化出对象,Benz类继承了Car类,且没有对继承下来的纯虚函数进行重写,所以Benz类也是抽象类,也不能实例化出对象。所以纯虚函数从某种程度上来说它强制了派生类重写纯虚函数,因为不重写派生类就实例化不出对象。

正确使用方式如下代码:

class Car
{
public:virtual void Dirve() = 0;
};
class Benz : public Car
{
public:virtual void Dirve() { cout << "Benz-舒适" << endl; }// 对纯虚函数进行重写
};
class BMW : public Car
{
public:virtual void Dirve() { cout << "BMW-操控" << endl; }// 对纯虚函数进行重写
};
int main()
{//Car c;Car* PBenz = new Benz;Car* PBMW = new BMW;PBenz->Dirve();PBMW->Dirve();return 0;
}

结果:
在这里插入图片描述
可以看到Car虽然不能实例化出对象,但它可以定义指针,代码中定义了两个抽象类指针分别指向不同的派生类,这样就可以实现多态。

简单来说就是,纯虚函数间接强制了派生类重写虚函数,还有就是,如果不想某个类实例化出对象,它是一种抽象的表达,在现实当中没有对应的实体,或者说定义出来也没有多大意义,那么我们就可将这个类定义成包含纯虚函数的抽象类。

(四)多态的原理

1.虚函数表指针

通过下面这道题,来学习虚函数表指针,代码如下:

下面这段代码的输出结果是多少?(在32位程序下运行)

#include <iostream>
using namespace std;class Base
{
public:virtual void func1(){cout << "func()" << endl;}virtual void func2(){cout << "func2()" << endl;}void func3(){cout << "func3()" << endl;}
protected:int _b = 1;char _ch = 'x';
};
int main()
{Base b;cout << sizeof(b) << endl;return 0;
}

按照内存对齐的规则,大家可能会认为是8,结果如下:
在这里插入图片描述
从结果来看,这个Base类占了12个字节,与我们前面猜测的多四个字节,多出来的这四个字节就是用来存放虚函数表指针的,如何证明那四个字节存放的就是虚函数表指针呢?看下面的监测窗口
在这里插入图片描述
这就解答了我们的一个疑惑,为什么只有虚函数重写才能完成多态,就跟这个虚函数表指针有关了。从上面的代码来看,你有多个虚函数是没有影响的,还是只有一个虚函数表指针,它就是用来存放虚函数的地址。

所以,当有了虚函数之后,一个类的对象的存储就会发生变化,除了成员变量,至少还会多一个虚函数表指针。这个虚函数表指针会指向一个指针数组,数组中就存放那些虚函数的地址,因此这个指针数组也被称为虚函数表,一个类中只要有一个虚函数,至少就会有一个虚函数指针

总结,多态的原理的第一个支撑就是对象里面会有一个指针,这个指针指向一张表,这张表中存放的是虚函数的地址,所以形成多态的条件之一就是虚函数。

2.多态是如何实现的

多态是如何做到指向父类调父类,指向子类调子类的呢?

来看下面的代码:

class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}
protected:string _name;int _age;
};
class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票-打折" << endl;}
protected:int _id;
};
int main()
{Person ps;Student st;return 0;
}

通过监视窗口,我们来看这两个类的虚表有什么特点呢?
在这里插入图片描述
从监视窗口可以看到,person对象ps里面有两个成员变量,还有一个虚函数表指针,该指针指向的虚表里面有一个虚函数,再看派生类对象st,它有两部分构成,一个是自己的成员变量,还有一部分是父类的成员,子类当中的父类也有一个虚表指针,该虚表指针与父类中的虚表指针不同,这两个虚表指针最大的不同点就是,它们分别指向的虚表中存放的虚函数不同,父类当中的虚表存放的是父类的虚函数的地址,而子类当中的虚表存放的是子类重写的虚函数的地址

那么,**如果没有被派生类重写的虚函数会不会在派生类的虚表里面呢?**看下面的代码及监视窗口:

class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}virtual void func1(){cout << "func1()" << endl;}
protected:string _name;int _age;
};
class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票-打折" << endl;}
protected:int _id;
};
int main()
{Person ps;Student st;return 0;
}

在这里插入图片描述
代码中,派生类没有对func1这个虚函数进行重写,但是它出现在了派生类的虚表中,且与父类的func1的地址相同。再看到Buyticket这个虚函数,按理来说派生类的Buyticket函数应该也与父类的Buyticket函数的地址一致才对,但是派生类对Buyticket这个虚函数进行重写了,所以派生类的虚表中存放的Buyticket函数就不是父类虚表中的Buyticket这个虚函数了,存放的而是重写过后的虚函数。因为func1这个虚函数没有被派生类重写,没有成功覆盖掉原来从父类拷贝过来的虚函数,所以该派生类的func1函数还是与父类的保持一致。

总结,第一如果一个函数是虚函数,这个虚函数就会被放到虚表中;第二如果派生类完成了虚函数的重写,这个被重写的虚函数就会覆盖从父类的虚表中拷贝过来的对应的虚函数,所以说为什么虚函数的重写也叫作覆盖,覆盖是指虚表的这层概念

以上的总结个多态的实现有怎样的关系呢?怎样做到指向谁调用谁的,又为什么说是动态的多态?来看下面的代码及图片:

class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}virtual void func1(){cout << "func1()" << endl;}
protected:string _name;int _age;
};
class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票-打折" << endl;}
protected:int _id;
};
void func(Person* ptr)
{ptr->BuyTicket();
}
int main()
{Person ps;Student st;func(&ps);func(&st);return 0;
}

在这里插入图片描述
在代码和图片中,当ptr指向的是父类对象时,ptr就会在运行时,会到指向的对象的虚表中去找到对应的虚函数,所以当指向的是父类对象,就会调用父类的虚函数。
在这里插入图片描述
当指向子类对象,这时就会发生一个“切割/切片”的现象,所以,父类的指针永远看到的都是父类的对象,只是说可能直接指向的就是父类对象,也有可能是,子类当中切割出来的父类对象。但不同的是如果传的是父类对象,就会到父类的虚表中找到这个虚函数,如果传的是子类对象,就会找到这个虚表指针,到虚表指针指向的虚表中找到子类对应的虚函数,所以就能做到指向谁调用谁,形成多态。

总结:

  • 指向父类,运行时就会到指向父类对象的虚表中找到对应的虚函数进行调用
  • 指向子类,运行时就会到指向子类对象切片出的父类对象的虚表中找到对应的虚函数进行调用
  • 达到的效果就是指向谁调用谁的虚函数

3.动态绑定和静态绑定

动态绑定:满足多态条件的函数在调用的时候是在运行时绑定的,也就是在运行时到指定的对象的虚表中找到对应的函数的地址

来看下面一段汇编指令:
在这里插入图片描述
当调用的BuyTicket满足多态条件时,编译器会到虚表中找到对应的虚函数的地址,再进行call指令(call是调用函数)来调用函数地址,这就是动态绑定。

静态绑定:对不满足多态条件(父类指针或引用+调用虚函数)的函数在调用的时候是编译时绑定,也就是编译时确定调用函数的地址

来看调用的是不满足多态条件的函数的汇编指令:
在这里插入图片描述
调用的BuyTicket不满足多态条件,当调用它时,编译器就直接确定了调用的函数的地址,然后就直接调用该函数,这就是静态绑定。

4.虚函数表

  • 基类对象的虚函数表中存放基类所有虚函数的地址

通过下面的代码及监视窗口来进行观察:

class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}virtual void func1(){cout << "func1()" << endl;}void func2(){cout << "func2()" << endl;}
};
class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票-打折" << endl;}
};
int main()
{Person ps;Student st;return 0;
}

在这里插入图片描述
通过代码及监视窗口可以看到,基类对象的虚表中,没有存放func2这个成员函数,因为它不是虚函数。

  • 对于派生类而言,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针,但要注意的是,这里继承下来的基类部分的虚函数表指针与基类对象的虚函数表指针不是同一个

通过下面的代码及监视窗口来进行观察:

class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}virtual void func1(){cout << "func1()" << endl;}void func2(){cout << "func2()" << endl;}
protected:int _a1 = 1;int _a2 = 2;
};
class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票-打折" << endl;}
protected:int _a3 = 3;
};
int main()
{Person ps;Student st;return 0;
}

在这里插入图片描述
我们知道派生类是由两部分构成的,一部分是继承下来的基类和自己的成员。从监视窗口中我们可以看到,派生类对象st中有继承下来的基类部分,也有自己的成员_a3,所以继承了基类中的虚函数表指针后,自己就不会再生成一个虚函数表指针了,但我们又从监视窗口中看到,继承下来的基类部分的虚函数表指针与基类对象的虚函数表指针不是同一个指针,说明基类对象的虚函数表指针和派生类对象的虚函数表指针是独立存在的,就像基类对象的成员和派生类对象中的基类对象成员也是独立存在的。

  • 派生类中重写的基类的虚函数会将从基类中继承下来的派生类的虚函数表中对应的虚函数覆盖掉,变成派生类重写的虚函数的地址

通过下面的代码及监视窗口来进行观察:

class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}virtual void func1(){cout << "func1()" << endl;}
};
class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票-打折" << endl;}
};
int main()
{Person ps;Student st;return 0;
}

在这里插入图片描述
从监视窗口中可以看到,派生类重写了BuyTicket函数,所以派生类中的BuyTicket函数就会覆盖掉从基类中继承下来的BuyTicket函数,变成派生类重写之后的虚函数的地址,而派生类没有对func1进行重写,所以派生类中的func1函数与基类中的func1函数一致。可以“形象”的理解为,派生类的虚表是从基类中的虚表中拷贝了一份下来的,如果派生类对拷贝下来的这一份虚表中的虚函数进行了重写,就会将原来拷贝过来的覆盖掉,这就可以达到多态的效果。

还要注意的是,同一类型的对象,共用同一张虚函数表

  • 派生类的虚函数中包含,基类的虚函数地址,派生类重写的虚函数的地址,派生类自己的虚函数地址
  • 虚函数和普通函数一样,编译好后是一段指令,都是存储在代码段(常量区)中,只是虚函数的地址又存到了虚函数表中

http://www.ppmy.cn/devtools/163006.html

相关文章

线程同步辅助类的使用

一、countDownLatch(减法) public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException {CountDownLatch countDownLatch new CountDownLatch(6);for (int i 0; i < 6; i) {new Thread(()->{System.out.println(Thread.cu…

Docker教程(喂饭级!)

如果你有跨平台开发的需求&#xff0c;或者对每次在新机器上部署项目感到头疼&#xff0c;那么 Docker 是你的理想选择&#xff01;Docker 通过容器化技术将应用程序与其运行环境隔离&#xff0c;实现快速部署和跨平台支持&#xff0c;极大地简化了开发和部署流程。本文详细介绍…

月之暗面改进并开源了 Muon 优化算法,对行业有哪些影响?

互联网各领域资料分享专区(不定期更新): Sheet 正文 月之暗面团队改进并开源的 Muon 优化算法 在深度学习和大模型训练领域引发了广泛关注,其核心创新在于显著降低算力需求(相比 AdamW 减少 48% 的 FLOPs)并提升训练效率,同时通过开源推动技术生态的共建。 1. 显著降低大…

TCP/IP 5层协议簇:物理层

目录 1. 物理层&#xff08;physical layer&#xff09; 2. 网线/双绞线 1. 物理层&#xff08;physical layer&#xff09; 工作设备&#xff1a;网线、光纤、空气 传输的东西是比特bit 基本单位如下&#xff1a;数字信号 信号&#xff1a;【模拟信号&#xff08;放大器&a…

Linux: 已占用接口

Linux: 已占用接口 1. netstat&#xff08;适用于旧系统&#xff09;1.1 书中对该命令的介绍 2. ss&#xff08;适用于新系统&#xff0c;替代 netstat&#xff09;3. lsof&#xff08;查看详细进程信息&#xff09;4. fuser&#xff08;快速查找占用端口的进程&#xff09;5. …

Android 10.0 Settings中系统菜单去掉备份二级菜单

1.前言 在10.0的系统rom定制化开发中,在系统Settings开发过程中,会发现在settings中的系统菜单中需要去掉 备份这个菜单,接下来就需要分析下系统菜单中的备份菜单的相关功能,然后实现去掉备份菜单的功能 2.Settings中系统菜单去掉备份二级菜单的核心类 packages/apps/Se…

HBase常用的Filter过滤器操作

HBase常用的Filter过滤器操作_hbase filter-CSDN博客 HBase过滤器种类很多&#xff0c;我们选择8种常用的过滤器进行介绍。为了获得更好的示例效果&#xff0c;先利用HBase Shell新建students表格&#xff0c;并往表格中进行写入多行数据。 一、数据准备工作 &#xff08;1&am…

Claude-3.7-Sonnet 的混合推理:解锁 AI 的双重潜力

引言 随着人工智能技术的快速发展&#xff0c;大型语言模型&#xff08;LLM&#xff09;的能力不断提升。2025 年 2 月&#xff0c;Anthropic 推出了 claude-3.7-sonnet&#xff0c;这款模型首次引入了“混合推理”能力&#xff0c;成为其最显著的创新点之一。对于普通用户和开…