类的介绍与使用
类的定义与目的:
定义:一种数据类型
目的:归纳某种对象,包含该对象的各种属性
类的特性:
1.类中的参数默认是私有的,只能提供类中的函数进行访问
class 与 struct 的区别
- class中成员默认为private,struct中成员默认为public
- struct留下是为了兼容C语言
类的使用:
1.类的创建:
class Player{
public://变为公有,才能访问
int hp,mp;
};// 不要漏了 ";"
int main(){
Player player;
player.hp=1;
player.mp=1;
}
2.构造函数:
函数名为类名的成员函数,不可能为静态函数。
1、无参构造函数:
若没有调用构造函数,会自动调用该函数。C++自己有默认的构造函数
class Temple{
int x,y;
Temple(){
x=1;
y=1;
}
}
2、一般构造函数:
带有参数的构造函数,可以有多个构造函数。会根据传入的参数来调用不同的构造函数。
class Temple{
int x,y;
Temple(){
x=1;
y=1;
}
Temple(int x){
this.x=x;
y=-1;
}
Temple(int a,int b):x(a),y(b){
}
Temple(Temple temple){
x=temple.x;
y=temple.y;
}
}
int main(){
Temple t1;//无参
Temple t2(2);// 第二个
Temple t3(2,2)// 第三个
Temple t4(t1)//第四个
}
注意:要使用class或者struct 作为STL容器的类型时,若自定义有参数的构造函数,需要明写出无参的构造函数。因为作为容器类型时,会调用无参的构造函数,若没有明写出,会出错
成员变量列表
class Example{
int x,y;
Example(int X,int Y):x(X),y(Y){}
Example(int a,int b):y(a),x(b){}//error x=a,y=b,按照声明的顺序进行初始化,不理前面的名称
}
与在函数体内构造的区别:
1.当成员变量中有类对象(自己写的、string)时,在函数体内会生成两个对象,第一个对象使用默认构造函数生成,第二个再根据情况生成并覆盖原对象,原对象丢弃。
class Example{
Example(){
cout<<"No1"<<endl;
}
Example(int x){
cout<<"No2"<<endl;
}
};
class Ex{
string str;
Example e;
Ex(){
str="YOU";//
e=new Example(1);//会输出 No1 与No2
}
}
str会先调用默认构造函数生成对象,再使用"YOU"来生成另一个对象并覆盖原本的,原本的被丢弃
e会调用默认构造函数生成对象,再使用参数构造函数生成另一个并覆盖原本的,原本的被丢弃
而使用列表则只会根据情况生成一个对象,减少消耗
class Ex{
string str;
Example e;
Ex():str("YOU"),e(Example(1)){};// 只输出 No2
};
3.拷贝构造函数
拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构造及初始化。利用旧对象构造新对象。
其唯一的形参必须是对象的引用,一般会加上const。
由于同一个类的对象在内存中有完全相同的结构,因此作为一个整体进行复制或称拷贝是完全可行的。而且拷贝过程中只需要拷贝成员变量,因为函数成员是共用的(只有一份拷贝)。
在类中如果没有显式给出拷贝构造函数时,则C++编译器自动给出一个缺省的拷贝构造函数。
使用拷贝构造函数的情况:
1)类对象作为函数的参数,以值传达的方式传入。
class Ex{
};
void Funtion(Ex ex){}// 作为函数的参数
2)作为函数的返回值,以值传达的方式返回。从局部对象到临时对象的拷贝构造。
class Ex{
};
Ex Funtion(Ex ex){
return ex;
}
3)用一个对象给另一个对象进行初始化,常称为赋值初始化。
int main(){
Ex e1(1,2);
Ex e2(e1);
Ex e3=e1;
}
默认拷贝构造函数:
没有定义拷贝构造函数的情况下,C++会生成默认的拷贝构造函数来实现对象的拷贝。等效于下面的拷贝构造函数。
class Ex{
int a,b;
int* c;
Ex(const Ex& ex){
a=ex.a;
b=ex.b;
c=ex.c;// 指针也是直接等于
}
};
但默认拷贝构造函数有个缺点,它是浅拷贝。若成员变量中包含指针,则新旧对象中该指针会指向同一个内存地址。之后无论新旧对象哪个修改了的指针的值,两个对象的都会发生改变。
因此包含指针时,应进行深拷贝。让两个指针指向不同的内存地址。
class Ex{
int a,b;
int* c;
Ex(const Ex& ex){
a=ex.a;
b=ex.b;
c=new int;// 对于指针,不是简单的等于。而为其申请地址
*c=*(ex.c);
}
};
4.子类使用父类的构造方法
3.析构函数:
当类对象被删除,即超出作用域时,会调用析构函数,可以在该函数释放成员变量所占内存。
class Temple{
int *x;
Temple(){
x=new int [10];// 分配内存
cout<<"Creat"<<endl;
}
~Temple(){// 析构函数
free(x)// 释放x所占的内存
cout<<"Over"<<endl;
}
}
4.类中的方法:
可以设置为private或public
class Player{
public://变为公有,才能访问
int hp,mp;
void Change(int h,int m){
hp=h;
mp=p;
}
void Change1(int hp,int mp){// 与类中成员同名时,使用this代表类中的成员
this.hp=hp;
this.mp=mp;
}
};// 不要漏了 ";"
类的继承:
1.使用方法:
拥有父类的所有成员,大小也包含父类的大小
注意:基类的指针可以指向子类,但子类的指针不能指向基类。
class Base{
int x,y;
void Move(){
return x;
}
};
class Player: public Base{// 继承了 Base类
char* name;
void Print(){
cout<<name<<endl;
}
};
int main(){
Player player;
player.x=5;
Player* p=new Base();//合法
Base* base=new Player();// 非法
}
2.虚函数 virtual:
用于实现多态,子类可以重写父类的虚函数。
原理:利用了动态联编,通过虚函数表来在运行时确定调用的函数。
对于非虚函数,在编译时就确定要调用哪个函数,因此调用非函数
虚函数表:
当一个类包含虚函数时,会生成虚函数表,保存虚函数的地址。其派生类也会有虚函数表
创建类实例时,若有虚函数,则会生成虚函数指针,指向其虚函数表。
当一个基类的指针指向了 子类对象,则运行时,会使用子类的虚函数表指针寻找子类虚函数的地址,而不是基类的。
编译器创建虚函数表的过程:
1.拷贝每个基类的虚函数表。
2.查看子类是否重写了基类的虚函数,有则修改子类的虚函数表;查看子类是否增加了虚函数,有则加入虚函数表
使用情况:
有一个基类与一个子类
class Base(){
void Print1(){// 非虚函数
cout<<"Base"<<endl;
}
virtual void Print2(){
cout<<"Base"<<endl;
}
};
class Son():public Base{
void Print1(){
cout<<"Son"<<endl;
}
void Print2() override{// 重写虚函数,加上override关键字,非必要,但可以检测错误
cout<<"Son"<<endl;
}
};
1.基类指针指向子类,且调用非虚函数:会调用基类的函数,而不是子类的函数
int main(){
Son* s=new Son();
Base* base=s;// 基类指针指向
base->Print1();// 输出“Base”,非虚函数
}
2.基类指针指向子类,且调用虚函数:会根据虚函数表调用子类函数,
int main(){
Son* s=new Son();
Base* base=s;// 基类指针指向
base->Print2();// 输出“Son”,虚函数
}
虚函数的缺点:
- 增大内存的消耗
- 每次调用虚函数时,要遍历虚函数表
3.纯虚函数(接口)
基类的虚函数只是一个声明,并没有实现。子类必须实现该虚函数。
使用场景:可以用于制作模板,规定子类都必须实现某些函数
有纯虚函数的子类,不能实例化。
class Base(){
virtual void Print()=0;// 纯虚函数
};
class Son1():public Base{
void Print() override{// 实现纯虚函数,加上override关键字,非必要,但可以检测错误
cout<<"Son1"<<endl;
}
};
class Son2():public Son2{
void Print() override{// 由于基类实现了纯虚函数,所以该类可以不重写Print()
cout<<"Son2"<<endl;
}
};
void PrintName(Base* b){
b->Print();
}
int main(){
Base* base=new Base();// 会报错,不能实例化
Son1* son1=new Son1();
Son2* son2=new Son2();
PrintName(son1);// 输出“Son1”
PrintName(son2);// 输出“Son2”
}
可见性
1.private
只能在该类的内部访问,子类也不行。只能提供类中的函数进行调用与修改,在其他地方不能使用
class Entity{
private:
int x,y;
int PrintX(){
return x;
}
public:
int Print(){
return PrintX();// 只能在类在的函数进行调用
}
};
class Son:public Entity{
public:
Son(){
x=2;// error,子类也不行
PrintX();// error
}
}
int main(){
Entity e;
e.x=2;// error
e.PrintX();// error
e.Print();
}
2.protected
自己与子类可以访问
class Entity{
protected:
int x,y;
int PrintX(){
return x;
}
public:
int Print(){
return PrintX();// 只能在类在的函数进行调用
}
};
class Son:public Entity{
public:
Son(){
x=2;// OK,子类可以
PrintX();// OK
}
}
int main(){
Entity e;
e.x=2;// error
e.PrintX();// error
e.Print();
}
3.public
任何地方都可以访问。