C++复习(六)——类和对象
面向对象编程的基本思想
面型对象就是根据需要,将后续用到的各种变量、函数按类别打包到一起,这些不同的类别就是类,代入数据之后(实例化)得到的就是对象。
一、类与对象的定义
1.类
类是对数据(属性)与函数(行为)的封装,定义格式:
class 类名
{
private:
成员数据;
成员函数;
protect:
成员数据;
成员函数;
public:
成员数据;
成员函数;
}**;**
注:对三个关键字(又叫限定词)的解释
private
:私有的。只能在类的内部使用,无法被派生类继承,无法在类外被调用。protect
:保护的。只能在类的内部和类的子类之内被调用,不能在类外被调用。public
:公有的。整个文件都有访问权限,是公开的。
限定词一旦使用,一直有效,直到下一个限定词出现。默认限定词为private
。
成员函数可以只在类内做函数原型声明,在类外定义使用作用域运算符::
定义函数体
函数类型 类名 ::函数名(参数表)
{
函数体
}
在类定义时并未分配空间,不能直接对成员数据初始化。
2. 成员数据
1.访问私有数据
可以在public
中定义函数,实现对私有数据的访问。可以使用一般变量,指针,引用作为参数。
2.对象
对象即类的实例,定义了对象之后会为对象分配空间,存储成员数据,但成员函数的代码是共享的。
定义对象的方式与一般变量相同:类名 对象名;
通过.
运算符调用对象的成员数据与函数。
同类型的变量可以整体赋值
3.类指针
用法与一般指针相同,用来存储类的对象的地址
类名 *指针名
可以用类指针访问成员:p -> setx()
3.成员函数重载与线性表
成员函数重载与普通函数重载相同。
3.this指针
不同对象占据内存中的不同区域,它们的成员数据保存的位置各不相同,但对成员数据进行操作的成员函数的程序代码是同一段代码。当对一个对象调用成员函数时,编译程序先将对象的地址赋给this指针,然后调用成员函数,每次成员函数存取数据成员时,也隐含使用this指针。
this指针用来存放类对象的地址,在类内部访问其他成员函数,由于存在this
指针,可以不使用对象名.成员函数
的方式,在类外调用必须这样
二、构造函数和析构函数
1.构造函数
在类体内定义:
类名(参数表)
{
函数体
}
在类体外定义:
类名::类名(参数表)
{
函数体
}
1. 构造函数的特点:
- 参数可以缺省,可以无参。
- 可以重载
- 没有类型说明
- 无返回值
- 自动调用
- 以类名作为函数名
2. 当显式地定义了构造函数之后,系统不会自动生成缺省的构造函数。默认的(无参或缺省)构造函数只能有一个,即只能有一个构造函数可以在不提供参数而被调用。
3. 使用new运算符动态建立对象时可以直接初始化
A *pa1,*pa2;
pa1=new A(3.0, 5.0);//用new动态开辟对象空间,初始化
4. 局部对象,静态对象,全局对象的构造函数调用:
- 局部对象每一次定义对象都需要调用
- 静态对象只在第一次定义史调用
- 全局对象在main函数之前调用
2.析构函数
在对象生命期结束时,析构函数回收系统为对象分配的空间
~类名(参数表)
1. 析构函数的特征:
- 无参
- 不可重载
- 无返回值
- 不指定类型
- 在撤消对象时由系统自动调用的
- 不显式定义时,系统会自动生成一个
2. 对象中使用new运算符创建空间需要使用显式定义析构函数,在函数体中使用delete
运算符回收空间。
3. 当使用运算符delete删除一个由new动态产生的对象时,它首先调用该对象的析构函数,然后再释放这个对象占用的内存空间
4. 可以使用new
建立对象数组
pa1=new A[3];
使用delete []pa1
回收空间
3.调用顺序
全局对象:
局部对象
static局部对象:
new创建的对象:
4.拷贝构造函数
定义一个对象时,用另一个对象为其初始化
类名 :: 类名(类名 &对象名)
{
函数体
}
调用格式:
类名 对象1(对象2)
类名 对象1=对象2
没有定义显示定义拷贝构造函数时,系统会自动生成一个拷贝构造函数,将实参对象的所有成员数据一一复制到新对象:
类名(类名 &对象名)
{
成员数据1=对象名.成员数据1;
……
}
有时,自动生成的拷贝构造函数会出问题。
但是,当类中的数据成员中使用new运算符,动态地申请存储空间进行赋初值时,必须在类中显式地定义一个完成拷贝功能的构造函数,以便正确实现数据成员的复制。否则会出现两次回收同一段内存的错误,因为new
运算符所开空间通过指针使用。
5.在A中含B的对象的构造函数
在b类中使用a类的对象,构造函数的格式
class a
{
public:
int a;char b
...
}
clas b
{
public:
int a;
a a1;
b(int a,char b, intc):a1(a,b){a=c}
}
静态成员与友元
通常,每当说明一个对象时,把该类中的有关成员数据拷贝到该对象中,即同一类的不同对象,其成员数据之间是互相独立的
1.静态成员数据
如果将类中的某一成员数据用static
修饰,则这个成员数据会在编译时分配空间,该类所有的对象共享这块空间,即对于所有对象,该成员数据都是相同的,共享一块内存,所有对象都可以引用,公用的。
类的静态数据成员是静态分配存储空间的,而其它成员是动态分配存储空间的(全局变量除外)。当类中没有定义静态数据成员时,在程序执行期间遇到说明类的对象时,才为对象的所有成员依次分配存储空间,这种存储空间的分配是动态的;而当类中定义了静态数据成员时,在编译时,就要为类的静态数据成员分配存储空间。
- 静态成员受到访问权限约束,只有
public
权限的静态成员数据才能在类外被访问。 - 静态成员数据必须 在类体外初始化,格式:
数据类型 类名::静态成员数据名 = 初值
,不定义初值则默认为0 - 定义类以后,静态数据成员就可以被访问,不需要定义对象
- 使用作用域运算符
::
调用,类名::静态成员数据名
或对象名.静态成员数据名
- 静态成员数据可以在构造函数中赋初值,但是为了保证数据的一致性,一般是在定义性说明时赋初值。
2.静态成员函数
用static
修饰成员函数
2. 与静态数据成员一样,在类外的程序代码中,通过类名加上作用域操作符,可直接调用静态成员函数。
- 静态成员函数只能直接使用本类的静态数据成员或静态成员函数,但不能直接使用非静态的数据成员 (可以引用使用)。这是因为静态成员函数可被其它程序代码直接调用,所以,它不包含对象地址的this指针。
- 静态成员函数的实现部分在类定义之外定义时,其前面不能加修饰词static。格式
5. 不能把静态成员函数定义为虚函数。
3.友元函数
1. 普通函数作为友元函数
友元函数是一种定义在类外部的普通函数,其特点是能够访问类中私有成员和保护成员,即类的访问权限的限制对其不起作用。
friend <type> FuncName(<args>);
friend float Volume(A &a);
说明:
- 友元函数不是成员函数,是普通函数,没有
this
指针。 - 友元函数可以无视访问权限的限制,访问类的所有数据成员。
2. 其他类的成员函数作为类的友元函数
A类中的某个成员函数是B类中的友元函数,这个成员函数可以直接访问B类中的私有数据。
注意:
- 此时,应先定义B再定义A
- 对于B中需要用到A中数据成员的函数,其在B中声明,在A的定义之后给出具体的函数体定义
- A需要在B之前做提前声明
class A;
class B
{
};
class A
{
...
friend 函数类型 B::成员函数名(参数)
...
};
例:
class A;
class B
{
double h;
public:
B(double h){h=high;}
void cal(A &c) //B中的成员函数z需要使用A中的数据成员
};
class A
{
double r;
public:
A(double a){r=a;}
friend void B::cal(A &c);//A的友元,B的成员函数,在A中只能给出声明
};
void B::cal(A &c) // A的友元,B的成员,在A、B之外给出完整定义
{
double v = 3.14*c.r*c.r*h
}
友元类
一个类的友元可以自由地用该类中的所有成员。
B是A的友元类,B可以使用A中的所有数据成员
class A
{
.....
friend class B;
}
一般先定义A,再定义B,但是B会在A之前先声明。
运算符重载
为了重载运算符,必须定义一个函数,并告诉编译器,遇到这个重载运算符就调用该函数,由这个函数来完成该运算符应该完成的操作。这种函数称为运算符重载函数,它通常是类的成员函数或者是友元函数。运算符的操作数通常也应该是类的对象。
规定:
- 只能重载已有的运算符
- 不是所有运算符都可以重载
- 重载运算符*不能改变操作数的个数
- 重载不能改变运算优先级
不能重载:?:(三目运算符) .(成员运算符) .*(成员指针) ::(作用域运算符) sizeof(字节个数运算符)
1.用成员函数重载
格式:
类名 operator<运算符>(<参数表>)
{ 函数体 }
A operator + (A &);
重新定义运算符,由左操作符调用右操作符。最后将函数返回值赋给运算结果的对象,没有返回值就为void
类型。
2. 自增(自减)运算符重载:
前置:
<type> operator ++( )
{ ......;}
后置:
<type> operator ++(int)
{ ......;}
class A
{ float x, y;
public:
A(float a=0, float b=0){ x=a; y=b; }
A operator ++( ){A t; t.x=++ x; t.y=++y; return t;} //前置
A operator ++(int) { A t; t.x=x++; t.y=y++; return t;} //后置
};
void main(void)
{ A a(2,3), b;
b=++a;
b=a++;
}
调用格式:
b=a.operator++( );
b=++a;
注:成员函数实现运算符的重载时
- 运算符重载函数的参数只能有二种情况:没有参数或带有一个参数。对于只有一个操作数的运算符(如++),在重载这种运算符时,通常不能有参数;而对于有二个操作数的运算符,只能带有一个参数。这参数可以是对象,对象的引用,或其它类型的参数。
- 运算符的左操作数为当前对象,并且要用到隐含的this指针。运算符重载函数不能定义为静态的成员函数,因为静态的成员函数中没有this指针。
2.用友元函数重载
所有参与运算的对象都作为参数,放入参数表
friend <类型说明> operator<运算符>(<参数表>)
{......}
friend A operator + (A &a, A &b)
{.....}
调用:
c=a+b;
// c = operat0r+(a,b)
重载自增自减运算符时,对于后置需要在参数表中加一个int
参数,用以区分前置。当++
或--
后置时,则调用含有int
的函数
注:=
(赋值运算符),[]
(下标运算符),->
(指针运算符)不可以用友元函数重载。
3.转换函数
不同类型的数据运算时的相互转换规则,只能用成员函数实现。
声明格式:
operator < type >( )
定义格式(不需要返回值)
ClassName :: operator <type>( )
A :: operator float ( )
- 转换函数必须是类的成员函数
- 转换函数的调用是隐式的
转换函数可以实现cout << 对象
。
转换之后的数据如果支持某些运算符,则可以直接运算。
运算符调用顺序:
- 成员函数
- 非成员函数
- 转换后的类型是否支持该运算符
注:
- 转换函数只能是成员函数,不能是友元函数。
- 转换函数的操作数是对象。
- 转换函数可以被派生类继承,也可以被说明为虚函数。
4.重载赋值运算符
复合赋值运算符(+= -= *= /=
)可以用成员函数或友元函数重载,赋值运算符(=
)只能用成员函数重载。
1. 复合赋值运算符
声明格式:
返回值类型 operator+=(参数)
2. 赋值运算符
类名 &operator=(类名 &对象名)
{
成员数据1=对象名.成员数据1;
成员数据2=对象名.成员数据2;
...
成员对象n=对象名.成员数据n;
return *this;
}
5.perator 与&operator
简单来说就是operator 返回的是这个值,而&operator返回的是这个的地址(引用)。
主要的区别于用处就在于这个运算符的连用性,如果需要连用的话必须使用引用。
返回引用是为了能够连续赋值 如(a=b)=c,如果不返回引用的话像楼上说的那样,*this是当做临时变内量返回的容,C++为了保证临时变量从产生到返回不被修改,从而把临时变量定义为const,因而(a=b)=c的话,a=b为一个const,c是不能赋值给他的。
6.字符串类的重载
7.重载输入(提取)和输出(插入)运算符
1. 输入运算符(>>
)
声明格式:
friend istream &operater>>(istream &, ClassName &);
返回类istream的引用,cin中可以连续使用运算符“>>”。
例:
istream &operator>>(istream &is, incount &cc)
{
is>>cc.c1>>cc.c2;
return is;
}
2. 输出运算符(<<
)
声明格式:
friend ostream &operater<<(ostream &, ClassName &);
定义格式:
ostream &operator<<(ostream &os,incount &cc) //重载cout<<
{os<<"c1="<< cc.c1<<'\t'<<"c2="<< cc.c2<< endl; return os;}
返回值为is或os对象
继承与派生
在派生类中不可以定义基类的对象
三种访问权限:public
,protected
,private
1.单继承
class className: 访问权限 基类名
{
...
}
1. 公有派生(public
)
访问权限变化:
成员属性 | 派生类中 | 派生类外 |
---|---|---|
公有 | 可以访问 | 可以访问 |
protected | 可以访问 | 不可以访问 |
private | 不可以访问 | 不可以访问 |
class A { int x;
protected: int y;
public: int z;
A(int a,int b,int c){x=a;y=b;z=c;}//基类初始化
int Getx(){return x;}//返回x
int Gety(){return y;}//返回y
void ShowA(){cout<< "x="<<x<<'\t'<<"y="<<y<<'\t'<<"z="<<z<<'\n';}
};
class B:public A{
int m,n;
public:
B(int a,int b,int c,int d,int e):A(a,b,c){m=d;n=e;} //需要调用A的构造函数
void Show(){cout<<“m="<<m<<'\t'<<“n="<<n<<'\n';
cout<<"x="<<Getx()<<'\t'<<"y="<<y<<'\t'<<"z="<<z<<'\n'; }
int Sum(){return ( Getx()+y+z+m+n);}
};
void main(void)
{
B b1(1,2,3,4,5);
b1.ShowA();
b1.Show();
cout<< "Sum="<<b1.Sum()<<'\n';cout<<"x="<<b1.Getx()<<'\t';
cout << "y=" <<b1.Gety()<<'\t';
cout << "z="<<b1.z<<'\n';
return 0;
}
2. 保护派生(protected
)
成员访问权限降一级,public->protected->private
成员属性 | 派生类中 | 派生类外 |
---|---|---|
public | 可以访问 | 不可以访问 |
protected | 可以访问 | 不可以访问 |
private | 不可以访问 | 不可以访问 |
class A
{
int x, y;
protected:
A(int a,int b){x=a;y=b;}
public:
void ShowA(){cout<< "x="<<x<<'\t'<<"y="<<y<<'\n';}
};
class B: public A
{
int m;
A a1; //在派生类中也不可以定义A的对象,实际上还是类外调用
public:
B(int a,int b,int c):A(a,b){m=c;} //可以在派生类中调用A的构造函数
void Show()
{
cout<<"m="<< m<< '\n' ;
ShowA(); //在派生类中调用基类的函数
}
};
void main(void)
{ B b1(1,2,3); //可以定义派生类对象
b1.Show();
A aa; //不可定义A的对象(A是抽象基类)
}
3. 私有派生(private
)
访问权限全部变为private
访问权限:
成员属性 | 派生类内 | 派生类外 |
---|---|---|
public | 可以访问 | 不可以访问 |
protected | 可以访问 | 不可以访问 |
private | 不可以访问 | 不可以访问 |
class A
{
int x;
protected:
int y;
public:
int z;
A(int a,int b,int c){x=a;y=b;z=c;}
int Getx(){return x;}//返回x
int Gety(){return y;}//返回y
void ShowA(){cout<< "x="<<x<<'\t'<<"y="<<y<<'\t'<<"z="<<z<<'\n';}
};
class B:private A //私有继承
{
int m,n;
public:
B(int a,int b,int c,int d,int e):A(a,b,c){m=d;n=e;}//B的构造函数
void Show(){cout<<“m="<<m<<'\t'<<“n="<<n<<'\n';
cout<<"x="<<Getx()<<'\t'<<"y="<<y<<'\t'<<"z="<<z<<'\n'; }
int Sum(){return ( Getx()+y+z+m+n);}
};
void main(void)
{ B b1(1,2,3,4,5);
b1.ShowA(); b1.Show();
cout<< "Sum="<<b1.Sum()<<'\n';cout<<"x="<<b1.Getx()<<'\t';
cout << "y=" <<b1.Gety()<<'\t'; cout << "z="<<b1.z<<'\n';}
4. 抽象基类
这个类只能用作基类来派生出新的类,而不能用这种类来定义对象时,称这种类为抽象类。
将类的构造函数或者析构函数设为protected
时,该类为抽象类。
注:将类的构造函数或者析构函数设为private
没有意义
5. 构造函数
派生类构造函数名(总参数列表) 基类构造函数(参数列表)
{派生类新增数据的初始化语句}
6. 析构函数
先执行派生类的析构函数,再执行基类的析构函数
2.多继承
定义格式
class 类名:<访问权限>类名1,..., <访问权限>类名n
{
private: ...... ; //私有成员说明;
public: ...... ; //公有成员说明;
protected: ...... ; //保护的成员说明;
};
例:
class A{ int x1,y1;
public: A(int a,int b) { x1=a; y1=b; }
void ShowA(void){ cout<<"A.x="<<x1<<'\t'<<"A.y="<<y1<<endl; }
};
class B{int x2,y2;
public: B(int a,int b) {x2=a; y2=b; }
void ShowB(void){ cout<<"B.x="<<x2<<'\t'<<"B.y="<<y2<<endl; }
};
class C: public A,private B{ //多继承
int x,y;
public: C(int a,int b,int c,int d,int e,int f):A(a,b),B(c,d) {x=e; y=f; } //构造函数
void ShowC(void){cout<<"C.x="<<x<<'\t'<<"C.y="<<y<<endl;
ShowA();ShowB(); }
};
void main(void)
{ C c(1,2,3,4,5,6);
c.ShowC();
c.ShowA ();
c.ShowB ();
}
注:构造函数不能被继承,派生类的构造函数必须调用基类的构造函数来初始化基类成员基类子对象。
当派生类中有基类的对象时,在派生类的构造函数中需要单独调用对象的构造函数。
派生类构造函数的调用顺序如下:
- 基类的构造函数(基类的构造函数按照继承时的说明顺序调用)
- 子对象类的构造函数
- 派生类的构造函数
例:
class Derived:public Base2, public Base1
{
int z;
Base1 b1,b2; //基类的对象
public:
Derived(int a,int b):Base1(a),Base2(20), b1(200),b2(a+b) //构造函数单独初始化基类的对象
{z=b; cout<<"调用派生类的构造函数!\n";}
~Derived( ){cout<<"调用派生类的析构函数!\n";}
}
- 当基类有默认的构造函数或者没有定义构造函数,派生类的构造函数可以省略对接里构造函数的调用
- 基类构造函数使用1个或多个参数时,派生列必须定义构造函数,提供传参途径。
当撤销派生类对象时,析构函数的调用正好相反
3.继承的冲突与支配
1. 冲突
多继承时,可能出现派生类继承多个同名的数据成员或成员函数,需要使用类限定符区分:类::成员名;
例:
class A{
public: int x;
void Show(){cout <<"x="<<x<<'\n';}
A(int a=0){x=a;}
};
class B{
public: int x;
void Show(){cout <<"x="<<x<<'\n';}
B(int a=0){x=a;}
};
class C:public A,public B{ int y;
public: void Setx(int a){ x=a;} //c1对象中有两个x成员
void Sety(int b){y=b;}
int Gety() {return y;}
};
void main(void)
{ C c1; c1.Show(); //c1对象中有两个Show()函数
}
2. 支配
派生类中新增的数据成员或者成员函数可能会与基类的数据成员或者成员函数重名。
当派生类中新增加的数据或函数与基类中原有的同名时,若不加限制,则优先调用派生类中的成员。
class A{
public: int x;
void Show(){cout <<"x="<<x<<'\n';}
};
class B{
public: int y;
void Show(){cout <<"y="<<y<<'\n';}
};
class C:public A,public B{
public: int y; //类B和类C均有y的成员
};
void main(void)
{ C c1; c1.x=100;
c1.y=200; //给派生类中的y赋值
c1.B::y=300; //给基类B中的y赋值
c1.A::Show();
c1.B::Show(); //用作用域运算符限定调用的函数
cout <<"y="<<c1.y<<'\n'; //输出派生类中的y值
cout <<"y="<<c1.B::y<<'\n'; //输出基类B中的y值
}
3. 赋值
派生类对象可以赋值给基类对象,反之则不行。
赋值类型
-
派生类对象赋值给基类对象
-
派生类对象初始化基类引用
-
派生类对象的地址赋给基类的指针
class A
{
int x;
int y;
…
};classB:public A
{
int a;
int b;
…
}B b1;
A &a1 = b1;
基类对象只会使用派生类对象中基类有定义的内容
4. 补充
任一基类在派生类中只能继承一次,否则,会造成成员名的冲突。若在派生类中,确实要有二个以上基类的成员,则将基类的两个对象作为派生类的成员。
4.虚基类
实际的继承、派生关系很复杂,可能会出现一个派生类D中会有多个基类A的拷贝,造成数据成员的使用模糊。
虚基类可以使简介继承公共基类时,只保留一份公共基类的拷贝。
虚基类在派生时进行声明,只需要在每个直接继承的派生类中声明即可
class B:public virtual A //
{
类体
};
如果在虚基类中定义了带参数的构造函数或者没有定义带参数的构造函数,则在所有派生类中(直接、间接)都需要显示调用虚基类的构造函数。
- 不显式调用虚基类的构造函数,则使用默认构造函数
- 同时出现虚基类与非虚基类的构造函数,先调用虚基类的构造函数
- 只有创建对象的派生类才会调用虚基类的构造函数
虚函数
多态性
1.虚函数
虚函数可以实现,基类对象访问派生类中的同名成员函数。
可以在程序运行时通过调用相同的函数名而实现不同功能的函数称为虚函数
定义格式:
virtual 函数类型 函数名(参数列表)
一旦把基类的成员函数定义为虚函数,由基类所派生出来的所有派生类中,该函数均保持虚函数的特性。
在派生类中重新定义基类中的虚函数时,可以不用关键字virtual来修饰这个成员函数。
class A
{
protected:
int x;
public: A(){x =1000;}
virtual void print(){ cout <<“x=”<<x<<‘\t’; }//虚函数
};
class B:public A{
int y;
public: B() { y=2000;}
void print(){ cout <<“y=”<<y<<‘\t’; }//派生虚函数
};
class C:public A{ int z;
public: C(){z=3000;}
void print(){ cout <<“z=”<<z<<‘\n’; }//派生虚函数
};
void main(void )
{ A a, *pa;
B b; C c;
a.print(); b.print(); c.print(); //静态调用
pa=&a; pa->print();//调用类A的虚函数
pa=&b; pa->print();//调用类B的虚函数
pa=&c; pa->print();}//调用类C的虚函数
说明:
- 当在基类中把成员函数定义为虚函数后,在其派生类中定义的虚函数必须与基类中的虚函数同名,参数的类型、顺序、参数的个数必须一一对应,函数的返回的类型也相同,才能为派生虚函数(由于函数重载)。
- 必须使用基类类型的指针变量,并使该指针指向不同的派生类对象,并通过调用指针所指向的虚函数才能实现动态的多态性。
- 在派生类中没有重新定义虚函数时,当调用这种派生类对象的虚函数时,则调用其基类中的虚函数。(虚函数可以继承)
- 可以把析构函数定义为虚函数,但不能把构造函数定义为虚函数。
- 一个函数如果被定义成虚函数,则不管经历多少次派生,仍将保持其虚特性,以实现一个接口,多个形态。
虚函数比一般的成员函数执行得慢
虚函数用基类指针调用才能体现多态性,用对象调用与一般成员函数无区别
2.纯虚函数
在基类中不对虚函数给出有意义的实现,它只是在派生类中有具体的意义。这时基类中的虚函数只是一个入口,具体的目的地由不同的派生类中的对象决定。
定义:
virtual 函数类型 函数名(参数表)= 0;
说明:
- 纯虚函数没有函数体,只有声明语句
- 拥有纯虚函数的基类不能定义对象,但可以定义指针和引用。
=0
表示没有具体实现,在派生类中可以随便改写。
例:弦截法求解方程的根
# include<iostream>
# include<cmath>
using namespace std;
class root
{
double
}
3. 抽象类
至少包含一个纯虚函数的类。这种类只能作为派生类的基类,不能用来说明这种类的对象。
- 抽象类的纯虚函数可以是在抽象类中定义的,也可以是从抽象基类继承的。
- 抽象类不可以实例化,但可以定义指针和引用
- 不能用作函数参数类型、函数返回值类型或显示转换类型(不可实例化)
- 抽象类的派生类没有重载纯虚函数,给出函数实现,则该派生类仍为抽象类