当前位置: 首页 > news >正文

贪吃蛇超精讲(C语言)

前言

        如果你还是个萌新小白,那么该项目的攻克过程一定会十分艰难。虽然作者已经将文章尽可能写的逻辑清晰,内容详细。但所谓“纸上得来终觉浅”,在讲到陌生结构和函数时,大家请一定自己动手去敲一遍代码,这很重要!!!如果遇到问题,欢迎评论区提问和讨论。制作不易,不求三连,只求一个免费的赞。

游戏介绍

        本文讲解的是经典贪吃蛇游戏,玩家需要控制一条蛇在一个有边界的区域内移动,并吃掉尽可能多的食物。蛇的身体会不断地变长,当蛇头碰到自己的身体或者碰到边界时,游戏结束。这款经典小游戏,相信大家或多或少都玩过。在这里,我就不再赘述它的玩法和规则了。那么我们现在开始上强度,大家速度上车!

准备工作

    1.控制台程序的更改

       该代码的运行需要使用控制台程序!如果你使用的是终端,请按照如下步骤更改:

        (1)点击红色框住的箭头标志

        (2)点击设置

        (3)更改为控制台主机即可(不要忘了点保存)

主要思路

   

学习新知

                                            -学前必看-

        这一部分放在了代码详解前呢,是希望大家先学会陌生知识,再去看代码中的应用。但本人比较推荐先看代码详解,遇到不会的再来这里学习。这样既不会突兀,又避免花了许久功夫好不容易把这块知识学完,结果看代码时却忘的差不多了。

1.控制台程序的基本设定

        该步骤我们将设定控制台的窗口大小和名称,代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{//设置控制台相关属性system("mode con cols=100 lines=30");//设置控制台窗口大小 列、行system("title 贪吃蛇");//设置控制台名称(程序结束前有效)system("pause");//作用即“按任意键继续……”,防止程序结束名称初始化return 0;
}

        运行结果如下:

2.坐标的概念及COORD结构的认识

    (1)坐标的概念

        在终端和控制台输出时,不同的位置是有与之对应的坐标的。如果你没有专门设置输出的位置,那么系统就是默认从(0,0)开始输出。换行即y轴+1,空格即x轴+1。如图所示:

         值得一提的是,这个坐标轴并不像大多人印象中的坐标轴一样。这个坐标轴上,X轴上的两个单位长度等于Y轴上的一个单位长度。如下图:

        那么肯定有人发问:汉字正正方方的不像英文字母,这种长方形的格子怎么放得下?是的,一个格子确实放不下,系统是用两个格子来放汉字以及一些特殊符号的,例如“☆”等。因此,我们也称这种字符为“宽字符”(记住这个概念很重要,我们后面要用到,需要更清楚解释的请前往疑难解答)。

     (2)COORD结构的介绍

        COORD是Windows API中定义的一种结构,表示一个字符在控制台屏幕上的坐标。

        定义如下:

        现在记住即可,因为例子还要用到后文的一些知识,我们等一下再进行举例。

3.光标的设置

    (1)句柄(HANDLE)的概念

        句柄是一种抽象的标识符,用于在操作系统中引用或操作特定的对象或资源。

        在该代码片段中,句柄被实现为void*类型的指针,用来。关于句柄的详细介绍由于篇幅过长,我们不放在思路处进行介绍。想了解的同学请前往后文的“疑难解答”处了解详情。

    (2)GetStdHandle 函数

   GetStdHandle 是一个 Windows API 函数,用于获取指定标准设备(标准输入、标准输出或标准错误)的句柄。

        是不是看到了“_in_”有点懵?这是标注函数参数的作用或方向的标识符,我放在疑难解答咯!

    (3)SetConsoleCursorPosition 函数

  SetConsoleCursorPosition 函数用于在指定的控制台屏幕缓冲区中设置光标位置。

        当我们想将光标定位到某位置进行输出时,我们将标准输出的句柄坐标(COORD结构)进行传参即可改变光标的位置。 

        现在我们将运用SetConsoleCursorPosition函数和COORD结构来实现精准位置输出,例子如下: 

int main()
{//获得标准输出设备的句柄HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定位光标的位置COORD pos = { 10,10 };SetConsoleCursorPosition(houtput, pos);system("pause");return 0;
}

         运行结果如下图:

        (4)CONSOLE_CURSOR_INFO 结构

        CONSOLE_CURSOR_INFO 是一个结构体,用于描述控制台光标的信息,包括光标的大小和可见性等。

        该函数很好理解。第一个参数为光标在一个坐标格的占比,第二个参数为光标的可见性(‘0’或“false”代表不可见,‘1’或“ture”代表可见)。

        这里也请先不要着急,我们后面会进行举例。

        (5)GetConsoleCursorInfo 函数

 SetConsoleCursorInfo 函数用于设置指定控制台屏幕缓冲区的光标的大小和可见性。

         该函数与SetConsoleCursorPosition函数十分相似,都是将HANDLE作为第一个参数,将对应的结构体作为第二个参数。

        下面我们举个例子:

int main()
{//获取标准输出设备的句柄HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定义一个光标信息的结构体CONSOLE_CURSOR_INFO cursor_info = { 0 };//获取和houtput句柄相关的控制台上的光标信息,存放在cursor_info中GetConsoleCursorInfo(houtput, &cursor_info);//修改光标占比cursor_info.dwSize = 100;//修改光标可见度cursor_info.bVisible = 0;//设置和houtput句柄相关的控制台上的光标信息SetConsoleCursorInfo(houtput, &cursor_info);system("pause");return 0;
} 

        运行结果如下 :

        没错,这里的光标没有了(是真的没有了,不是我正好截到了光标闪烁消失的一帧)。光标大小的代码这里不再进行演示,大家请自行尝试。

4.地图的打印

         (1)setlocale函数

    setlocale函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。

        setlocale的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。

        C标准给第二个参数仅定义了2种可能取值:“C”(正常模式)和“”(本地模式)。 

         在任意程序执行开始,都会隐藏式执行调用:

        setlocale(LA_ALL,"C");

        当地区设置为“C”时,设置为C语言默认的语言模式,这时库函数按正常方式执行。

        当程序运行起来后想改变地区,就只能显示调用Setlocale函数,用“”作为第二个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地程序。

        比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。

        setlocale(LC_ALL,"");

        setlocale的返回值是一个字符串指针,表示已经设置好的格式 。如果调用失败,则返回NULL。

        setlocale还可以用来查询当前地区,此时第二个参数设为NULL即可。代码如下:

int main()
{char* ret = setlocale(LC_ALL, NULL);printf("%s\n", ret);ret = setlocale(LC_ALL, "");printf("%s\n", ret);return 0;
}

        运行结果如下图:

     (2)宽字符的打印方式

      文件中,我们会用到一些特殊符号的打印,这时候我们便需要用到宽字符的知识了。否则可能会无法正常输出,我们拿图说话:

          所以还是有必要学一下的(虽然可以替换成普通字符,但不太好看不是?)。我们的变量名由char换为wchar_t;printf换为wprintf;且在单、双引号前应加上‘L’也因此我们每次wprintf函数在Chinese (Simplified)_China.936环境时只能输出一个字符,其他环境这里就不补充了。感兴趣的同学……呃~应该也不会有吧)。

        老规矩,例子来一个:

