目录
-
-
- [3 类 -- class](#3 类 -- class)
-
- [3.1 什么是类](#3.1 什么是类)
- [3.2 类的定义](#3.2 类的定义)
- [3.3 类内参数的使用](#3.3 类内参数的使用)
- [3.4 类与结构体 -- class VS struct](#3.4 类与结构体 -- class VS struct)
- [3.5 类的实操 -- log类(日志类)](#3.5 类的实操 -- log类(日志类))
- [3.6 类定义 与 `static`](#3.6 类定义 与
static
)
-
3 类 -- class
3.1 什么是类
- 类是面向对象的一种设计,其中封装了我们对对象的"描述",或者说对象的"特征"
- 比方说我们要描述一个游戏玩家,那么这个玩家至少要有以下"特征"
- 玩家在地图中的位置
- 玩家的速度
- 等等
- 在本节我们也会不断地举玩家这个例子
3.2 类的定义
class player
{
int x, y;
int speed;
};
int main()
{
player player1; //创建一个player变量
}
player player1; //创建一个player变量
一般称创建一个新变量的过程为"实例化",新的变量称为"对象"- 类的基础定义其实和结构体没什么两样,但以下我们来讲讲类的进阶玩法
3.3 类内参数的使用
-
假设此时玩家移动了,我们想计算出玩家移动后的位置,我们定义一个
Move
函数 -
注意:这里如果想调用类内的参数,像结构体一样在变量后加
.
就行void Move(player& player1, int& xa, int& ya) //xa,xy为位置偏移量
{
player1.x += xa * speed;
player1.y += ya * speed;
}int main()
{
player player1;
player1.x = 10;
player1.y = 0;
player1.speed = 2;Move(player1, 1, 1); return 0;
}
-
但如果你此时调用
Move
函数你会发现编译器报错了
-
这里报错的原因是,如果我们想使用
player
中的变量,需要给player中的变量打上public
的标签,来表明这个变量允许给类的外部使用void Move(player& player1, int& xa, int& ya) //xa,xy为位置偏移量
{
public:
player1.x += xa * speed;
player1.y += ya * speed;
} -
此时编译器就不会再报错了
-
Move
既然只给player
用,那我们就可以把Move
封装进player
中,以后这个Move
就专门给player用了,封装进player
中之后,player
中的变量不用打上public
的标签也能正常调用Move
,能让变量更加安全一些(当然,以下示例中没有删除掉public
是因为创建完变量后还要初始化) -
默认类中参数为"私有"
-
打上
public
标签之后的类中参数将会变成"公有" -
那么将函数封装进类中,此时我们称这个函数为"方法"
class player
{
public:
int x, y;
int speed;void Move(int xa, int ya) { x += xa * speed; y += ya * speed; }
};
-
此时我们如果想使用这个方法,只需要在变量后加
.
即可int main()
{
player player1;
player1.x = 10;
player1.y = 0;
player1.speed = 2;player1.Move(1, 1); return 0;
}
3.4 类与结构体 -- class VS struct
-
上文我们提到过类与结构体非常相似,事实上排除默认给类内参数提供
public
的话,类和结构体并没有本质区别,事实上他俩就是一样的,如果咱写过C实现 [贪吃蛇][贪吃蛇链接] 的话,其实在其项目中结构体的使用思想就是面向对象的思想,和类的使用是非常相似的(函数也可以封装进结构体成为方法) -
上文我们提到过,我们可以使用
public
将默认为"私有"的类内参数改为"公有",而结构体的参数默认为"公有",那么相反,我们可以使用private
将结构体内参数改为"私有",能够看到,参数改为私有之后调用就会报错了struct struct_player
{
private:
int x, y;
int speed;void Move(int xa, int ya) { x += xa * speed; y += ya * speed; }
};
int main()
{
struct struct_player player2;
player2.x = 10;
player2.y = 0;
player2.speed = 2;player2.Move(1, 1); return 0;
}
-
虽然在代码上类与结构体并没有本质上差别,但实际在使用中,他们俩的差别还是非常大的
-
CPP存在结构体这个玩意,纯粹是为了兼容C而做出的妥协,如果你只使用CPP中"更新"的内容,那完全可以删除结构体而不影响使用,甚至说你可以用
#define
把所有的struct
替换成class
,那你的代码中就不会存在struct
这个东西了 -
所以说我们在实际使用中是使用类还是结构体,纯粹是个人的编程风格,你想把
struct
当作class
用,也没问题,你想把class
当作struct
用,也没问题,纯粹取决于自己 -
比较通用的风格是,结构体只用来储存数据,不用来做任何"继承"操作(后面的文章咱们会提到的),例如我们想保存一个玩家在地图上的位置
typedef struct position
{
int x, y, z;
void Move(int& x, int& y, int& z, struct offset& os)
{
x += os.x;
y += os.y;
z += os.z;
}
}position; -
这里这个结构体仅仅只是数据的载体,而非类
-
比方说游戏CS2 中,有类
player
,阵营分为"反恐精英"和"恐怖分子",每个阵营的玩家有不同的模型,击杀提示,带不带"包"等等,两个阵营的玩家统统继承自player
这个大类,而结构体只用来存数据,它不是一个比较广的概念 -
即数据集用结构体,继承用类
3.5 类的实操 -- log类(日志类)
-
日志类用于根据消息的不同等级,向控制台输出不同等级的信息
-
起初日志会分为三个等级,分别为"错误","警告","消息及跟踪"
-
我们需要在log类内定义方法,使创建新log变量的时候能让其拥有某个具体等级,算是..."继承"?
-
向这个类传不同等级的信息而使其输出不同的等级的信息
-
信息等级不匹配变量的等级就报错
class Log
{
public: //将枚举类型转为公共使得其他函数也能用它
enum LogLevel //枚举类型用来规定等级
{
info, //普通消息
warning, //警告
erorr, //报错
};private:
LogLevel Level = info; //当前变量的等级(记得给个默认等级)//把方法转为公共,否则不能被类外的其他函数调用
//如果你在类中定义多个函数且是层层调用的话,只用公共最外层的接口就行了
//不转为公共的话这些函数就只能被类中其他函数调用了
public:
void SetLogLevel(const LogLevel seting_level) //用于实例化变量后赋予其他 等级
{
Level = seting_level;
}void Warn(const char* a) //如果当前log等级大于warning的话就允许输出warn信息 { if (Level >= warning) { std::cout << "[WARNING]: " << a << std::endl; } else //否则输出等级错误 { std::cout << "Log_Level error" << std::endl; } } void Err(const char* a) //如果当前log等级大于error的话就允许输出err信息 { if (Level >= erorr) { std::cout << "[ERROR]: " << a << std::endl; } else //否则输出等级错误 { std::cout << "Log_Level error" << std::endl; } } void Info(const char* a) //如果当前log等级大于info的话就允许输出info信息 { if (Level >= info) { std::cout << "[INFO]: " << a << std::endl; } else //否则输出等级错误 { std::cout << "Log_Level error" << std::endl; } }
};
int main()
{
Log log;log.SetLogLevel(log.info); //设置初始等级 log.Info("hello world!"); //打印符合该等级下的信息 log.Err("hello world!"); //打印不符合该等级下的信息 return 0;
}
- 不难看出,定义类这个操作只是创建一个"模具",真正造一个能用的变量出来还需要把一些参数扔到模具里面套(某种角度上说还是结构体那套逻辑)
3.6 类定义 与 static
-
和函数中的静态变量一样,无论外部如何改变,类中的静态变量有且只存在一个
-
封装性:
- 封装性是指将对象的数据成员和成员函数打包在一起,对外部隐藏对象的内部实现细节,只暴露必要的接口,
class
本身成员默认自带的private
属性就很好的表现了其封装性 - 封装性也可以体现为某个和成员变量因为使用了
static
,此成员变成了静态成员变量及函数,和类本身绑定在一起,成为了形容类这个东西本身的属性
- 封装性是指将对象的数据成员和成员函数打包在一起,对外部隐藏对象的内部实现细节,只暴露必要的接口,
-
被
static
标记的成员变量 -- 多个实例化对象共享该成员变量的空间,是类的一部分而不是实例化对象的一部分,一般称为静态成员变量 -
被
static
标记的成员函数(方法) -- 与被static
标记的成员变量一样,是类的一部分而不是实例化对象的一部分,仅可以用于静态成员变量,一般称为静态成员函数 -
比方说以下这个例子:
-
在这个例子中,我们定义手机这个类
-
在苹果鼎盛时期,国产手机有一种模仿苹果的浪潮,此时手机屏幕类型普遍为:刘海屏,那我们可以认为手机类的属性一定附带有刘海屏这个特征,这和其他配置无关
class Phone
{
public:
enum screenType
{
Bang_screen, //刘海屏(0)
hole_screen, //挖孔屏(1)
};private:
static screenType screentype; // 静态成员变量public:
// 静态成员函数,用于设置屏幕类型
static void setScreenType(const screenType type)
{
screentype = type;
}// 静态成员函数,用于获取屏幕类型 static screenType getScreenType() { return screentype; }
};
//初始化屏幕类型
Phone::screenType Phone::screentype = Phone::Bang_screen;
// 类型 | 需要初始化的成员名 | 需要赋的值
//(老实说这里其实可以不赋初值的,当然这取决于个人习惯)int main()
{
Phone xiaomi;
Phone::setScreenType(Phone::Bang_screen);
std::cout << Phone::getScreenType() << std::endl;std::cout << "一段时间后,挖孔屏逐渐变成了主流" << std::endl; //将手机屏幕设置为挖孔屏 Phone::setScreenType(Phone::hole_screen); std::cout << Phone::getScreenType() << std::endl; return 0;
}
-
-
其实咱也能看到,调用静态成员函数的时候,全都和对象没有任何关系,所有的操作都是在类上完成的
-
以上这个例子仅仅用来理解静态成员变量和静态成员函数与类的关系,其处于绑定状态,静态成员变量和静态成员函数属于类的一部分,对象只是共享空间,逻辑上我们认为对象继承了类的属性罢了
-
PS:在结构体中也可以遵循这套理论