在守望先锋学习C++的类与对象
Posted Booksort
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在守望先锋学习C++的类与对象相关的知识,希望对你有一定的参考价值。
C++是一门OOP(面向对象)的语言。
而C语言只是一门面向过程的语言。
里面有些思路需要重新改变。
前情提要:文章较长,请根据目录自行选择
一、守望先锋
天使姐姐不漂亮吗?
为一个场守望先锋街机先锋统计数据
如果是面向过程的程序员,可能会考虑:
记录每个玩家所选的英雄,击杀人数,对对面伤害量,治疗量,承受伤害量,命中率等等。
同时还要记录每个英雄的移动,释放技能,技能是否命中,技能造成的伤害的计算,甚至还要考虑技能的运动等等。
甚至还要考虑对方的技能对自己的影响,以及队友的技能对自己的影响。
用一个主函数 main 来获取所有数据。
调用另外一些函数来分别计算英雄的移动,技能伤害,技能影响,收到的伤害等等。
再调用另外一部分函数来显示结果。玩过守望先锋的玩家都知道,对于这些数据的 计算,统计,显示,都是瞬时性的,会随着玩家的操作不断变化。而且,一次组队并不是只打一场,可能会由两场左右。
守望先锋并不是传统的FPS游戏,而是FPS与Moba游戏的结合
而且,对于不同的英雄都要根据英雄的技能去计算,统计不同的数据。
如:安娜要分别统计开镜和不开镜的命中率,以及睡针的命中人数。
而法拉要统计火箭弹直接命中人数等等。
而不同模式,又会有不同的变化,对英雄的移动,技能的影响也是不同的。
如:战斗狂欢,让所有英雄的血量翻倍,技能CD减半,这就会造成很多不一样的“化学反应”。
像我有一次战斗狂欢打了 1.5h 才打完(路霸一直卡车旁边,都在卡加时),对于普通快速10分钟左右可是相当长的。对于比赛的各个数据上限又该怎么设置?
而不一样的地图对角色的移动,技能的影响又是不一样的。比如:地形杀。
但对于数据怎么办?又要重新统计。这相对于计算机而言都太复杂。(如果是这样,我相信网易的服务器早就崩了)。
总之,对于过程性编程,首先要考虑遵循的步骤,然后要考虑如何表示这些数据。
如果是一位OOP程序员,其不仅要考虑如何表示数据,还要考虑如何使用数据 。
我要跟踪什么呢?当然是每个玩家,因此要有一个对象来表示每个玩家的各个方面的数据。可以选择为各个不同的英雄定义不同的类,在通过类来为玩家创建对象,让计算机执行计算玩家之间的数据交互,可以自动计算。我们要做的仅仅是研究如何跟踪或表示,每个玩家之间的数据交互。
对于OOP程序员,只需为每个不同英雄定义属于他们的类,每次游戏开始,再为每个玩家根据其选择创建对象。再利用算法跟踪玩家之间的数据交互即可
这不比面向过程编程简单?(不知道我理解的对不对)
ps:Dv.A爱你呦~
二、对象和类
这个世界太复杂,处理复杂性的方法之一是简化和抽象。
守望先锋游戏中,通过为每个英雄定义一个类,为每个玩家创建一个对象来统筹数据。
在C++中,用户定义类型指点是实现抽象接口的类的设计。
(说人话就是按照需求,定义类。)
1、什么是类
我们都知道建一栋房子首先需要什么?
需要图纸,房子的结构图纸。
有了图纸,我们就可以根据图纸见很多大同小异的房子(一张图纸建出来的么)。
房子可以建很多,而图纸始终是那一张。
这样可以帮助我们更好理解。
对像是一个实体,而类只是一个自定义的类型。
就像int a;
a 是一个实体变量,在内存中有空间,而 int 只是一个类型,表明 a 的身份。
这样应该理解了吧。
基本类型(内置类型)定义变量完成了三项操作
- 决定数据对象需要的内存数量
- 决定如何解释内存在的位(long和float在内存中占用的位数相同,但是他们转换成数值的方法不同);
- 决定可使用数据对象执行的操作或方法。
对内置类型而言,有关信息全部被内置到编译器中。但是C++在自定义类型是,必须要自己提供所有信息。
类的实例化:就是根据类创建一个对象
C语言创建一个栈的”类“
C语言也是存在”类“。我们常用的结构体。
比如我们用C语言去是实现一个栈
#define CAP 4
typedef int STData;
typedef struct Stack//结构体用于维护栈
{
int top;//栈顶标记
STData* arr;//栈的指针
int capacity;//栈的容量
}STstack;
这是对于栈的数据的定义,栈就是我们用结构体定义的一个”自定义类型“–”栈类型“。(因为它并不具有自定义类型的全部信息)
void InitStack(STstack* st);//栈的初始化
void StackPush(STstack* st, STData n);//元素入栈
STData StackPop(STstack* st);//元素退栈
void StackExpansion(STstack* st);//扩容
int StackEmpty(STstack* st);//判断栈是否为空
void StackDestory(STstack* st);//销毁栈,防止内存泄漏
void StackPrint(STstack* st);//打印栈的元素,但前提是要退栈才能得到元素
这些函数是我们能够对栈执行的操作。
但是数据和执行方法都是分离的。
这就导致我们重点关注的是对栈操作的整个过程。
就是,我们这一步选择入栈,下一步选择弹栈。
C++创建一个栈的类
我们会将栈看为一个类,也就是一个类型,一个自定义类型。
而一个类型,我们需要两个部分
1,类的成员变量(类的属性)
2,类的成员函数(类的行为/能够执行的操作)
这个也满足上面关于类型的三项操作。
对于C++而言,就不再用结构体这个概念了,该叫类。
C++将struct
从C语言的结构体 - 升级到 - 类
你目前可以认为C++中的类就是C语言的结构体除了定义结构体成员变量还有结构体成员函数。
比如:
#define CAP 4
typedef int STData;
struct Stack//结构体用于维护栈
{
//结构体成员变量
int top;//栈顶标记
STData* arr;//栈的指针
int capacity;//栈的容量
//结构体成员函数
void InitStack(STData capacity=4);//栈的初始化//缺省
void StackPush(STData n);//元素入栈
STData StackPop();//元素退栈
void StackExpansion();//扩容
int StackEmpty();//判断栈是否为空
void StackDestory();//销毁栈,防止内存泄漏
void StackPrint();//打印栈的元素,但前提是要退栈才能得到元素
};
你目前可以理解成长这样。
而且,其类名就可以是其自定义类型的类名。
直接Stack a1;
。这就定义了一个对象。
而且也不用传上面指针过去了,可以a1.Init()
,就可以调用那些成员函数。
由于C++兼容C语言,这可以是一个类。但C++会使用class
(其中文有类的意思)。
2、类的定义
class ClassName//class是定义类的关键字 后面接一个类的名字
{
//类体:由成员变量和成员函数组成
};
与定义结构体很像。
对于类而言,其中的成员函数。你可以直接在类中定义。也可以在.h文件
类中声明,去.c文件
中定义。但是,在类中的成员变量,那只是声明,并不是定义。
声明与定义的区别
声明:告诉编译器,我有一个这样的东西在这,但并未实现。
定义:根据声明,去实现这个对象的需求。
形象来说就是:
游戏公司建了一个新游戏的文件夹,并向外发布公告,说未来会发布一款新游戏,也可能文件夹都还没建好。
这就是声明,只是告诉你有,但你不知道游戏剧情、内容、玩法是什么。只知道有这个游戏。
当游戏发售后,你买了。你就可以知道游戏的所有内容。
这就是定义。
声明是对的是一个属性,而定义对的是一个实体。
访问限定符
C++对于类中的成员提供了访问限定符
private(私有),public(公有),protected(保护)
。
他们描述了对类成员的访问控制
根据类创建的对象,都可以访问到对象类的公有部分,并且只能通过公有函数来访问对象类的私有成员
访问控制,也是对对象类的数据的保护。
OOP编程的主要目标之一是隐藏数据,因此这类数据通常放在私有部分,
而组成类接口的成员函数放在公有部分,否则就无法调用这些函数。
由于C++兼容C语言,且C++对C语言的结构进行了拓展,如
struct A
{};
class A
{};
在C++中,都是类。
两者区别
对于struct
创建的类,里面的成员默认公有。
对于class
创建的类,里面成员默认私有。
例如:创建一个OW英雄-安娜的类
class Ana
{
public:
void ShowData();//展示数据
void Teletherapy(bool input);//远程治疗
void SleepyNeedle(bool input);//睡针
private:
char Name[20];//名字
int Weight;//体重
int Height;//身高
double Speed;//移动速度
double Blood;//血量
//睡针类 睡针
};
私有数据,我们是不能修改或访问的,
只能通过公有函数去操作,或通过公有函数来了解私有数据的状态。
如果我们能直接访问到私有数据,那么我们也能对私有数据进行修改,但那样不就成开挂了吗?(正常人谁打竞技游戏开挂)。
所以,一般情况下,成员变量都是私有的,想给你用的成员函数是公有的,不希望被调用的函数是私有的。
类大小的计算
根据规则:计算类的大小,是不考虑类中成员函数的,只计算类中成员变量,同时还要考虑结构体内存对齐规则,也就是计算结构体大小的规则。
结构体内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8 - 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是
所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
那就计算一下
#include <iostream>
class Ana
{
public:
void ShowData();//展示数据
void Teletherapy(bool input);//远程治疗
void SleepyNeedle(bool input);//睡针
private:
char Name[20];//名字
int Weight;//体重
int Height;//身高
double Speed;//移动速度
double Blood;//血量
};
int main(void)
{
Ana a1;
Ana a2;
std::cout << "a1 = "<<sizeof(a1)<<std::endl;
std::cout << "a2 = "<<sizeof(a2)<< std::endl;
return 0;
}
创建了两个对象。
编译器输出结果。
来看看其内存的概念图
对于类中的成员函数是不会进行计算大小的。
3、this指针
这也是类中一个比较重要的知识点
通过一个日期类来分析(因为日期类比较简单)
简单的日期类
#include <iostream>
class Date
{
public:
void Init(int year = 1, int month = 1, int day = 1)//缺省参数
{
_year = year;
_month = month;
_day = day;
}
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day<<"日"<<std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1;//类的实例化
Date d2;
d1.Init(2002,2,5);//对象d1初始化
d2.Init(2019,4,1);//对象d2初始化
d1.print();//打印
d2.print();//打印
}
输出结果
在调用对象类的成员函数初始化对象 d1,d2 。
在成员函数中可以访问到对象封装的成员变量。
再介绍一下
对于类的实例化,创建对象。
Date d1;//类的实例化
Date d2;
系统会为对象d1 d2
在栈中分配空间。
但是只是对象中的成员变量分配在该对象的空间内。
成员变量与成员函数的空间分布
但是对于成员函数,其是在一个代码公共区段,而不是在每个对象空间的内部。不会每调用一次就分配一块空间。
每个对象都可以访问那个区段。
this指针的分析
我们就该想,当调用函数时,进入公共区段。如何确定调用的是那个对象的成员变量呢?
其实,传递过去的参数,不仅仅有显示声明定义的参数,还有一个隐藏的this
指针。
即,C++编译器会为每一个非静态成员函数配备一个隐藏的指针参数。
该指针指向当前对象(函数运行时调用该函数的对象)。
当然,你也可以这样写
void Init(int year = 1, int month = 1, int day = 1)//缺省参数
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
但是我们不能自己在参数列表中加一个this指针,因为编译器自己会传递,会处理。要是自己加了,就会造成参数缺失。
这样也是允许的,不容易搞混。
成员函数调用的真实样子
d1.Init(2002, 2, 5);//->Init(&d1,2002, 2, 5);
d2.Init(2019, 4, 1);//->Init(&d2,2019, 4, 1);
d1.print();//->print(&d1)
d2.print();//->print(&d2)
图片更好看一些。
注意
而且,对于类的成员变量,最好命名独特一点,不然,如果和缺省参数名字一样,编译器会无法识别,
this
指针指向不明,从而报错
this指针的特性
- this指针的类型:类型const
- 只能在成员函数的内部使用
- this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
this指针储存在哪?
就我目前微薄的知识,this
指针是储存在栈中的。
因为,this
指针毕竟是一个形参。而形参和局部变量都是储存在栈中的。
可以随着成员函数的调用而创建,函数结束就销毁。但是不同编译器是按照不同的规则的,比如VS就是通过ecx寄存器自动传递。
看看这个,来理解
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
void Show()
{
cout<<"Show()"<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
p->Show();
}
创建一个对象指针,并把它初始化为空。
p->PrintA();
如果this->_a
是行不通的,本身就是空指针,指向空,根本就不会有指向有权限的空间。算是非法访问。
但是p->Show();
,并未访问对象内,而是访问到类的公共区段。不会造成非法访问。
三、类的六个默认成员函数
类的默认成员函数即使我们
不自己声明定义,编译器也会自动创建定义
对于一个什么成员函数都没有的类,是一个空类。但是,当编译器处理时,会自动生成6个默认成员函数来防止出错。
事实上,真正用处大的是前4个,后面两个,基本上没多大用处。
1、 构造函数
该函数可以完成对对象的初始化。
C++提供这个默认成员函数。是为了解决,没有初始化就使用对象的问题。
对于日期类
class Date
{
public:
//void Init(int year = 1, int month = 1, int day = 1)//缺省参数
//{
// this->_year = year;
// this->_month = month;
// this->_day = day;
//}
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
private:
int _year;
int _month;
int _day;
};
如果没有Init()
函数,如果是C语言的话,就会报错,因为没有初始化。
就比如这样
int main(void)
{
Date d1;
Date d2;
d1.print();//->print(&d1)
d2.print();//->print(&d2)
}
我们没有初始化,却直接打印。事实上,编译器输出
虽然没有报错,但是却输出了随机值。这样也是防止了程序直接崩溃的问题。
构造函数的概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员
都有 一个合适的初始值,并且在对象的生命周期内只调用一次
这是一个默认成员函数,意思就是如果我们自己不定义,系统就会自动生成。
加上构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
输出结果
如果我们自己不定义,系统会自动生成,我们定义,系统就调用我们定义的函数。
什么?你不相信系统调用了构造函数
那我就加一个打印,来看看结果
Date(int year = 1, int month = 1, int day = 1)
{
std::cout << "Date()" << std::endl;
_year = year;
_month = month;
_day = day;
}
看看输出结果:
现在相信了吧。
构造函数的特性
构造函数虽然名字感觉像是负责为对象创建分配空间,
但实际上,起作用只是完成对对象的成员变量的初始化,防止未初始化就使用而造成的程序崩溃。
特性
- 名字与类名一样
- 函数无返回值(注意:是无返回值,
void
是返回一个空,实际上还是有返回值,而构造函数其根本就没有返回类型) - 在对象实例化时,编译器会自动调用对应的构造函数(我们没定义就调用默认的)
- 构造函数可以重载
虽然可以重载
但是
Date()
{
std::cout << "Date()" << std::endl;
_year = 10;
_month = 1;
_day = 1;
}
Date(int year=1, int month = 1, int day = 1)
{
std::cout << "Date()" << std::endl;
_year = year;
_month = month;
_day = day;
}
却不受欢迎的,因为编译器也不知道如何处理。会报错。
但可以这样
Date(int year,int month = 1, int day = 1)
{
std::cout << "Date()" << std::endl;
_year = year;
_month = month;
_day = day;
}
在初始化的时候,自己无论如何都要提供一个值。
或者
Date()
{
std::cout << "Date()" << std::endl;
_year = 10;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)
{
std::cout << "Date()" << std::endl;
_year = year;
_month = month;
_day = day;
}
可以运行,但还不如全缺省的构造函数。
自定义类型与内置类型
内置类型:内置类型就是语法已经定义好的类型:如int/char…,
自定义类型:是我们使用class/struct/union自己定义的类型
对于普通的成员变量而言,其默认的构造函数,感觉效果并不怎样。但是,如果有一个成员变量也是类?(自定义类型)。
#include <iostream>
class A
{
public:
A()
{
std::cout << "A()" << std::endl;
}
private:
int a;
};
class Date
{
public:
void print(void)
{
std::cout << _ye以上是关于在守望先锋学习C++的类与对象的主要内容,如果未能解决你的问题,请参考以下文章