int main()
{setlocale(LC_ALL, "");wchar_t a = L'求';wchar_t b = L'个';wchar_t c = L'三';wchar_t d = L'连';wchar_t x = L'☆';wprintf(L"%lc\n", x);wprintf(L"%lc\n", a);wprintf(L"%lc\n", b);wprintf(L"%lc\n", c);wprintf(L"%lc\n", d);wprintf(L"%lc\n", x);return 0;
}

        运行结果如下:

(3)用Rand函数和时间戳生成随机数

        这个内容比较简单,这里不花费大篇幅讲解。此内容在我之前的文章有讲,链接放在下面了:C语言扫雷(极简版精讲)_扫雷csdn-CSDN博客 

5.蛇的移动

         (1)GetAsyncKeyState函数

        用于获取按键情况,GetAsyncKeyState函数原型如下:

SHORT GetAsyncKeyState(int vKey);

        将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。GetAsyncKeyState函数的返回值是short类型,在上一次调用 GetAsyncKeystate函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果果最高位是0,说明按键的状态是抬起;

        GetAsyncKeyState 函数返回的 short 类型数字的最低位表示自该函数上一次被调用后,对应的键是否被按下过。如果最低位为 1,则表示该键在这两次函数调用之间被按过;如果最低位为 0,则表示该键在此期间未被按过。

        如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。

       那么传参的vkey又是什么意思呢?这是每个键对应的16进制编号,传入的数字不同,表示检测的按键不同。编号与按键的对应表我放在了下面:

        参考:虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn

        老规矩,举个例子:

#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)  
// 定义一个宏,用于检查指定键的状态,通过判断GetAsyncKeyState(vk)的最低位int main()
{while (1)  // 无限循环{if (KEY_PRESS(0x30))  // 如果数字 0 键的状态符合条件(通过宏判断){printf("0\n");  // 输出 0}else if (KEY_PRESS(0x31))  // 否则如果数字 1 键的状态符合条件{printf("1\n");  // 输出 1}else if (KEY_PRESS(0x32))  // 依此类推,检查其他数字键{printf("2\n");}else if (KEY_PRESS(0x33)){printf("3\n");}else if (KEY_PRESS(0x34)){printf("4\n");}else if (KEY_PRESS(0x35)){printf("5\n");}else if (KEY_PRESS(0x36)){printf("6\n");}else if (KEY_PRESS(0x37)){printf("7\n");}else if (KEY_PRESS(0x38)){printf("8\n");}else if (KEY_PRESS(0x39)){printf("9\n");}}return 0;
}

代码详解

       ●流程图

     1.贪吃蛇结构体的创建

        对于贪吃蛇这个结构体,其中包含了许多信息。有些信息比较复杂,例如:蛇头、贪吃蛇状态等。我们先学习贪吃蛇结构体中这些比较复杂的成分,再来创建贪吃蛇结构体。

        (1)创建贪吃蛇蛇身节点

        我们使用链表来维护贪吃蛇的身体。链表的数据域装的是蛇身的坐标,指针域则是下一节点的地址。

typedef struct SnakeNode
{//坐标int x;int y;//指向下一节点的指针struct SnakeNode* next;
}SnakeNode, pSnakeNode;

        (2)贪吃蛇的方向

        这段代码枚举了上、下、左、右四个方向,包含了贪吃蛇运行时可能行走的所有方向。

enum DIRECTION//enum为枚举类型
{UP = 1,DOWN = 2,LEFT = 3,RIGHT = 4
};

        (3)贪吃蛇的状态

        这段带码枚举出了贪吃蛇运行时可能出现的四种状态分别为正常、撞墙、撞到自己、正常退出。

//正常、撞墙、撞到自己、正常退出
enum GAME_STATUS
{OK,//正常KILL_BY_WALL,//撞墙KILL_BY_SELF,//撞到自己END_NORMAL//正常退出
};

     (4)贪吃蛇的创建

        贪吃蛇架构提的创建在这一步就算是真正完成了,这段代码中有我们创建一条贪吃蛇所必备的信息,详细注释以为大家写好,大家自行浏览。

//贪吃蛇
typedef struct Snake
{pSnakeNode pSnake;//指向蛇头的指针pSnakeNode pFood;//指向食物节点的指针enum DIRECTION dir;//蛇的方向enum GAME_STATUS status;//游戏的状态int food_weight;//一个食物的分数int score;//总成绩int sleep_time;//休息时间、时间越短、速度越快、时间长、速度快 
}Snake;

2.游戏开始-GameStart函数

      (1)欢迎界面的打印

        实现出的效果图如下:

       ①窗口大小、窗口名称及隐藏光标的操作

        通过前文学习,这里应该不难读懂,我将源码放在下面,有问题评论区见。

// 设置窗口大小,并隐藏光标
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
// 隐藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo); // 获取控制台光标信息
CursorInfo.bVisible = false; // 隐藏控制台光标
SetConsoleCursorInfo(houtput, &CursorInfo); // 设置控制台光标状态
       ②自定义定位光标函数:SetPos函数

    相信大家对于printf这一函数已经可以轻松拿捏了。这里的难点在于如何运用SetConsoleCursorPosition 函数定位光标后进行输出。由于每次都去获得句柄、调用函数比较麻烦,我们将其封装成了一个定位光标函数。

         该函数很好理解,其代码本质就是在学习SetConsoleCursorPosition函数时举的例子。源码如下:

void SetPos(short x,short y)
{//获得标准输出设备的句柄HANDLE houtput = NULL;houtput = getstdhandle(STD_OUTPUT_HANDLE);//定位光标的位置COORD pos = { x,y };SetConsoleCursorPosition(houtput, pos);
}

        下面的代码是运用该函数进行定位光标的实操:

        我们封装了一个WelcomeToGame函数,用于输出以上两个界面的内容。

void WelcomeToGame()
{// 设置光标位置,并打印欢迎信息SetPos(40, 14);printf("欢迎来到贪吃蛇小游戏\n");// 暂停程序,等待用户按键SetPos(42, 20);system("pause");// 清屏system("cls");// 设置光标位置,打印操作说明SetPos(25, 14);printf("用 ↑. ↓. ←. → 来控制蛇的移动,按 F3 加速,F4 减速\n");SetPos(25, 15);printf("加速能够得到更高的分数\n");// 暂停程序,等待用户按键SetPos(42, 20);system("pause");// 清屏system("cls");
}

      (2)游戏地图边界的打印

        这里如果wprintf函数的运用和SetPos函数的运用好像也没有什么需要讲的,有问题的话评论区见。

#define WALL L'□'
void CreateMap()
{// 打印上边界int i = 0;for (i = 0; i < 29; i++){wprintf(L"%lc", WALL);}// 打印左边界for (i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}// 打印右边界for (i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}// 打印下边界SetPos(0, 26);for (i = 0; i < 29; i++){wprintf(L"%lc", WALL);}
}

        运行结果如下图:

     (3)初始化蛇身

        蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上蛇的初始位置从(24,5)开始。
        再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个食物的分数。

  •   游戏状态是:OK
  •   蛇的移动速度:200毫秒
  •   蛇的默认方向:RIGHT
  •   初始成绩:0
  •   每个食物的分数:10
