上一篇我们做了一个简单的界面优化,并且我们可以选择进入游戏界面,所以这一篇我们来实现贪吃蛇和食物。
C++学习,b站直播视频
文章目录
- 4.0 课程目标
- 4.1 结构体
- 4.1.1 c语言面向对象
- 4.1.2 c++的结构体
- 4.1.3 内存对齐
- 4.2 union
- 4.2.1 union应用,判断大小端
- 4.3 类
- 4.3.1 类的封装
- 4.3.2 类的构造函数和析构函数
- 4.3.3 拷贝构造函数
- 4.3.4 移动构造函数
- 4.3.5 =delete =default
- 4.3.6 初始化列表
- 4.3.7 this指针
- 4.3.8 运算符重载
- 4.3.9 拷贝赋值函数
- 4.3.10 移动赋值函数
- 4.4 友元
- 4.4.1 友元函数
- 4.4.2 友元类
- 4.4.3 友元成员函数
- 4.5 食物+蛇的实现
- 4.5.1 食物代码
- 4.5.2 蛇代码实现
4.0 课程目标
我们这节课的目标就是实现贪吃蛇的的也要功能,实现蛇的移动,和吃食物会长身体,如果一节讲不完,就拆成两节。
4.1 结构体
首先我们来看食物是怎么描述的,食物具体是什么,具备一个x坐标,和一个y坐标,并且支持一个随机产生食物的方法。
所以我们这里可以使用类,因为我们学习面向对象最值得吹牛逼的一句话,就是万物皆对象。记得这句是可以吹水的,哈哈哈。
但我这里就想用结构体来描述食物,当然是可以的,我们在c语言中,没有类的时候,不也是直接上结构体么,结构体就是我们的类。
既然说到c语言,知道c语言是怎么实现面向对象么?
4.1.1 c语言面向对象
// 写一个随机一个x,y坐标
void GetFood(struct Food* f, int Height, int Width) // 需要把自己对象传递到函数中,(相当于this指针)
{
srand(time(0));
f->x = (rand() % (Width - 2)) + 1; // 不要随机到两条边上
f->y = (rand() % (Height - 2)) + 1;
}
struct Food {
int x; // x 坐标
int y; // y 坐标
// 既然要直接面向对象,肯定要有方法才行,那c怎么实现的方法,c实现的方法,是使用函数指针
void(*getFood)(struct Food* f, int Height, int Width); // 这个就相当于类的方法
};
int main()
{
//std::cout << "Hello World!\n";
// 我们来试一下c方式,实现的
struct Food f;
f.getFood = GetFood; // 需要对指针赋值,所以这个创建对象,c语言习惯写一个函数,Food *newFood(); 在函数内部把指针准备好
f.getFood(&f, 22, 44); // 就是这样伪造了一个对象的封装属性
}
4.1.2 c++的结构体
其实如果是c++结构体,不用这么复杂,本来就支持在结构体中实现自己的函数,这是因为c++对结构体做了一个扩充,c++的结构体其实就是一个类,跟类的唯一不同就是结构体的所有成员和方法都是public,而类需要自己定义,这个到类的章节再介绍。
所以我们直接写c++的结构体:
struct FoodCPP {
int x; // x 坐标
int y; // y 坐标
void GetFood(int Height, int Width) // 这个编译器在函数内部是可以方法到结构体的变量的
{
srand(time(0));
x = (rand() % (Width - 2)) + 1; // 是不是可以访问到
y = (rand() % (Height - 2)) + 1;
}
};
FoodCPP fcpp;
fcpp.GetFood(22, 44); // 是不是很方便
4.1.3 内存对齐
不知道大家听说过结构体内存对齐不?
举个例子:
// 内存对齐
struct test {
char a;
int b;
short c;
};
test t;
std::cout << sizeof(t) << std::endl // 这个t有多少个字节
输出结构是12。知道为啥不
struct test {
char a; // 1字节
int b; // 4字节
short c; // 2字节
};
// 不知道地址,那我们就打印一下
std::cout << &t << std::endl; // 000000BF68AFF6B8 // 4字节 a -> b
t.a = 0xff;
std::cout << &t.b << std::endl; // 000000BF68AFF6BC // 4字节
t.b = INT_MAX;
t.c = 65536;
std::cout << &t.c << std::endl; // 000000BF68AFF6C0 // 4字节
我们可以直接gdb来查看内存的值的改变。
从图看出,我们这个windows系统是4字节对齐的,每个变量都朝着4字节去对齐。
为啥需要对齐呢?
是因为cpu取值的时候,如果对齐的话,就会一次取值指令周期就可以取到值了,如果不对齐,可能需要两次或者更多次。
但是有一种情况,我们需要强制一字节对齐,就是在网络传输的过程中,我们传输一个结构体出去,如果对面的操作系统是8字节对齐的,那我结构体的值不是凉凉了。所以这种情况,我们需要强制一字节对齐。
#pragma pack(n) // 强制多少字节对齐
#pragma pack() // 接触强制,按默认
一设置强制,在去看看,大小就是7了。
关于计算机之间网络通信,我们到linux 网络篇,再做介绍。
4.2 union
结构体老哥出现了之后,这个联合体(有些地方也叫共用体)的老弟也要出来露露面。
那结构体和联合体有啥亲戚关系呢?其实一点关系都没有,但是写法上还是还相似。
union uu {
char x;
int y;
};
是不是很像结构体,但是其实联合体的成员是共用一块内存,也就是说上面的uu的大小是4字节,大家可以自行测试,这就是结构体和联合体的区别,那联合体有啥用呢?其实用处也挺大的,下面就是一个常见的笔试题。
测大小端之前,这里先补一个知识,什么是大小端?
大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,存储模式类似把数据当作字符串顺序处理。
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,存储模式将地址的高低和数据位权有效地结合起来。
来自百度百科。
4.2.1 union应用,判断大小端
// union判断大小端
uu u;
u.y = 1; // 给int 四个字节赋值了一个1
std::cout << &u << std::endl; // 0x0000008b65eff8d4
std::cout << &u.y << std::endl; // 0x0000008b65eff8d4
// 如果int的 高位 -> 低位是怎么排的
// 00 00 00 01
if (u.x == 1) // 低位 对于低位 小端
{
std::cout << "小端" << std::endl;
}
else if (u.x == 0) // 如果是0的话,就说明低位在高位,所以大端
{
std::cout << "大端" << std::endl;
}
union以后还会使用,大小端以后也会使用,这里大家可以好好消耗一下。
这里吐槽一下go语言,没有联合体,也不能对内存经常一字节对齐,真的有点头大。
gdb看到的内存是从小到大排的,低字节在低位,所以是小端模式。
4.3 类
经过前面结构体和联合体的学习,现在就很期待我们类的学习了吧,类的学习分为两部分学,这一篇我们学习类的一些简单的知识,类的继承,多态,留在我们后面学习。
4.3.1 类的封装
我们知道面向对象的三大特性,第一个就是封装,类就是负责把自己有关的属性封装起来,我们这里通过写蛇的代码来看看怎么封装:
// 类的封装
// 蛇的节点
struct SnakeNode {
int x; // 蛇也或x y坐标
int y;
SnakeNode* next; // 蛇除了x ,y坐标,还是两个上个结点和下个结点的指针,因为一个蛇是有很多结点的
SnakeNode* prev;
};
class Snake {
public:
// 我们使用什么来表示一条蛇,我使用的是链表,蛇的每个节点就是一个链表的结点
private:
// 蛇头结点:
SnakeNode* m_head; // 写类的成员变量,比较喜欢用m_开头
// 还要记录蛇的长度,虽然遍历一轮链表也能得到,但能快速取到的为啥要遍历
int m_snakeLen;
public:
// 除了成员变量外,一个类也要有成员方法
// 蛇的移动,蛇肯定自己知道自己怎么移动的是吧
void Right() { // 这种写在类定义中的,基本都是编译器搞成内联函数
// 知道啥是内联函数不?内联函数就是当我们调用这个函数的时候,编译器会自动给我们展开在本地,不用走函数调用那个过程(第二节讲过)
std::cout << "右移" << std::endl;
}
// 竟然可以写在类里面的就可以写在类外面的
void Left(); // 这个是一个函数声明
// 都写到这里,随带提一个静态成员
static int a; // 这个静态成员是不属于对象的,属于整个类的,就是所有对象共享一个静态成员变量。(其实我看这个变量的地址就知道了)
static void haha() { // 除了静态变量外,还存在一个静态成员方法,也是由类持有的。
std::cout << "右移" << std::endl;
}
};
// 真正的函数在这里
void Snake::Left() // 要把Snake带上,要不然 那知道是哪个类的是吧
{
std::cout << "左移" << std::endl;
}
// 类的使用
Snake s; // 申请一个类对象,跟我们平时申请变量是一样的
// s.m_head = ; // 发现我们调用不了?这是因为类有权限控制,在我们没写权限控制的时候,都是private,这里是不是想起了结构体,
// 结构体在默认情况下都是public,这就是类和结构体的区别
// 在我们一股脑的加上public后,
// s.m_head = nullptr; // 这样改成私有的访问变量就异常了
s.Left(); // 访问无异常
// 但是我们这里就有一习惯,一般变量都是私有的,如果需要访问变量,我们才会提供公有方法间接访问变量
4.3.2 类的构造函数和析构函数
按照我们上面写的,申请一个Sanke的对象,结果发现这个对象啥也没初始化,还是自己在外部初始化,这种事情在c++是不存在的,c++专门定义了一种叫构造函数的语法,来完成类的初始化,并且不需要自己手动调用,由编译器自行调用,是不是很贴心。
// 类的封装
// 蛇的节点
struct SnakeNode {
int m_x; // 蛇也或x y坐标
int m_y;
SnakeNode* Next; // 蛇除了x ,y坐标,还是两个上个结点和下个结点的指针,因为一个蛇是有很多结点的
SnakeNode* Prev;
// 结构体也可以有构造函数的
SnakeNode(int x, int y)
{
m_x = x;
m_y = y;
Next = nullptr;
Prev = nullptr;
}
};
class Snake {
public:
// 构造函数的名字跟类名字是一样的
Snake() { // 这个就是构造函数
std::cout << "Snake" << std::endl; // 这个调用就是 Snake s;
}
// 构造函数其实也可以重载的
Snake(int start_x, int start_y, int start_len) // 构造函数支持传参,我们传参 蛇头开始的x和y的点,开始蛇的长度
// 来构造默认的小蛇
{
// 先判断参数是否正确,首先蛇的长度要大于start_x
if (start_len < start_x)
{
// 原始长度大于蛇的x结点
return;
}
m_head = new SnakeNode(0, 0); // 空结点
auto pPrev = m_head;
for (int i = 0; i < start_len; i++)
{
auto node = new SnakeNode(start_x - i, start_y);
pPrev->Next = node;
node->Prev = pPrev;
pPrev = node;
m_snakeLen++;
//m_tail = node;
}
}
// 我们在构造函数中使用了new来申请内存了,需要在析构的时候也要delete掉
// 析构函数这样写:
~Snake()
{
// 如果类中有指针,在析构函数里面一定要处理
auto node = m_head; // 因为我们这个是链表,所以要循环delete
while (node)
{
// 把next指针先保存好
auto next = m_head->Next;
delete node; // 删除node
node = next; // next就是下一个node,一直循环删除
}
}
// 蛇头结点
// 我们使用什么来表示一条蛇,我使用的是链表,蛇的每个节点就是一个链表的结点
private:
// 蛇头结点:
SnakeNode* m_head; // 写类的成员变量,比较喜欢用m_开头
// 还要记录蛇的长度,虽然遍历一轮链表也能得到,但能快速取到的为啥要遍历
int m_snakeLen;
public:
// 除了成员变量外,一个类也要有成员方法
// 蛇的移动,蛇肯定自己知道自己怎么移动的是吧
void Right() { // 这种写在类定义中的,基本都是编译器搞成内联函数
// 知道啥是内联函数不?内联函数就是当我们调用这个函数的时候,编译器会自动给我们展开在本地,不用走函数调用那个过程
std::cout << "右移" << std::endl;
}
// 竟然有写在里面的就有想成外面的
void Left(); // 这个是一个函数声明
private:
// 都写到这里,随带提一个静态成员
static int a; // 这个静态成员是不属于对象的,属于整个类的,就是所有对象共享一个静态成员变量。(其实我看这个变量的地址就知道了)
static void haha() { // 除了静态变量外,还存在一个静态成员方法,也是由类持有的。
std::cout << "右移" << std::endl;
}
};
4.3.3 拷贝构造函数
除了默认构造函数外,还有一个拷贝构造函数,就是我是一个对象赋值给另外一个对象,
// 拷贝构造函数
Snake s1 = s; // 这种调用的就是拷贝构造函数
// 因为我们类里有指针,如果是这种浅拷贝的话,就是导致直接拷贝指针的值,这样容易出现问题,所以我们要重写拷贝构造函数
// 我们来测试一下写的拷贝构造函数对不对
Snake s2(3, 1, 3); // 创建一个 头结点为3,1的蛇头,长度为3
std::cout << s2.GetHead()->m_x << s2.GetHead()->m_y << std::endl;
Snake s3 = s2; // 使用拷贝构造函数
std::cout << s3.GetHead()->m_x << s3.GetHead()->m_y << std::endl;
// 拷贝构造函数这样写
Snake(const Snake& s) // 这个s就是模板s,传递s这个值来构造一个新的对象
{
m_head = new SnakeNode(0, 0); // 申请一个空结点
auto pPrev = m_head;
auto pTargetNode = s.m_head; // 目标蛇的头结点
for (int i = 0; i < s.m_snakeLen && pTargetNode; i++) // 按照对方蛇的长度进行初始化蛇
{
pTargetNode = pTargetNode->Next; // 取出第一个节点,因为m_head指向的第一个节点时空结点
auto node = new SnakeNode(pTargetNode->m_x, pTargetNode->m_y); // 以另一条蛇的结点,创建一台新蛇
pPrev->Next = node;
node->Prev = pPrev;
pPrev = node;
m_snakeLen++;
//m_tail = node;
}
}
因为我们含有大量的指针,所以拷贝构造函数需要重新new。
4.3.4 移动构造函数
c++11后,添加了一个移动构造函数,为啥要增加一个移动构造函数呢?
我们来看看这个例子:
// 移动构造函数
Snake s4(3, 1, 3); // 看一下调用了多少次构造函数
// 这时候我不需要s4对象了,需要把s4对象转移给s5,按照以前的做法,只能是调用拷贝构造函数,然后在把s4释放掉,
// 现在c++11有了移动构造函数,就是可以把这块内存,直接转移到新的对象上,被转移的对象不能使用了
// 还记得我上节课介绍的右值引用么?没错这个移动构造函数的的参数就是右值引用,我们可以使用std::move把左值变成右值
Snake s5 = std::move(s4);
// 接下来我们看看移动构造函数的样子
// 没错,就是长的跟右值引用差不多,总感觉是不是左值引用被拿来当拷贝构造函数了,所以只能在家一个语法
Snake(Snake&& s)
{
std::cout << "Snake(const Snake&& s)" << std::endl;
// 移动构造函数的目前很简单,就是把s这个对象的内存转移给新构造的对象(只有是变量)
m_snakeLen = s.m_snakeLen; // 一下子就转给去了
m_head = s.m_head; // 把s的蛇头结点指针给了m_head。
// 做完转移工作,要把原对象清0
s.m_snakeLen = 0;
s.m_head = nullptr;
// 写完了发现是真简洁
}
4.3.5 =delete =default
上面我写了这么多个构造函数,可能在实际写代码的时候,不需要拷贝构造函数,也不需要移动构造函数,我们贪吃蛇游戏,怎么可能存在两条一摸一样的蛇,所以我们要把拷贝构造函数和移动拷贝函数都需要屏蔽掉,如果在c++98时代,只能通过私有化,private,这样就不能调用了,不过在c++11时代,我们有直接的关键字来删除,就是=delete。
Snake() = delete;
Snake(const Snake& s) = delete; // 没有拷贝构造函数
Snake(const Snake&& s) = delete; // 没有移动构造函数
代码是这样的,没有这么多构造函数,
=defalut,这个关键字的意思就是使用编译器生成的默认构造函数。
Snake() = defalut; // 一般只对默认构造函数有效
4.3.6 初始化列表
上面的例子我都是在构造函数内进行初始化的,其实可以用初始化列表。
在构造函数后面,加上:就可以写初始化函数列表了。
Snake(int snakeLen) : snakeLen(snakeLen) { // 这个就是构造函数
std::cout << "Snake" << std::endl; // 这个调用就是 Snake s;
}
如果我类的成员变量定义成snakeLen,然后构造函数也传参叫snakeLen。这样一赋值是不是都懵逼了。
所以在前面我建议是成员变量都加上m_。
如果真的定义一样的话,只能是用this指针能区别,是类的成员变量还是外部的了。
Snake(int snakeLen) { // 这个就是构造函数
std::cout << "Snake" << std::endl; // 这个调用就是 Snake s;
this->snakeLen = snakeLen;
}
4.3.7 this指针
this指针的理解:可以理解为就是这个对象,我们在用c写面向对象的时候,是不是要把自己的结构体传入,c++其实也是这样的,但是编译器默认已经帮我把这个对象传入到函数里面了,这个对象的指针就叫this指针。这样明白了么?
void Right() const { // 这种写在类定义中的,基本都是编译器搞成内联函数
// 知道啥是内联函数不?内联函数就是当我们调用这个函数的时候,编译器会自动给我们展开在本地,不用走函数调用那个过程
std::cout << "右移" << std::endl;
}
这种const是限制this不能修改的,可能没地方放了,就放在最后了,哈哈哈,多看一会就熟悉了。
4.3.8 运算符重载
c++是直接运算符重载的,运算符有哪些,就是+,-,*,/, =,[]这些操作符,基本很多操作符都可以重载,为啥要支持运算符重载,这个其实是为了简化我们对对象的操作。
我举个例子:
// 重载括号,因为蛇是一条链表,我如果直接取一个结点的数据,可以用数组下标来取
// a[1] = 22; // 我们数组返回是可以做左值的,所以返回值是一个左值
SnakeNode operator[] (int i) { // operator是固定,后面加重载的运算符 ()是函数的括号
// 第一:肯定要做好边界处理
SnakeNode s(0, 0); // 申请一个结构
if (i > m_snakeLen)
{
std::cout << "operator[] " << std::endl;
return s;
}
auto pNode = m_head->Next;
int len = i;
int j = 0;
for (; j < len && pNode; j++)
{
pNode = pNode->Next;
}
if (j == i)
{
return *pNode;
}
return s;
}
// []重载
std::cout << s5[0].m_x << s5[0].m_y << std::endl;
std::cout << s5[1].m_x << s5[1].m_y << std::endl;
std::cout << s5[2].m_x << s5[2].m_y << std::endl;
// 输出结果:
31
21
11
直接重载了操作符之后,我们就可以直接使用下标来取到这个结点了,是不是很方便。
4.3.9 拷贝赋值函数
既然可以重载运算符,那我们等号的运算符是不是也要注意一下,其实主要我们类中有指针,拷贝构造函数和拷贝赋值函数都需要重写。
我们来重写一下
// 拷贝赋值函数
const Snake& operator=(const Snake& s) {
std::cout << "const Snake& operator=(const Snake& s)" << std::endl;
// 重写拷贝赋值函数有三步,一定要记住了,不要搞错了
// 第一步:判断是不是自己赋值给自己,不要搞乌龙了
if (&s == this)
{
return s;
}
// 第二步:如果该对象已经为指针分配了内存,要释放
if (m_head)
{
// 我们这个是链表,要全部释放,尴尬了
auto node = m_head; // 因为我们这个是链表,所以要循环delete
while (node)
{
// 把next指针先保存好
auto next = m_head->Next;
delete node; // 删除node
node = next; // next就是下一个node,一直循环删除
}
}
// 第三步:重新申请内存,并且用传过来的对象赋值
m_head = new SnakeNode(0, 0); // 申请一个空结点
auto pPrev = m_head;
auto pTargetNode = s.m_head; // 目标蛇的头结点
for (int i = 0; i < s.m_snakeLen && pTargetNode; i++) // 按照对方蛇的长度进行初始化蛇
{
pTargetNode = pTargetNode->Next; // 取出第一个节点,因为m_head指向的第一个节点时空结点
auto node = new SnakeNode(pTargetNode->m_x, pTargetNode->m_y); // 以另一条蛇的结点,创建一台新蛇
pPrev->Next = node;
node->Prev = pPrev;
pPrev = node;
m_snakeLen++;
//m_tail = node;
}
}
// 拷贝赋值函数
Snake s6(4, 1, 3);
s6 = s5; // 这种就是调用了拷贝赋值函数
std::cout << s6[0].m_x << s6[0].m_y << std::endl;
std::cout << s6[1].m_x << s6[1].m_y << std::endl;
std::cout << s6[2].m_x << s6[2].m_y << std::endl;
没有重写的话,因为编译器使用的是浅拷贝,只是把指针拷贝过去,所以在释放的时候会崩溃。
可以试试使用编译器的拷贝赋值函数,看有没有崩溃。
4.3.10 移动赋值函数
由于c++11引入了移动构造函数,所以这里我们也需要提一提移动赋值函数,移动赋值函数其实也是把这个对象的内存,移动给另一个对象,跟移动构造函数差不多:
// 移动赋值函数
Snake& operator=(Snake&& s) noexcept // 这种函数默认不抛异常
{
std::cout << "Snake& operator=(Snake&& s)" << std::endl;
// 写法其实跟拷贝赋值差不多
// 第一步:先判断
if (&s == this)
{
return s;
}
// 第二步:也是释放指针
if (m_head)
{
// 我们这个是链表,要全部释放,尴尬了
auto node = m_head; // 因为我们这个是链表,所以要循环delete
while (node)
{
// 把next指针先保存好
auto next = m_head->Next;
delete node; // 删除node
node = next; // next就是下一个node,一直循环删除
}
}
// 第三步:就不一样了,把s的指针和值都移动过来
m_head = s.m_head;
m_snakeLen = s.m_snakeLen;
// 第四步:还有最后一步,清除
s.m_head = nullptr;
s.m_snakeLen = 0;
}
// 移动赋值函数
Snake s7(5, 1, 3);
s7 = std::move(s6);
// 这以后s6就不能使用了,我们在代码也写的好好的,把s6都给清除了
std::cout << s7[0].m_x << s7[0].m_y << std::endl;
std::cout << s7[1].m_x << s7[1].m_y << std::endl;
std::cout << s7[2].m_x << s7[2].m_y << std::endl;
4.4 友元
4.4.1 友元函数
上面介绍了这么多,是不是还没介绍怎么打印类,学过Java的应该知道,主要重写Java的tostring方法,我们打印这个类的时候,就会输出tostring方法的打印。
c++没有一个tostring方法,但是上帝总要打开一扇窗,不是么,c++实现类的打印,需要使用到友元函数。
啥是友元函数,我们先来看看c++平时是怎么打印的:
cout << s << endl;
这样打印的,cout是std的实现函数,所以我们需要重载<<这个操作符,那我们总不能直接在std中再次重载,所以c++实现了一种可以在类外写一个重载cout的操作符。
std::ostream& operator<<(std::ostream& out, Snake& c1) // 大概就长这样
{
// osstream就是std封装的输出类,第一个参数out 就是类外重载cout的这种写法,
// 第二个参数就是我们需要的真正参数,我们这里需要打印Snake,所以就要把这个传入
std::cout << "snakeLen: " << c1.m_snakeLen << std::endl;
int i = 0;
auto pNode = c1.m_head;
while (pNode)
{
std::cout << "node: " << i << "node.x:" << pNode->m_x << "node.y: " << pNode->m_y << " " << pNode << " " << pNode->Next << " " << pNode->Prev << std::endl;
i++;
pNode = pNode->Next;
}
return out;
}
具体实现是这样,但是编译器不给过啊,因为c1访问了类中的私有变量,总不能给每个私有变量添加函数,在由函数访问,这个不是不行,主要是比较累,干脆就加个友元函数就好,友元函数就是这个函数是类的朋友,可以访问类的私有变量。
friend std::ostream& operator<<(std::ostream& out, Snake& c1);
所有我们需要在类中这么写的就可以,就把这个函数声明成Snake类的友元函数,这么写之后,编译器就不报错了。
我们接着来打印一下看看:
// 友元函数
std::cout << s7 << std::endl;
/* snakeLen: 3
node: 0node.x : 0node.y : 0 000001D0422BF590 000001D0422BF7D0 0000000000000000
node : 1node.x : 3node.y : 1 000001D0422BF7D0 000001D0422BF890 000001D0422BF590
node : 2node.x : 2node.y : 1 000001D0422BF890 000001D0422BFB90 000001D0422BF7D0
node : 3node.x : 1node.y : 1 000001D0422BFB90 0000000000000000 000001D0422BF890
*/
这样的打印,是不是比我们手工打印好了,所以说不要着急,慢慢的都会有的。
4.4.2 友元类
友元类是两个类之间的关系,
class B
{
void test(A& a)
{
std::cout << a.m_a << std::endl; // 这个是正确的
}
private:
int m_b;
};
class A
{
public:
friend class B; // 类B是类A的友元类,所以类B可以访问类A的私有成员
void test(B& b)
{
std::cout << b.m_b << std::endl; // 这个报错
}
private:
int m_a;
};
这种比较少用,就当做知道这个语法就行。
4.4.3 友元成员函数
class A;
class B
{
public:
void test(A& a);
private:
int m_b;
};
class A
{
public:
//friend class B;
// 友元成员函数
friend void B::test(A& a);
void test2(B& b)
{
//std::cout << b.m_b << std::endl;
}
private:
int m_a;
};
void B::test(A& a)
{
std::cout << a.m_a << std::endl;
}
友元成员函数,B::test是类A的友元成员函数,所以可以访问类A私有成员,但是这个好像写要写在外面才能编译成功。
4.5 食物+蛇的实现
4.5.1 食物代码
食物的实现:
#pragma once
#include <iostream>
// typedef char(*GetCharFunc)(int, int);
struct Food
{
Food(const int Height, const int Width)
{
m_height = Height;
m_width = Width;
// m_getChar = getChar;
srand(time(0));
}
int x;
int y;
// GetCharFunc m_getChar;
int m_height;
int m_width;
void GetFood()
{
//do
{
x = (rand() % (m_width - 2)) + 1; // 不要随机到两条边上
y = (rand() % (m_height - 2)) + 1;
std::cout << x << y << std::endl;
} // while (m_getChar(x, y) != ' '); // 有可能生成在蛇身上
}
};
食物类的使用:
// 初始化食物类
Food f(height, width);
// 获取食物
f.GetFood();
win[f.y][f.x] = '$';
4.5.2 蛇代码实现
gitee地址:https://gitee.com/jiangyoushixiong/linux_cpp_study2/blob/master/4.client/04.1%20%20%E8%B4%AA%E5%90%83%E8%9B%87%E5%AE%9E%E7%8E%B0/Snake.cpp
https://gitee.com/jiangyoushixiong/linux_cpp_study2/blob/master/4.client/04.1%20%20%E8%B4%AA%E5%90%83%E8%9B%87%E5%AE%9E%E7%8E%B0/Snake.h
蛇的初始化:
Snake s(3, height / 2, 3);
// 调用
// Snake实现
showSnake(win, s);
s.Right();
目前代码看上去蛇不能自动动,需要我们按一下按键,这是因为我们用按键阻塞了,不过后面可以优化,还有食物还没有做去重处理,这个也是后面再优化,好了,今天就到这里了。