在守望先锋学习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 的身份。

这样应该理解了吧。
基本类型(内置类型)定义变量完成了三项操作

  1. 决定数据对象需要的内存数量
  2. 决定如何解释内存在的位(long和float在内存中占用的位数相同,但是他们转换成数值的方法不同);
  3. 决定可使用数据对象执行的操作或方法。

对内置类型而言,有关信息全部被内置到编译器中。但是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++将structC语言的结构体 - 升级到 - 类

目前可以认为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;//血量
	//睡针类 睡针
};

私有数据,我们是不能修改或访问的,
只能通过公有函数去操作,或通过公有函数来了解私有数据的状态。

如果我们能直接访问到私有数据,那么我们也能对私有数据进行修改,但那样不就成开挂了吗?(正常人谁打竞技游戏开挂)。

所以,一般情况下,成员变量都是私有的,想给你用的成员函数是公有的,不希望被调用的函数是私有的。

类大小的计算

根据规则:计算类的大小,是不考虑类中成员函数的,只计算类中成员变量,同时还要考虑结构体内存对齐规则,也就是计算结构体大小的规则。

结构体内存对齐规则

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是
    所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

那就计算一下

#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指针的特性

  1. this指针的类型:类型const
  2. 只能在成员函数的内部使用
  3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  4. 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;
}

看看输出结果:
在这里插入图片描述
现在相信了吧。
在这里插入图片描述

构造函数的特性

构造函数虽然名字感觉像是负责为对象创建分配空间,
但实际上,起作用只是完成对对象的成员变量的初始化,防止未初始化就使用而造成的程序崩溃。

特性

  1. 名字与类名一样
  2. 函数无返回值(注意:是无返回值,void是返回一个空,实际上还是有返回值,而构造函数其根本就没有返回类型)
  3. 在对象实例化时,编译器会自动调用对应的构造函数(我们没定义就调用默认的)
  4. 构造函数可以重载

虽然可以重载
但是

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++的类与对象的主要内容,如果未能解决你的问题,请参考以下文章

守望先锋开启压力测试

守望先锋app

《守望先锋》压力测试时间延长一天

浅谈《守望先锋》中的 ECS 构架

守望先锋图片

守望先锋app