// 初始化贪吃蛇的函数
void InitSnake(pSnake ps)
{int i = 0;pSnakeNode cur = NULL;for (i = 0; i < 5; i++){// 动态分配内存创建蛇身节点cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){// 打印内存分配失败的错误信息perror("InitSnake()::malloc()");return;}cur->next = NULL;cur->x = POS_X + 2 * i;cur->y = POS_Y;// 头插法插入链表if (ps->_pSnake == NULL) // 空链表{ps->_pSnake = cur;}else // 非空{cur->next = ps->_pSnake;ps->_pSnake = cur;}}cur = ps->_pSnake;while (cur){// 设置光标位置,打印蛇身SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}// 设置贪吃蛇的属性ps->_dir = RIGHT; // 默认向右ps->_score = 0;ps->_food_weight = 10;ps->_sleep_time = 200; // 单位是毫秒ps->_status = OK;
}

(4)创建食物

        我们的代码有以下5个功能,大家注意去对应以下,代码就会变得好理解许多。详细注释已经标注,有问题欢迎向作者提问。

  1.    先随机生成食物的坐标
  2.    坐标必须是2的倍数(否则检查时坐标不容易对上)
  3.    食物的坐标要在墙体内部
  4.    食物的坐标不能和蛇身每个节点的坐标重复
  5.    创建食物节点,打印食物

        食物打印的宽字符:

#define FOOD L"★"

        创建食物的函数:CreatFood

// 函数:创建食物
void CreatFood(pSnake ps)
{int x = 0;int y = 0;// 生成 x 坐标为 2 的倍数// x:2~54// y: 1~25
again:do{x = rand() % 53 + 2;  // 随机生成 x 坐标,范围是 2 到 54y = rand() % 25 + 1;  // 随机生成 y 坐标,范围是 1 到 25} while (x % 2 != 0);  // 如果 x 不是 2 的倍数,重新生成// 确保食物的位置不与蛇身重合// 遍历蛇身节点pSnakeNode cur = ps->_pSnake;  // 创建一个指针 cur 并初始化为蛇头节点// 只要 cur 不为空,说明还有未检查的蛇身节点while (cur)//蛇身遍历,只要 cur 不为 NULL ,循环就会继续执行{if (x == cur->x && y == cur->y)  // 如果食物的坐标与当前蛇身节点坐标重合{// 若重合,重新生成食物位置goto again;  // 跳转到 again 标签处重新生成}cur = cur->next;  // 将 cur 指针指向下一个蛇身节点,继续检查}// 创建食物的节点pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pFood == NULL){// 打印内存分配失败的错误信息perror("CreateFood()::malloc()");return;}pFood->x = x;pFood->y = y;pFood->next = NULL;// 设置食物的位置并打印SetPos(x, y);wprintf(L"%lc", FOOD);ps->_pFood = pFood;
}

 3.游戏运行-GameRun函数

        (1)打印帮助信息

        很简单,就是定位函数+输出,直接上代码:

// 打印帮助信息的函数
void PrintHelpInfo()
{SetPos(64, 14);wprintf(L"%ls", L"不能穿墙,不能咬到自己");SetPos(64, 15);wprintf(L"%ls", L"用 ↑. ↓. ←. → 来控制蛇的移动");SetPos(64, 16);wprintf(L"%ls", L"按 F3 加速,F4 减速");SetPos(64, 17);wprintf(L"%ls", L"按 ESC 退出游戏,按空格暂停游戏");SetPos(64, 18);wprintf(L"%ls", L"CSDN博主:Bit_Le");
}

(2)打印当前已获得分数和每个食物的分数

        同上,直接拿下。上代码:

//打印总分数和食物的分值
SetPos(64, 10);
printf("总分数%d\n",ps->_score);
SetPos(64, 11);
printf("当前食物的分数%d\n", ps->_food_weight);

(3)获取按键情况KEY_PRESS

      ①整体思路的概述

        这一部分就难了,先来第一步,把GetAsyncKeyState函数中演示代码的宏给拿来用:

#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)  

        这里有点难懂,感到懵圈的同学去上面再学习一下,有问题随时提问。

        下面接着上强度,这里先给大家讲清楚大致思路,方便大家有一个大体方向:

        游戏运行期间,右侧打印帮助信息,提示玩家,坐标开始位置(64,15)根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。需要的虚拟按键的罗列:

  •    上:VK_UP
  •    下:VK_DOWN
  •    左:VK_LEFT
  •    VK_RIGHT
  •    空格:VK_SPACE
  •    ESC:VK_ESCAPE
  •    F3:VK_F3
  •    F4:VK_F4
       ②对于方向键进行响应的代码

        这里需要注意一点,就是当蛇向下走的时候,你按上键是没有响应的。玩过的都知道,我们总不能让贪吃蛇折叠不是?同理,其他方向亦然:

        这是对于方向键进行响应的代码:

// 根据按键改变蛇的移动方向
if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)
{ps->_dir = UP;
}
else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)
{ps->_dir = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)
{ps->_dir = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)
{ps->_dir = RIGHT;
}
      ③对于暂停键的响应

        我们是通过Sleep函数实现游戏暂停的,当按下暂停通过死循环Sleep函数使代码停止运行。再次按下,break退出循环。代码如下:

