四、c++学习(类的简介)

news/2024/5/19 1:30:13 标签: c++, 学习, 链表, 类简介, 构造函数

上一篇我们做了一个简单的界面优化,并且我们可以选择进入游戏界面,所以这一篇我们来实现贪吃蛇和食物。

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();

目前代码看上去蛇不能自动动,需要我们按一下按键,这是因为我们用按键阻塞了,不过后面可以优化,还有食物还没有做去重处理,这个也是后面再优化,好了,今天就到这里了。


http://www.niftyadmin.cn/n/321696.html

相关文章

C语言面试经典笔试题分析、实践,BAT面试笔试题精讲

C语言面试 考点关键词关键字&#xff1a;static、局部变量、全局变量1、static全局变量与普通全局变量、static局部变量和普通局部变量、static函数与普通函数有什么区别&#xff1f;2、全局变量可不可以定义在可被多个.c文件包含的头文件中&#xff1f;为什么&#xff1f;3、局…

人类与ChatGPT:互动中的共同进步

一、ChatGPT的发展历程 1. GPT模型 ChatGPT是由OpenAI推出的一款聊天机器人&#xff0c;其核心技术基于GPT模型。GPT模型&#xff08;Generative Pre-training Transformer&#xff09;是一种基于Transformer结构的预训练语言模型。它在大规模的文本语料库上进行无监督的预训…

神经网络的训练过程、常见的训练算法、如何避免过拟合

神经网络的训练是深度学习中的核心问题之一。神经网络的训练过程是指通过输入训练数据&#xff0c;不断调整神经网络的参数&#xff0c;使其输出结果更加接近于实际值的过程。本文将介绍神经网络的训练过程、常见的训练算法以及如何避免过拟合等问题。 神经网络的训练过程 神…

多重网格算法求解二维泊松方程的python实现

问题描述 二维泊松方程 { − Δ u = f , in Ω , u =

【AI面试】RoI Pooling 和 RoI Align 辨析

RoI Pooling和RoI Align是两种常用的目标检测中的RoI特征提取方法。它们的主要区别在于&#xff1a;如何将不同大小的RoI对齐到固定大小的特征图上&#xff0c;并在这个过程中保留更多的空间信息。 一、RoI Pooling RoI Pooling最早是在Fast R-CNN中提出的&#xff0c;它的基…

linux 内核开启调试选项

前言 嵌入式 linux 经常要编译 linux 内核&#xff0c;默认情况下编译出的内核镜像是不带调试信息的&#xff0c;这样&#xff0c;当内核 crash 打印 PC 指针和堆栈信息时&#xff0c;我们需要反汇编来确认出错位置&#xff0c;不直观。 如果内核开启了调试选项&#xff0c;我…

MySQL MHA

概述 什么是 MHA MHA&#xff08;Master High Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出现就是解决MySQL 单点的问题。 MySQL故障切换过程中&#xff0c;MHA能做到0-30秒内自动完成故障切换操作。 MHA能在故障切换的过程中…

50 Projects 50 Days - Blurry Loading 学习记录

项目地址 Blurry Loading 展示效果 Blurry Loading 实现思路 元素组成只需要有一张图片和中间的文本即可。针对动态过程分析初始和终止状态即可&#xff0c;初始时图片全模糊&#xff0c;文本显示0%&#xff1b;终止时&#xff0c;图片完全不模糊&#xff0c;文本会显示100…