void Pause()
{while (1){Sleep(200);// 检测空格是否按下,若按下则退出循环if (KEY_PRESS(VK_SPACE)){break;}}
}
      ④退出游戏

        状态改一下就行,状态不是OK游戏就不再运行。

else if (KEY_PRESS(VK_ESCAPE))
{// 正常退出游戏ps->_status = END_NORMAL;
}
     ⑤加速减速的实现

        先来讲一下我们是如何控制速度的。如果没有Sleep函数,贪吃蛇以默认向右的方向一直走,以现在电脑的算力,刚打开就撞墙噶了。我们的运用Sleep函数呢,就是使代码在进行到Sleep函数是会停顿,停顿的时间即为函数的参数(单位为毫秒)。我们初始设置Sleep时间为200ms,也就是当蛇每隔0.2s前进一格。加速就是减少休眠时间,减速就是增加休眠时间。不过这个也是有限度的,如果我们一直减或增肯定会出bug。那我们就设置一个区间,这里我设置的为加减不超过80ms。下面上代码:

else if (KEY_PRESS(VK_F3))
{// 加速if (ps->_sleep_time > 80){ps->_sleep_time -= 30;ps->_food_weight += 2;}
}
else if (KEY_PRESS(VK_F4))
{// 减速if (ps->_food_weight > 2){ps->_sleep_time += 30;ps->_food_weight -= 2;}
}

        仔细看代码的同学应该已经看出来了,速度越快,一个食物的分值就越高。毕竟“风浪越大鱼越贵”嘛。 

        如此呢,我们就实现了所有功能按键的响应,此小节告终!

(4)贪吃蛇的移动-SnakeMove函数

        我们这一节呢,先去预算出贪吃蛇下一步要走的位置的坐标,然后判断该位置是否有食物。对于有无食物的结果进行不同的操作。

      ①对下一步的预测

        我们先根据方向状态预测下一步的坐标,然后进行后续判断。

switch (ps->_dir){case UP:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1;break;case DOWN:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1;break;case LEFT:pNextNode->x = ps->_pSnake->x - 2;pNextNode->y = ps->_pSnake->y;break;case RIGHT:pNextNode->x = ps->_pSnake->x + 2;pNextNode->y = ps->_pSnake->y;break;}
      ②有食物

        我们食物的类型和蛇身节点类型一样。因此我们传参将即将到达位置的参数和蛇头的参数传进去,将下一位置节点通过头插法插入到蛇身链表中,再将头节点的指针设置为空即可。我们此时蛇的长度增加,重新打印一下即可。不要忘了加分并重新创建食物哈。

void EatFood(pSnakeNode pn, pSnake ps)
{// 头插法将食物节点插入蛇身链表ps->_pFood->next = ps->_pSnake;ps->_pSnake = ps->_pFood;// 释放之前分配给节点 pn(食物)的内存空间free(pn);pn = NULL;pSnakeNode cur = ps->_pSnake;// 重新打印蛇身while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}// 增加分数ps->_score += ps->_food_weight;// 重新创建食物CreatFood(ps);
}
      ③无食物

        和有食物的操作差不多,不同点如下:

        我们将最后一个节点打印成空格,否则不覆盖直接释放节点会在地图留下一个蛇身图案。

        释放最后一个蛇身的节点,将倒数第二个置为空即可。

        链表示意图如下,左边为尾,右边为头。

// 未吃到食物的处理函数
void NoFood(pSnakeNode pn, pSnake ps)
{// 头插法将新节点插入蛇身链表pn->next = ps->_pSnake;//新节点指向头ps->_pSnake = pn;//头改为新节点pSnakeNode cur = ps->_pSnake;while (cur->next->next != NULL){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}// 把最后一个结点打印成空格SetPos(cur->next->x, cur->next->y);printf("  ");// 释放最后一个结点free(cur->next);// 把倒数第二个节点的地址置为 NULLcur->next = NULL;
}
        ④判断下一节点有无食物

        就是检测一下我们预测的下一节点是否与食物重合。话不多说,上代码:

// 判断下一个位置是否为食物的函数
int NextIsFood(pSnakeNode pn, pSnake ps)
{// 更简洁的方式,直接返回比较结果return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
       ⑤判断下一节点是否为墙

        就是检测蛇头的坐标是否有和墙重合的部分,直接上代码:

// 检测蛇是否撞墙的函数
void KillByWall(pSnake ps)
{// 判断蛇头是否撞到边界if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 ||ps->_pSnake->y == 0 || ps->_pSnake->y == 26){ps->_status = KILL_BY_WALL;}
}
       ⑥判断下一节点是否为蛇身

        就是遍历一遍蛇身,看看蛇头与蛇身是否重合。

// 检测蛇是否咬到自己的函数
void KillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;while (cur){// 判断蛇头是否与蛇身重合if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y){ps->_status = KILL_BY_SELF;break;}cur = cur->next;}
}
      ⑦SnakeMove函数源码
// 蛇移动的函数
void SnakeMove(pSnake ps)
{// 创建一个新节点表示蛇即将到达的下一个位置pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNextNode == NULL){// 打印内存分配失败的错误信息perror("SnakeMove()::malloc()");return;}switch (ps->_dir){case UP:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1;break;case DOWN:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1;break;case LEFT:pNextNode->x = ps->_pSnake->x - 2;pNextNode->y = ps->_pSnake->y;break;case RIGHT:pNextNode->x = ps->_pSnake->x + 2;pNextNode->y = ps->_pSnake->y;break;}// 检测下一个位置是否为食物if (NextIsFood(pNextNode, ps)){EatFood(pNextNode, ps);}else{NoFood(pNextNode, ps);}// 检测蛇是否撞墙KillByWall(ps);// 检测蛇是否咬到自己KillBySelf(ps);
}

 (5)GameRun函数源码

        介绍完上面重要的部分,GameRun函数基本就介绍完了。只要贪吃蛇状态为OK就一直循环,每次循环时间间隔为一次_sleep_time。

// 游戏运行的主逻辑函数
void GameRun(pSnake ps)
{// 打印帮助信息PrintHelpInfo();do{// 打印总分数和食物的分值SetPos(64, 10);printf("总分数:%d\n", ps->_score);SetPos(64, 11);printf("当前食物的分数:%2d\n", ps->_food_weight);// 根据按键改变蛇的移动方向if (KEY_PRESS(VK_UP) && ps->_dir != DOWN){ps->_dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP){ps->_dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT){ps->_dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT){ps->_dir = RIGHT;}else if (KEY_PRESS(VK_SPACE)){Pause();}else if (KEY_PRESS(VK_ESCAPE)){// 正常退出游戏ps->_status = END_NORMAL;}else if (KEY_PRESS(VK_F3)){// 加速if (ps->_sleep_time > 80){ps->_sleep_time -= 30;ps->_food_weight += 2;}}else if (KEY_PRESS(VK_F4)){// 减速if (ps->_food_weight > 2){ps->_sleep_time += 30;ps->_food_weight -= 2;}}SnakeMove(ps); // 蛇移动一步Sleep(ps->_sleep_time);} while (ps->_status == OK);
}

4. 游戏结束-GameEnd函数

        该代码的作用是在游戏结束后打印死亡信息,并清空所有蛇身节点。相信对于看完前面所有代码的你不难,我们直接上代码:

void GameEnd(pSnake ps)
{SetPos(24, 12);switch (ps->_status){case END_NORMAL:wprintf(L"您主动结束游戏\n");break;case KILL_BY_WALL:wprintf(L"您撞到墙上,游戏结束\n");break;case KILL_BY_SELF:wprintf(L"您撞到了自己,游戏结束\n");break;}// 释放蛇身的链表pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}
}

    5.收尾

        我们将上述内容封装成一个函数,这里就叫它test函数吧。其功能为将GameStart函数、GameRun函数、GameEnd函数依次进行完成后。在屏幕上显示是否再来一局,并对结果进行检测。不难,我们上代码:

void test()
{int ch = 0;do{system("cls");//创建贪吃蛇Snake snake = { 0 };//初始化游戏//1. 打印环境界面//2. 功能介绍//3. 绘制地图//4. 创建蛇//5. 创建食物//6. 设置游戏的相关信息GameStart(&snake);//运行游戏GameRun(&snake);//结束游戏 - 善后工作GameEnd(&snake);SetPos(20, 15);printf("再来一局吗?(Y/N):");ch = getchar();while (getchar() != '\n');} while (ch == 'Y' || ch == 'y');SetPos(0, 27);
}

         到这里,test函数就可以完成我们贪吃蛇游戏的运行了。我们的main函数也只需要在test函数前添加一个setlocale函数和srand函数即可。代码如下:

int main()
{//设置适配本地环境setlocale(LC_ALL, "");srand((unsigned int)time(NULL));test();return 0;
}

        结束!学到这里,相信你把下面的源码看完就可以掌握了。 

完整代码

1.snake.h

#pragma once
#pragma once#include <windows.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>#define POS_X 24
#define POS_Y 5#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'//类型的声明//蛇的方向
enum DIRECTION
{UP = 1,DOWN,LEFT,RIGHT
};//蛇的状态
//正常、撞墙、撞到自己、正常退出
enum GAME_STATUS
{OK, //正常KILL_BY_WALL, //撞墙KILL_BY_SELF, //撞到自己END_NORMAL //正常退出
};//蛇身的节点类型
typedef struct SnakeNode
{//坐标int x;int y;//指向下一个节点的指针struct SnakeNode* next;
}SnakeNode, * pSnakeNode;//typedef struct SnakeNode* pSnakeNode;//贪吃蛇
typedef struct Snake
{pSnakeNode _pSnake;//指向蛇头的指针pSnakeNode _pFood;//指向食物节点的指针enum DIRECTION _dir;//蛇的方向enum GAME_STATUS _status;//游戏的状态int _food_weight;//一个食物的分数int _score;      //总成绩int _sleep_time; //休息时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * pSnake;//函数的声明//定位光标位置
void SetPos(short x, short y);//游戏的初始化
void GameStart(pSnake ps);//欢迎界面的打印
void WelcomeToGame();//创建地图
void CreateMap();//初始化蛇身
void InitSnake(pSnake ps);//创建食物
void CreateFood(pSnake ps);//游戏运行的逻辑
void GameRun(pSnake ps);//蛇的移动-走一步
void SnakeMove(pSnake ps);//判断下一个坐标是否是食物
int NextIsFood(pSnakeNode pn, pSnake ps);//下一个位置是食物,就吃掉食物
void EatFood(pSnakeNode pn, pSnake ps);//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps);//检测蛇是否撞墙
void KillByWall(pSnake ps);//检测蛇是否撞到自己
void KillBySelf(pSnake ps);//游戏善后的工作
void GameEnd(pSnake ps);

2.snake.c

#define _CRT_SECURE_NO_WARNINGS 1#include "snake.h"// 定义一个函数用于设置控制台光标的位置
void SetPos(short x, short y)
{// 获得标准输出设备的句柄HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE);// 定义一个 COORD 结构体变量来指定光标的位置COORD pos = { x, y };// 设置控制台光标的位置SetConsoleCursorPosition(houtput, pos);
}// 欢迎界面函数
void WelcomeToGame()
{// 设置光标位置,并打印欢迎信息SetPos(40, 14);wprintf(L"欢迎来到贪吃蛇小游戏\n");// 暂停程序,等待用户按键SetPos(42, 20);system("pause");// 清屏system("cls");// 设置光标位置,打印操作说明SetPos(25, 14);wprintf(L"用 ↑. ↓. ←. → 来控制蛇的移动,按 F3 加速,F4 减速\n");SetPos(25, 15);wprintf(L"加速能够得到更高的分数\n");// 暂停程序,等待用户按键SetPos(42, 20);system("pause");// 清屏system("cls");
}// 创建游戏地图的函数
void CreatMap()
{// 打印上边界int i = 0;for (i = 0; i < 29; i++){wprintf(L"%lc", WALL);}// 打印下边界SetPos(0, 26);for (i = 0; i < 29; i++){wprintf(L"%lc", WALL);}// 打印左边界for (i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}// 打印右边界for (i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}// 初始化贪吃蛇的函数
void InitSnake(pSnake ps)
{int i = 0;pSnakeNode cur = NULL;for (i = 0; i < 5; i++){// 动态分配内存创建蛇身节点cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){// 打印内存分配失败的错误信息perror("InitSnake()::malloc()");return;}cur->next = NULL;cur->x = POS_X + 2 * i;cur->y = POS_Y;// 头插法插入链表if (ps->_pSnake == NULL) // 空链表{ps->_pSnake = cur;}else // 非空{cur->next = ps->_pSnake;ps->_pSnake = cur;}}cur = ps->_pSnake;while (cur){// 设置光标位置,打印蛇身SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}// 设置贪吃蛇的属性ps->_dir = RIGHT; // 默认向右ps->_score = 0;ps->_food_weight = 10;ps->_sleep_time = 200; // 单位是毫秒ps->_status = OK;
}// 函数:创建食物
void CreatFood(pSnake ps)
{int x = 0;int y = 0;// 生成 x 坐标为 2 的倍数// x:2~54// y: 1~25
again:do{x = rand() % 53 + 2;  // 随机生成 x 坐标,范围是 2 到 54y = rand() % 25 + 1;  // 随机生成 y 坐标,范围是 1 到 25} while (x % 2 != 0);  // 如果 x 不是 2 的倍数,重新生成// 确保食物的位置不与蛇身重合// 遍历蛇身节点pSnakeNode cur = ps->_pSnake;  // 创建一个指针 cur 并初始化为蛇头节点// 只要 cur 不为空,说明还有未检查的蛇身节点while (cur)//蛇身遍历,只要 cur 不为 NULL ,循环就会继续执行{if (x == cur->x && y == cur->y)  // 如果食物的坐标与当前蛇身节点坐标重合{// 若重合,重新生成食物位置goto again;  // 跳转到 again 标签处重新生成}cur = cur->next;  // 将 cur 指针指向下一个蛇身节点,继续检查}// 创建食物的节点pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pFood == NULL){// 打印内存分配失败的错误信息perror("CreatFood()::malloc()");return;}pFood->x = x;pFood->y = y;pFood->next = NULL;// 设置食物的位置并打印SetPos(x, y);wprintf(L"%lc", FOOD);ps->_pFood = pFood;
}// 游戏开始的函数
void GameStart(pSnake ps)
{// 设置窗口大小,并隐藏光标system("mode con cols=100 lines=30");system("title 贪吃蛇");HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);// 隐藏光标操作CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo); // 获取控制台光标信息CursorInfo.bVisible = false; // 隐藏控制台光标SetConsoleCursorInfo(houtput, &CursorInfo); // 设置控制台光标状态// 打印欢迎界面和功能介绍WelcomeToGame();// 绘制地图CreatMap();// 创建蛇InitSnake(ps);// 创建食物CreatFood(ps);
}// 打印帮助信息的函数
void PrintHelpInfo()
{SetPos(64, 14);wprintf(L"%ls", L"不能穿墙,不能咬到自己");SetPos(64, 15);wprintf(L"%ls", L"用 ↑. ↓. ←. → 来控制蛇的移动");SetPos(64, 16);wprintf(L"%ls", L"按 F3 加速,F4 减速");SetPos(64, 17);wprintf(L"%ls", L"按 ESC 退出游戏,按空格暂停游戏");SetPos(64, 18);wprintf(L"%ls", L"CSDN博主:Bit_Le");
}// 检测按键是否按下的宏定义
#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)// 暂停游戏的函数
void Pause()
{while (1){Sleep(200);// 检测空格是否按下,若按下则退出循环if (KEY_PRESS(VK_SPACE)){break;}}
}// 判断下一个位置是否为食物的函数
int NextIsFood(pSnakeNode pn, pSnake ps)
{// 更简洁的方式,直接返回比较结果return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}// 吃食物的处理函数
void EatFood(pSnakeNode pn, pSnake ps)
{// 头插法将食物节点插入蛇身链表ps->_pFood->next = ps->_pSnake;ps->_pSnake = ps->_pFood;// 释放下一个位置的节点free(pn);pn = NULL;pSnakeNode cur = ps->_pSnake;// 重新打印蛇身while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}// 增加分数ps->_score += ps->_food_weight;// 重新创建食物CreatFood(ps);
}// 未吃到食物的处理函数
void NoFood(pSnakeNode pn, pSnake ps)
{// 头插法将新节点插入蛇身链表pn->next = ps->_pSnake;ps->_pSnake = pn;pSnakeNode cur = ps->_pSnake;while (cur->next->next != NULL){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}// 把最后一个结点打印成空格SetPos(cur->next->x, cur->next->y);printf("  ");// 释放最后一个结点free(cur->next);// 把倒数第二个节点的地址置为 NULLcur->next = NULL;
}// 检测蛇是否撞墙的函数
void KillByWall(pSnake ps)
{// 判断蛇头是否撞到边界if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 ||ps->_pSnake->y == 0 || ps->_pSnake->y == 26){ps->_status = KILL_BY_WALL;}
}// 检测蛇是否咬到自己的函数
void KillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;while (cur){// 判断蛇头是否与蛇身重合if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y){ps->_status = KILL_BY_SELF;break;}cur = cur->next;}
}// 蛇移动的函数
void SnakeMove(pSnake ps)
{// 创建一个新节点表示蛇即将到达的下一个位置pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNextNode == NULL){// 打印内存分配失败的错误信息perror("SnakeMove()::malloc()");return;}switch (ps->_dir){case UP:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1;break;case DOWN:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1;break;case LEFT:pNextNode->x = ps->_pSnake->x - 2;pNextNode->y = ps->_pSnake->y;break;case RIGHT:pNextNode->x = ps->_pSnake->x + 2;pNextNode->y = ps->_pSnake->y;break;}// 检测下一个位置是否为食物if (NextIsFood(pNextNode, ps)){EatFood(pNextNode, ps);}else{NoFood(pNextNode, ps);}// 检测蛇是否撞墙KillByWall(ps);// 检测蛇是否咬到自己KillBySelf(ps);
}// 游戏运行的主逻辑函数
void GameRun(pSnake ps)
{// 打印帮助信息PrintHelpInfo();do{// 打印总分数和食物的分值SetPos(64, 10);printf("总分数:%d\n", ps->_score);SetPos(64, 11);printf("当前食物的分数:%2d\n", ps->_food_weight);// 根据按键改变蛇的移动方向if (KEY_PRESS(VK_UP) && ps->_dir != DOWN){ps->_dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP){ps->_dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT){ps->_dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT){ps->_dir = RIGHT;}else if (KEY_PRESS(VK_SPACE)){Pause();}else if (KEY_PRESS(VK_ESCAPE)){// 正常退出游戏ps->_status = END_NORMAL;}else if (KEY_PRESS(VK_F3)){// 加速if (ps->_sleep_time > 80){ps->_sleep_time -= 30;ps->_food_weight += 2;}}else if (KEY_PRESS(VK_F4)){// 减速if (ps->_food_weight > 2){ps->_sleep_time += 30;ps->_food_weight -= 2;}}SnakeMove(ps); // 蛇移动一步Sleep(ps->_sleep_time);} while (ps->_status == OK);
}// 游戏结束的处理函数
void GameEnd(pSnake ps)
{SetPos(24, 12);switch (ps->_status){case END_NORMAL:wprintf(L"您主动结束游戏\n");break;case KILL_BY_WALL:wprintf(L"您撞到墙上,游戏结束\n");break;case KILL_BY_SELF:wprintf(L"您撞到了自己,游戏结束\n");break;}// 释放蛇身的链表pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}
}

3.test.c

#define _CRT_SECURE_NO_WARNINGS 1#include <locale.h>
#include "snake.h"//完成的是游戏的测试逻辑
void test()
{int ch = 0;do{system("cls");//创建贪吃蛇Snake snake = { 0 };//初始化游戏//1. 打印环境界面//2. 功能介绍//3. 绘制地图//4. 创建蛇//5. 创建食物//6. 设置游戏的相关信息GameStart(&snake);//运行游戏GameRun(&snake);//结束游戏 - 善后工作GameEnd(&snake);SetPos(20, 15);printf("再来一局吗?(Y/N):");ch = getchar();while (getchar() != '\n');} while (ch == 'Y' || ch == 'y');SetPos(0, 27);
}int main()
{//设置适配本地环境setlocale(LC_ALL, "");srand((unsigned int)time(NULL));test();return 0;
}

疑难解答

1.关于句柄(HANDLE)

    (1)句柄的概念

        句柄(Handle)是一个标识符或指针,用于在操作系统中引用或操作特定的对象或资源。 句柄的主要作用包括以下几个方面:

  1. 提供间接访问:句柄可以作为一种间接的方式来访问和操作对象,而无需直接暴露对象的内部细节和地址。这增加了系统的安全性和稳定性,因为应用程序不能直接访问和修改底层的资源。
  2. 资源管理:操作系统通过句柄来管理各种资源,如文件、窗口、进程、线程、内存块等。当应用程序请求创建或访问某个资源时,操作系统会返回一个句柄,应用程序通过这个句柄与操作系统进行交互来操作资源。
  3. 跨进程通信:句柄可以在不同的进程之间传递,使得多个进程能够共享和操作相同的资源。
  4. 系统资源保护:句柄可以限制应用程序对资源的访问权限和操作范围,防止错误或恶意的操作对系统造成破坏。 例如,在 Windows 操作系统中,当打开一个文件时,会得到一个文件句柄,通过这个句柄可以对文件进行读写操作。

        总之,句柄是操作系统和应用程序之间进行资源管理和交互的重要机制,有助于提高系统的安全性、稳定性和资源的有效利用。

    (2)句柄与指针的区别

        在前文呢,我提到句柄在本文的实现方式是void*类型。这是由存放对象的类型决定的,我们切忌将句柄和指针混淆。

        句柄与普通指针有以下重要区别:

  1. 抽象性:句柄是一种抽象的标识符,其具体的实现细节(包括所指向的内存结构和数据格式)对用户是隐藏的。而普通指针直接指向特定的内存地址,并且用户可以通过指针进行直接的内存操作。
  2. 系统控制:操作系统对句柄的管理和控制更加严格。例如,操作系统可以在必要时重新映射或回收句柄所关联的资源,而这对于普通指针是不太可能的。
  3. 跨进程有效性:句柄在某些情况下可以在不同的进程之间传递并且仍然有效,而普通指针通常在进程边界处是无效的。
  4. 类型安全性:与普通指针相比,句柄在类型上通常不具有直接的可操作性和可转换性,从而提供了更好的类型安全性。

        综上所述,虽然 HANDLE 在实现上可能与指针有某些相似之处,但不能简单地将其视为普通的指针,它们在概念和使用方式上存在显著的差异。

    2.关于标识符'_in_'和'_out_'

   _in_表示输入参数,意味着该参数是由调用者提供给函数的数据,函数通常会读取该参数的值,但不会修改它。

   _out_表示输出参数,调用者不需要预先给该参数赋值,而当函数执行完毕后,函数会将结果或相关数据存储在这个参数中,供调用者获取。

3.关于宽字符和本地化

     (1)宽字符的介绍

        这里简单的讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。

        C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为 exxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel(λ),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是一样的,不一样的只是128--255的这一段。        

        至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256x256=65536 个符号。

        后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型 wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

        在 C 语言中,宽字符(wchar_t)所占的字节数是由编译器决定的,在不同的编译环境下可能会有所不同。

        例如,在 Windows 的编译器中,通常使用 UTF-16 编码方式,wchar_t 的内存大小为 2 字节;而在大多数 Linux 系统中,使用 UTF-32 编码方式,wchar_t 的内存大小大多为 4 字节。

     (2)<locale.h>本地化

        <locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。

在标准中,依赖地区的部分有以下几项:

  •   数字量的格式
  •   货币量的格式
  •   字符集
  •   日期和时间的表示形式

        可有时候我们只希望改变上述项中的某几项,这时候便需要用到接下来讲的类项了。

        通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项:

  •   LC_COLLATE:影响字符串比较函数 strcoll()和 strxfrm()。
  •   LC_CTYPE:影响字符处理函数的行为。
  •   LC_MONETARY:影响货币格式。
  •   LC NUMERIC:影响 printf()的数字格式。
  •   LC_TIME:影响时间格式 strftime()和 wcsftime()。
  •   LC ALL-针对所有类项修改,将以上所有类别设置为给定的语言环境。

相关文章:

贪吃蛇超精讲(C语言)

前言 如果你还是个萌新小白&#xff0c;那么该项目的攻克过程一定会十分艰难。虽然作者已经将文章尽可能写的逻辑清晰&#xff0c;内容详细。但所谓“纸上得来终觉浅”&#xff0c;在讲到陌生结构和函数时&#xff0c;大家请一定自己动手去敲一遍代码&#xff0c;这很重要&…...

掌握Rust:函数、闭包与迭代器的综合运用

掌握Rust&#xff1a;函数、闭包与迭代器的综合运用 引言&#xff1a;解锁 Rust 高效编程的钥匙函数定义与模式匹配&#xff1a;构建逻辑的基石高阶函数与闭包&#xff1a;代码复用的艺术迭代器与 for 循环&#xff1a;高效数据处理的引擎综合应用案例&#xff1a;构建一个简易…...

【LeetCode】80.删除有序数组中的重复项II

1. 题目 2. 分析 3. 代码 class Solution:def removeDuplicates(self, nums: List[int]) -> int:if len(nums) < 3:return len(nums)i 0j 1k 2while(k < len(nums)):if (nums[i] nums[j]):while(k < len(nums) and nums[j] nums[k] ):k1if (k < len(nums…...

Armpro搭建教程全开源版的教程

Armpro搭建教程 全开源版的教程&#xff0c;其他未知 资源宝整理分享 www.httple.net 首先ssh执行指令安装运行环境 yum install java-1.8.0-openjdk* -y导入文件服务器 导入arm.zip到www目录下然后解压 导入jar包.zip到www目录然后解压 导入basic.zip到www目录然后解压在宝塔…...

nginx基本原理

进程模型 当nginx启动之后&#xff0c;会有一个master进程和多个worker进程。默认是一个worker进程。 master进程的作用&#xff1a;接收来自外界信号&#xff0c;向各worker进程发送信号&#xff0c;监控worker进程的运行状态&#xff0c;当worker进程在异常情况下退出后&am…...

在 CI/CD Pipeline 中实施持续测试的最佳实践!

随着软件开发周期的不断加快&#xff0c;持续集成&#xff08;CI&#xff09;和持续交付/部署&#xff08;CD&#xff09;已经成为现代软件开发的重要组成部分。在这一过程中&#xff0c;持续测试的实施对于确保代码质量、提高发布效率至关重要。本文将详细介绍在CI/CD流水线中…...

数据结构 —— B树

数据结构 —— B树 B树B树的插入操作分裂孩子分裂父亲分裂 我们之前学过了各种各样的树&#xff0c;二叉树&#xff0c;搜索二叉树&#xff0c;平衡二叉树&#xff0c;红黑树等等等等&#xff0c;其中平衡二叉树和红黑树都是控制树的高度来控制查找次数。 但是&#xff0c;这都…...

Redis 深度历险:核心原理与应用实践 - 读书笔记

目录 第一章 基础应用篇Zset并发问题 - 分布式锁再谈分布式锁客户端在请求时加锁失败策略redis异步队列位图Hyperloglog布隆过滤器GeoHashscan 命令字典结构rehash扩容大 key 扫描 第二章 原理篇线程IO模型RESP 序列化协议持久化管道事务PubSub内存管理 第三章 集群篇CAP主从同…...

微服务重启优化kafka+EurekaNotificationServerListUpdater

由于遇到服务重启导致的业务中断等异常&#xff0c;所以计划通过kafkaeureka实现服务下线通知&#xff0c;来尽可能规避这类问题。 如果可以升级spring&#xff0c;则可以考虑nacos等更为方便的方案&#xff1b; 程序优化&#xff1a; 1.默认启用的为 PollingServerListUpdater…...

removeIf 方法设计理念及泛型界限限定

ArrayList 中的 removeIf 方法是 Java 8 中引入的集合操作方法之一。它使用了 Predicate 接口作为参数&#xff0c;以便根据指定的条件移除集合中的元素。以下是对 removeIf 方法入参设计的详细解释&#xff1a; Predicate 接口 Predicate 是一个函数式接口&#xff0c;定义了…...

kubernetes集群部署elasticsearch集群,包含无认证和有认证模式

1、背景&#xff1a; 因公司业务需要&#xff0c;需要在测试、生产kubernetes集群中部署elasticsearch集群&#xff0c;因不同环境要求&#xff0c;需要部署不同模式的elasticsearch集群&#xff0c; 1、测试环境因安全性要求不高&#xff0c;是部署一套默认配置&#xff1b; 2…...

Java 随笔记: 集合与泛型

文章目录 1. 集合框架概述2. 集合接口2.1 Collection 接口2.2 List 接口2.3 Set 接口2.4 Map 接口 3. 集合的常用操作3.1 添加元素3.2 删除元素3.3 遍历元素3.4 判断大小3.5 判断是否为空 4. 迭代器4.1 迭代器的作用4.2 迭代器的使用4.3 迭代器与增强 for 循环4.4 迭代器的注意…...

SurrealDB:高效构建实时Web应用的数据库

SurrealDB&#xff1a;数据驱动&#xff0c;实时协同。用SurrealDB简化你的开发流程- 精选真开源&#xff0c;释放新价值。 概览 SurrealDB&#xff0c;一款专为现代Web应用设计的云原生数据库&#xff0c;以其创新的架构和功能&#xff0c;为开发者提供了一个强大的工具。它整…...

SQL Server查询计划阅读及分析

​​​​​​6.4.5. 查询计划阅读及分析 SQL Server中,SQL语句的查询计划可能会包含多个节点,每个节点除了包含和对应一个操作符外,还包含节点及操作符相关的其他信息,其细节与具体的操作符相关。SQL Server查询计划与Oracle执行计划中,虽然每个节点所包含内容的具体称谓…...

SAP Fiori 实战课程(二):新建页面

课程回顾 上一课中,利用Visual studio Code 新建、并运行了一个Demo工程。可以实现对项目的启动,启动后进入一个List清单。 那么本次课程的目前就是在上一节Demo的基础上,从零开始新建一个完整的页面。实现从首页清单,选择行后,鼠标点击,进入下一个页面。 准备工作 在开…...

【Rust光年纪】超越ORM:探索Rust语言多款数据库客户端库的核心功能和使用场景

数据库操作新选择&#xff1a;从异步操作到连接管理&#xff0c;掌握Rust语言数据库客户端库的全貌 前言 在现代软件开发中&#xff0c;与数据库进行交互是一个常见的任务。Rust语言作为一种高性能、内存安全的编程语言&#xff0c;拥有丰富的生态系统来支持各种数据库操作。…...

解决:事件监听器 addEventListener 被多次调用

背景&#xff1a; 给一个元素添加了事件监听&#xff0c;click 会触发 然而在实际场景中&#xff0c;点击一次&#xff0c;事件会被触发两次 阻止冒泡也没有用 解决&#xff1a; 使用API&#xff1a;event.stopImmediatePropagation() stopImmediatePropagation() 方法可防止…...

配置RIPv2的认证

目录 一、配置IP地址、默认网关、启用端口 1. 路由器R1 2. 路由器R2 3. 路由器R3 4. Server1 5. Server2 二、搭建RIPv2网络 1. R1配置RIPv2 2. R2配置RIPv2 3. Server1 ping Server2 4. Server2 ping Server1 三、模拟网络攻击&#xff0c;为R3配置RIPv2 四、在R…...

前端调试技巧:动态高亮渲染区域

效果&#xff1a; 前端界面的渲染过程、次数&#xff0c;会通过高亮变化来显示&#xff0c;通过这种效果排除一些BUG 高亮 打开方式 F12进入后点击ESC&#xff0c;进入rendering&#xff0c;选择前三个即可&#xff08;如果没有rendering&#xff0c;点击橘色部分勾选上&…...

深克隆与浅克隆的区别与实现

在软件开发中&#xff0c;克隆对象是一个常见需求。克隆的方式主要有两种&#xff1a;深克隆&#xff08;Deep Clone&#xff09;和浅克隆&#xff08;Shallow Clone&#xff09;。了解它们的区别及其实现方法&#xff0c;对于编写高效、安全的代码非常重要。 深克隆与浅克隆的…...

【学习笔记】无人机系统(UAS)的连接、识别和跟踪(六)-无人机直接C2通信

目录 引言 5.4 直接C2通信 5.4.1 概述 5.4.2 A2X直接C2通信服务的授权策略 5.4.3 USS使用A2X直接C2通信服务的C2授权程序 5.4.4 直接C2通信建立程序 引言 3GPP TS 23.256 技术规范&#xff0c;主要定义了3GPP系统对无人机&#xff08;UAV&#xff09;的连接性、身份识别…...

认识和安装R的扩展包,什么是模糊搜索安装,工作目录和空间的区别与设置

R语言以其强大的功能和灵活的扩展性,成为了无数数据分析师和研究者的首选工具。R的丰富功能和海量扩展包直接相关,但如何高效管理这些扩展包,进而充分发挥R的强大潜力?本文将为您揭示这些问题的答案。 一、R的扩展包 R的包(packages)是由R函数、数据和预编译代码组成的一…...

解决STM32开启定时器时立即进入一次中断程序问题

转自 解决STM32开启定时器时立即进入一次中断程序问题_stm32f407定时器初始化自动进入一次-CSDN博客 配置STM32定时器时&#xff0c;定时器中断使能、定时器使能、清除更新中断标志位&#xff0c;三者不同顺序程序执行时有不同效果&#xff0c;具体如下&#xff1a; TIM_Clea…...

Unity UGUI 之EventSystem

本文仅作学习笔记与交流&#xff0c;不作任何商业用途 本文包括但不限于unity官方手册&#xff0c;唐老狮&#xff0c;麦扣教程知识&#xff0c;引用会标记&#xff0c;如有不足还请斧正 1.EventSystem是什么&#xff1f; 有需要请查看手册&#xff1a;Unity - 手册&#xff1…...

USB转多路UART - USB 基础

一、 前言 断断续续做了不少USB相关开发&#xff0c;但是没有系统去了解过&#xff0c;遇到问题就很被动了。做这个USB转UART的项目就是&#xff0c;于是专门花了一天的时间学习USB及CDC相关&#xff0c;到写这文章时估计也忘得差不多了&#xff0c;趁项目收尾阶段记录一下&am…...

接近50个实用编程相关学习资源网站

Date: 2024.07.17 09:45:10 author: lijianzhan 编程语言以及编程相关工具等实用性官方文档网站 C语言文档&#xff1a;https://learn.microsoft.com/zh-cn/cpp/c-languageMicrosoft C、C和汇编程序文档&#xff1a;https://learn.microsoft.com/zh-cn/cppJAVA官方文档&#…...

在数据操作中使用SELECT子句

目录 一、INSERT 语句中使用 SELECT子句 二、UPDATE 语句中使用 SELECT子句 三、DELETE 语句中使用 SELECT子句 一、INSERT 语句中使用 SELECT子句 在 INSERT 语句中使用 SELECT子句&#xff0c;可以将一个或多个表或视图中的数据添加到另外一个表中。使用 SELECT 子句还可以…...

Golang | Leetcode Golang题解之第274题H指数

题目&#xff1a; 题解&#xff1a; func hIndex(citations []int) int {// 答案最多只能到数组长度left,right:0,len(citations)var mid intfor left<right{// 1 防止死循环mid(leftright1)>>1cnt:0for _,v:range citations{if v>mid{cnt}}if cnt>mid{// 要找…...

区块链技术在智能家居中的创新应用探索

随着物联网技术的发展和智能家居市场的蓬勃发展&#xff0c;区块链技术作为一种去中心化的数据管理和安全保障技术&#xff0c;正在逐渐引入智能家居领域&#xff0c;并为其带来了新的创新应用。本文将探讨区块链技术在智能家居中的具体应用场景、优势以及未来发展方向。 智能家…...

无需业务改造,一套数据库满足 OLTP 和 OLAP,GaiaDB 发布并行查询能力

在企业中通常存在两类数据处理场景&#xff0c;一类是在线事务处理场景&#xff08;OLTP&#xff09;&#xff0c;例如交易系统&#xff0c;另一类是在线分析处理场景&#xff08;OLAP&#xff09;&#xff0c;例如业务报表。 OLTP 数据库擅长处理数据的增、删、改&#xff0c…...

广州科 外贸网站建设/网销是做什么的

最近发现DOMDocument对象很重要,还有XMLHTTP也很重要 注意大小写一定不能弄错. 属性: 1Attributes 存储节点的属性列表(只读) 2childNodes 存储节点的子节点列表(只读) 3dataType 返回此节点的数据类型 4Definition 以DTD或XML模式给出的节点的定义(只读) …...

iis7搭建网站织梦/搜索引擎优化缩写

使用 declare 语句和strict_types 声明来启用严格模式&#xff1a; Caution&#xff1a; 启用严格模式同时也会影响返回值类型声明. Note: 严格类型适用于在启用严格模式的文件内的函数调用&#xff0c;而不是在那个文件内声明的函数。 一个没有启用严格模式的文件内调用了一个…...

wordpress showposts/2021百度最新收录方法

计算机网络 练习&#xff08;一百一十&#xff09; 当使用时间到达租约期的&#xff08;&#xff09;时&#xff0c;DHCP 客户端和 DHCP 服务器将更新租约。 A. 50% B. 75% C. 87.5% D. 100% ---------------------------------------- 答案&#xff1a; A 解析&#xff1a; …...

网页升级访问新区域/许昌正规网站优化公司

Zotero安装过程 Zotero官方下载链接&#xff1a;https://www.zotero.org/&#xff0c;目前应该是可以直接访问下载的。另外别忘了一起把浏览器的插件也安装好。 顺便注册一个同步账号&#xff0c;方便在其他移动端访问。注册好后&#xff0c;在Zotero中点击左上角“编辑”-“首…...

班级网站建设的参考文献/推广app赚佣金平台

在 老熊 的Blog上看到他们写的有关ORA-04031的文章&#xff0c;转到blog。 老熊的Blog&#xff1a; http://www.laoxiong.net/an-ora-04031-case.html ORA-04031这个错误&#xff0c;几乎每一个专业的DBA都遇到过。这是一个相当严重的错误&#xff0c;Oracle进程在向SGA申请内存…...

个人网站建设程序设计/google ads

Shell文本处理三剑客之一awk(2) 表达式与其他编程语言一样&#xff0c;awk表达式用于存储&#xff0c;操作和获取数据 一个awk表达式可由数值&#xff0c;字符常量&#xff0c;变量&#xff0c;操作符函数和正则表达式自由组合而成 变量是一个值的标识符&#xff0c;定义awk变量…...