最近有同学问我C语言学到什么程度算入门,我一直的观点是:不看翻了多少页书,就看能不能独立写出一个能跑的小项目。全局变量满天飞、指针只会声明不会用、链表只会在草稿纸上画箭头,这些毛病在写一个贪吃蛇的时候全都会暴露出来。用C语言从零实现贪吃蛇,麻雀虽小五脏俱全,数据结构、内存管理、程序流程控制、控制台交互全都涉及到了,是一个绝佳的练手项目。
这篇文章我会按照实际开发顺序来讲:从需求拆解到数据结构设计,从核心算法到完整代码实现,再到我实际调试中踩过的坑。最终能交出一个可直接编译运行的贪吃蛇,同时把链表、随机数、控制台I/O这些知识点在实战中弄明白。
1. 项目拆解与技术选型
1.1 贪吃蛇的核心需求清单
先别急着写代码,把需求想清楚。贪吃蛇游戏表面上看起来简单,但认真拆解后至少包含以下几个核心模块:
- 蛇身的表示:蛇是一条不断移动、会变长的动态序列,每次移动头部前进一格,尾部是否收缩取决于是否吃到食物。
- 移动控制:玩家通过方向键控制蛇头方向,蛇身沿蛇头轨迹前进。
- 食物生成:在空白区域随机出现食物,蛇头碰到食物后身体长度加一。
- 碰撞检测:蛇头撞到边界或自己的身体,游戏结束。
- 界面渲染:实时刷新画面,显示蛇、食物和得分。
把这五个模块列出来之后你就会发现,其中最核心的数据结构问题就是“如何表示蛇”。
1.2 为什么选链表而不是数组
这是整个项目最重要的一次选型。蛇的运动规律是头部增加一个节点,尾部减少一个节点,吃到食物后尾部不减少。这种“一端增长、一端收缩”的操作,用数组也能做,但是有两个麻烦:
第一,数组的大小是固定的。蛇最长能有多长?这取决于地图大小,提前声明一个足够大的数组当然也可以,但如果地图尺寸不确定,数组长度就不好定。第二,数组做头部插入需要移动后续所有元素,时间开销是O(n)。虽然贪吃蛇的蛇身长度一般不会超过几百,性能差距根本感觉不出来,但从学习角度讲,用链表能让你更深刻地理解“节点增删”这个操作的本质。
链表的选择也有讲究:单链表够用吗?够。蛇的移动只需要知道头节点(新蛇头位置)和尾节点(删除或保留),单链表完全可以胜任。当然用双向链表也无妨,只是没必要增加复杂度。我自己做的时候用的是单链表加一个尾指针,头部插入新节点是O(1),尾部删除只需要维护一个tail指针往前回溯。这里有一个细节:单链表删除尾节点时,需要找到倒数第二个节点,常规做法是遍历,时间复杂度O(n)。要优化也很简单,双向链表可以O(1)删除尾部,或者单链表在每次移动时通过遍历找到新的尾部——考虑到蛇身长度最多几百,遍历开销完全可以接受,所以我最终选了单链表,代码简单直白。
1.3 环境选型:跨平台与编译工具
贪吃蛇涉及控制台光标定位和键盘监听,不同平台的处理方式完全不同。如果你用的是Windows,可以用windows.h里的SetConsoleCursorPosition和_getch();如果你用的是Linux,一般用ncurses库来做界面刷新和键盘输入。
这里我建议初学者先在Windows环境下用windows.h实现一遍,因为不依赖第三方库,直接用Visual Studio或者gcc就能编译。如果你在macOS或者Linux上,用ncurses也很方便,代码风格会有差异,但核心逻辑完全一致。我后面给的代码会以Windows版本为主,同时会在最后一节说明Linux的移植要点。
2. 核心数据结构与算法设计
2.1 蛇的链式存储结构
蛇身的每个节点就是地图上的一个坐标点,数据结构定义得非常直白:
typedef struct SnakeNode { int x; int y; struct SnakeNode *next; } SnakeNode;蛇本身我建议用一个结构体来维护,而不是散落几个全局变量:
typedef struct { SnakeNode *head; SnakeNode *tail; int length; int direction; // 0上 1下 2左 3右 } Snake;把蛇作为一个整体对象来管理,比把head、tail、length都当成单独的全局变量要清晰得多。后面写移动、渲染、碰撞检测函数的时候,参数传一个Snake*就行,思路非常顺。
2.2 蛇的移动逻辑:挪一步还是变一节?
蛇的移动有两种实现思路。
第一种是“挪动法”:不改变节点数量,新的蛇头插入到前进方向的新坐标,然后删除尾部节点。这样蛇看起来就是在平滑移动,长度不变。
第二种是“增长法”:只有吃到食物时才插入新头,没吃到时蛇是不动的。但游戏要求蛇每时每刻都在前进,所以增长法并不符合实时移动的需求。
因此用挪动法,伪代码如下:
void snakeMove(Snake *snake, int newX, int newY) { // 新头插入 SnakeNode *newHead = createNode(newX, newY); newHead->next = snake->head; snake->head = newHead; // 尾部删除 SnakeNode *temp = snake->tail; snake->tail = findPrevNode(snake->head, temp); snake->tail->next = NULL; free(temp); snake->length--; // 后续吃到食物再++ }这里要注意:吃到食物的时候,尾部不要删除,只需要length++。所以移动函数和吃食物逻辑往往要合在一起考虑,通常的写法是返回一个布尔值表示是否吃到食物。
这里我觉得比较好的设计是:先判断新蛇头是否与食物坐标重合,如果是,只插入新头不删除尾部;否则插入新头并删除尾部。这样逻辑清晰,两个分支情况都在同一个函数里处理。
2.3 方向控制:防止180度掉头
方向控制是这个项目里最容易被忽略的细节。玩家按了左键,下一秒按右键,如果程序直接反转方向,蛇头会瞬间和第二节身体重叠,游戏立即判负。做过游戏的人都知道,这种体验极差。
解决办法是设置一个“缓冲方向”变量:每次读取键盘输入,只是更新缓冲方向,真正生效是在下一次移动时。并且在更新缓冲方向前要判断,新方向和当前方向不能是相反方向。
if ((dir == 0 && key != 1) && (dir == 1 && key != 0) && (dir == 2 && key != 3) && (dir == 3 && key != 2)) { // 可以有效更新方向 }这里的枚举我习惯用数字代替:0上、1下、2左、3右,不同的实现方式用不同的映射没关系,关键是“不能反向”的判断一定要做。
2.4 碰撞检测:边界的两种处理风格
碰撞检测分两类:撞边界和撞自己。
边界检测逻辑很简单:新蛇头的坐标是否超出地图边界。但这里有一个游戏设计的权衡——撞墙直接死亡还是穿墙从对面出来?经典贪吃蛇是撞墙死,也有的版本做成了穿墙模式(类似传送门)。个人建议初学阶段做撞墙死,逻辑更好理解,代码也更直观。
自撞检测需要遍历蛇身除尾节点以外的所有节点,看新蛇头坐标是否和它们重合。这里有个细节:如果蛇没有吃到食物,尾部节点在移动后会消失,所以理论上新蛇头不可能和旧尾部重合,这也是为什么我前面强调遍历时不需要检查尾节点。有些初学者写自撞检测时把所有节点都查一遍,结果发现蛇偶尔会“莫名死亡”,其实就是这个原因。
最小完整判断如下:
int isSelfCollision(Snake *snake, int newX, int newY) { SnakeNode *p = snake->head; while (p->next != NULL) { // 遍历到倒数第二个节点为止 if (p->x == newX && p->y == newY) return 1; p = p->next; } return 0; }不要小看这个“遍历到倒数第二个节点”的细节,这是算法正确性的关键,也是用链表写这个项目最容易踩的坑之一。
2.5 食物生成:随机性与地形冲突
食物生成的要求是:出现在空白格子上,不能和蛇身重合。最简单粗暴的做法是“随机生成,冲突了就重新随机”。当地图足够大、蛇身占比不高的时候,这种算法平均几次就能成功,效率不是问题。
但更优雅的做法是用“空白格子集合”。遍历整个地图,把没有蛇身占用的坐标收集到一个数组里,然后随机取一个。这个做法代码量稍大,但逻辑清晰,且一定不会死循环,尤其当蛇身很长、地图几乎占满的时候,随机重试的效率会急剧下降,甚至陷入死循环。这个场景在游戏后期是有可能出现的,所以我推荐直接用空白格子集合方案。
随机数方面需要提醒的是,rand()配合srand(time(NULL))在C语言里是标准做法,但rand()生成的随机数质量一般,对于游戏来说完全够用。你没必要在这个项目里引入更复杂的随机数生成算法,除非你想给自己加难度。
3. 环境准备与基础框架搭建
3.1 地图与坐标系统设计
地图我用二维坐标来定义,比如width=40, height=20,坐标范围x从0到width-1,y从0到height-1。控制台游戏的好处是,坐标系统可以直接映射到控制台的光标位置。
地图的四条边是墙,墙内是游戏区域。蛇初始位置一般放在地图中央偏左的位置,初始长度3节,方向向右。食物的初始位置随机生成,不与蛇身重合。
这里顺便说一下,一个常见的设计困惑是:地图用二维数组还是只用坐标集合?我的选择是:地图的显示由控制台渲染函数负责,逻辑层面只维护蛇的坐标链表和食物的坐标,不额外维护二维数组。这样避免了两份数据不一致的问题,代码也更简洁。判断某个位置是否是蛇身时,直接遍历链表即可。性能上完全没有问题。
3.2 Windows环境下的控制台编程要点
Windows下用windows.h实现控制台游戏有几个基础函数必须掌握。
第一是光标定位。控制台窗口里的输出是逐行逐字地写,你没法像图形界面那样画出任意图形。要让游戏画面“动起来”,通常的做法是清屏重绘,或者通过光标定位覆盖旧画面。清屏重绘最简单,但屏幕会闪烁;光标定位精确覆盖则画面更流畅。我做的是光标定位方案。
void gotoxy(int x, int y) { COORD pos; pos.X = x; pos.Y = y; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos); }第二是隐藏光标。如果不清除控制台光标,游戏画面里会一直有个小方块闪来闪去,非常影响观感。用下面的代码可以把光标隐藏:
CONSOLE_CURSOR_INFO cursorInfo; GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursorInfo); cursorInfo.bVisible = 0; SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursorInfo);第三是键盘监听。Windows下可以用_getch()实现非阻塞输入检测,但_getch()是阻塞的,会等待按键。不阻塞的方案是用_kbhit()先检测是否有按键,有的话再调_getch()读取。这样可以保证游戏循环里没有按键时蛇继续前进,不会卡住。
if (_kbhit()) { int key = _getch(); // 处理方向键 }方向键的读取比较特殊,方向键在Windows控制台里会返回两个字节的扫描码:第一个字节是224,第二个才是真正的键值(72上、80下、75左、77右)。所以读取方向键的代码要写成:
if (key == 224) { key = _getch(); switch (key) { case 72: newDir = 0; break; // 上 case 80: newDir = 1; break; // 下 case 75: newDir = 2; break; // 左 case 77: newDir = 3; break; // 右 } }这个细节我第一次做的时候完全不知道,调了很久才发现方向键返回值跟普通字符不一样。写出来提醒大家注意。
3.3 Linux/macOS环境下的移植思路
如果你在Linux或者macOS上开发,标准库没有提供_kbhit()和_getch(),不能用windows.h。一般用ncurses库解决。
ncurses的核心API是initscr()初始化屏幕,cbreak()设置非阻塞输入,noecho()关闭输入回显,keypad(stdscr, TRUE)开启特殊按键识别。方向键直接用KEY_UP、KEY_DOWN、KEY_LEFT、KEY_RIGHT这几个常量读取,反而比Windows更简单。
关键是Linux下没有SetConsoleCursorPosition,ncurses用move(y, x)来实现光标移动。这里注意参数顺序是“行、列”,和Windows的“x、y”刚好反着,移植时容易写错。
如果你不想依赖ncurses,也可以用ANSI转义序列实现光标定位,比如printf("\033[%d;%dH", y+1, x+1),加上清屏转义序列。这种方式代码更轻量,但在Windows默认终端下可能支持不完整,建议只在Linux下用。我自己的习惯是:跨平台优先选ncurses,省心。
4. 完整代码实现与逐步讲解
4.1 代码结构总览
我最终实现的版本规模大约300行C代码,包含以下几个文件:
- main.c:游戏主循环
- snake.h / snake.c:蛇的数据结构与核心逻辑
- food.h / food.c:食物生成逻辑
- render.c:画面渲染
如果你的环境不支持多文件编译,全部写在一个.c文件里也没问题。我实现时用了多文件,因为这样结构清晰,也方便你以后扩展。
首先给出snake.h的头文件内容:
#ifndef SNAKE_H #define SNAKE_H typedef struct SnakeNode { int x; int y; struct SnakeNode *next; } SnakeNode; typedef struct { SnakeNode *head; SnakeNode *tail; int length; int direction; } Snake; Snake *initSnake(); void snakeMove(Snake *snake, int newX, int newY, int grow); int isSelfCollision(Snake *snake, int newX, int newY); void freeSnake(Snake *snake); #endifmain.c里的游戏循环大概长这样:
#include <stdio.h> #include <windows.h> #include <time.h> #include "snake.h" int main() { // 初始化 srand((unsigned)time(NULL)); Snake *snake = initSnake(); Food food = generateFood(snake); int newDir = snake->direction; int score = 0; // 游戏循环 while (1) { // 读取输入并更新方向 if (_kbhit()) { int key = _getch(); if (key == 224) { key = _getch(); newDir = decodeDirection(key); } } // 方向不能反向 if (!isOpposite(newDir, snake->direction)) { snake->direction = newDir; } // 计算新蛇头位置 int newX = snake->head->x + dx[snake->direction]; int newY = snake->head->y + dy[snake->direction]; // 边界碰撞检测 if (newX < 0 || newX >= WIDTH || newY < 0 || newY >= HEIGHT) break; // 自撞检测 if (isSelfCollision(snake, newX, newY)) break; // 是否吃到食物 int isFood = (newX == food.x && newY == food.y); snakeMove(snake, newX, newY, isFood); if (isFood) { score++; food = generateFood(snake); } // 渲染画面 render(snake, food, score); // 控制游戏速度 Sleep(100); } // 游戏结束 freeSnake(snake); printf("Game Over! Score: %d\n", score); return 0; }4.2 初始化函数:内存分配常见错误
初始化蛇的函数非常直接,但也是初学者最容易犯内存错误的地方之一。
Snake *initSnake() { Snake *snake = (Snake *)malloc(sizeof(Snake)); if (!snake) exit(1); SnakeNode *node; SnakeNode *prev = NULL; for (int i = 0; i < 3; i++) { node = (SnakeNode *)malloc(sizeof(SnakeNode)); node->x = 10 + i; node->y = 10; node->next = NULL; if (prev == NULL) { snake->head = node; } else { prev->next = node; } prev = node; } snake->tail = prev; snake->length = 3; snake->direction = 3; // 初始向右 return snake; }这里有个关键点:tail等于最后一个节点的时候,如果忘记设置snake->tail,后续所有尾部删除操作都会出错。所以我在初始化后特意检查了一次:tail节点坐标应该是(12, 10)。
一个更隐蔽的问题是:malloc之后没检查返回值就继续使用。虽然在小程序里malloc失败的概率极低,但代码规范要求必须判断。上面的示例已经加上了检查。
4.3 移动与增长:单链表关键操作
移动函数是核心中的核心。每次移动需要插入一个头节点,再删除一个尾节点。这个函数我建议分成两个小函数:insertHead和deleteTail,这样职责更清晰。
void insertHead(Snake *snake, int x, int y) { SnakeNode *newNode = (SnakeNode *)malloc(sizeof(SnakeNode)); newNode->x = x; newNode->y = y; newNode->next = snake->head; snake->head = newNode; } void deleteTail(Snake *snake) { if (snake->head == NULL || snake->head->next == NULL) { // 只剩一个节点时特殊处理 free(snake->head); snake->head = snake->tail = NULL; snake->length = 0; return; } SnakeNode *p = snake->head; while (p->next != snake->tail) { p = p->next; } free(snake->tail); snake->tail = p; p->next = NULL; } void snakeMove(Snake *snake, int newX, int newY, int grow) { insertHead(snake, newX, newY); if (!grow) { deleteTail(snake); snake->length--; // 注意:插入时长度为+1,未增长时再-1,保持长度不变 } }一个常见的困惑是snake->length在这里的维护。更直观的做法其实是在insertHead里length++,deleteTail里length--。我把长度维护放在上一层函数是因为游戏主循环里还需要length来决定得分和食物逻辑,不过这不是硬性要求,看你个人习惯。
4.4 渲染逻辑:清屏还是局部更新?
如果你用printf加清屏来渲染,代码简单但画面闪烁。我在项目里使用的是gotoxy局部更新,每次移动只需把新蛇头画出来,把旧蛇尾擦除,再把旧蛇头位置变成蛇身。这样画面非常流畅。
局部更新的逻辑如下:
void renderSnakeMove(Snake *snake, int oldTailX, int oldTailY, int grew) { // 画新蛇头 gotoxy(snake->head->x, snake->head->y); printf("@"); // 如果长度增加了,不需要擦除尾部 if (!grew) { gotoxy(oldTailX, oldTailY); printf(" "); } }这里有一个技巧:为了擦除旧蛇尾,我需要记住移动之前尾巴的坐标,所以在调用snakeMove之前,先保存旧尾巴坐标。很多实现直接清屏重绘,看不到这个细节,但局部更新确实能明显提升游戏体验。
完整的render函数还需要绘制墙壁、食物和记分板。墙壁的绘制可以在初始化时做一次,不需要每帧重画。
void drawBorder() { for (int x = 0; x < WIDTH; x++) { gotoxy(x, 0); printf("#"); gotoxy(x, HEIGHT - 1); printf("#"); } for (int y = 0; y < HEIGHT; y++) { gotoxy(0, y); printf("#"); gotoxy(WIDTH - 1, y); printf("#"); } }4.5 游戏速度控制与帧率机制
游戏的“速度”就是每帧之间的间隔时间。C语言里最简单的做法是Sleep(),Windows下Sleep(100)表示每帧间隔100毫秒,也就是每秒移动10格。想要提高难度,可以把这个100毫秒变成一个变量,吃一个食物减5毫秒,快感加倍。
int speed = 100; ... Sleep(speed);实际上更精确的做法是记录每一帧的开始时间,再加上固定时间差来保证不同机器上的速度一致。但这个项目里Sleep就够了,毕竟控制台游戏对帧率精度要求不高。我在开发过程中发现,Sleep(100)在某些高分辨率计时器的机器上实际延迟不准,但这不影响游戏体验,只有游戏结束时统计成绩才会有细微偏差。
5. 常见问题与避坑指南
5.1 蛇的宿命:为什么蛇会自己撞死
这是所有初学者在写贪吃蛇时几乎都会遇到的现象——Snake突然死亡,明明前方没有障碍。
回忆一下前面说的自撞检测遍历范围:新蛇头不应该和旧尾节点比较。因为如果没吃到食物,移动完成后旧尾节点已经不存在了。如果在遍历时把tail也包含进来,当蛇头刚好移动到一个原本由尾巴占用的位置时,就会误判为撞到自己,游戏错误结束。我调试的时候就见过这种“灵异死亡”,最后打印日志才发现是遍历范围多了一个节点。
5.2 方向反转:按得快的致命陷阱
还有一次我测试的时候,蛇先向右走,我快速按了“下”再按“左”,结果蛇直接掉头撞死。问题出在读输入的时候一次性处理了多个按键,导致方向连续更新两次,第二次更新已经无法判断前一次方向了。
解决办法是只读取第一个有效按键,或者读入所有按键但只取最后一个有效值。但无论如何,核心是:每次移动只能根据当前真实方向来判断新方向是否有效,而不是基于缓冲方向来判断。因为缓冲方向可能叠加多个按键操作,判断链条拉长后就容易出bug。
5.3 内存泄漏:每次移动都是隐患
每移动一步,不增长时,将分配一个新头节点并释放一个尾节点。增长时,只分配新头节点。游戏结束时必须释放整条链表。
初学者最容易犯的错误是:游戏结束时只释放了head指针指向的一个节点,其他节点全部泄漏。正确的释放方式要遍历整条链表:
void freeSnake(Snake *snake) { SnakeNode *p = snake->head; while (p) { SnakeNode *temp = p; p = p->next; free(temp); } free(snake); }我在测试的时候用了一个简单的循环跑1000局游戏,然后用任务管理器观察内存占用量。如果发现内存只涨不降,那基本就是释放逻辑有问题。
还有一个更隐蔽的问题:删除尾节点时,先用临时指针指向tail,然后重新搜索新的tail并更新tail指针,最后free临时指针。如果顺序错了,先free再搜索,那你访问的就是被释放的内存,在Windows上可能不报错,但已经是未定义行为。确保顺序是:存旧tail -> 找新tail -> 更新tail -> free旧tail。
5.4 字体与终端影响画面
控制台游戏的渲染效果和终端设置关系很大。Windows默认终端对字符间距处理得不好,导致蛇看起来“宽高比例失调”。解决办法是在主函数开头设置控制台窗口大小和字体。经验值是控制台窗口设置在80x25左右,地图做得比窗口略小,蛇身的字符用单个窄字符,视觉上就正常了。
5.5 方向键扫描码的再提醒
这个坑值得再强调一次。在Windows中,方向键和功能键(F1-F12)都返回两个字节。如果你只调用了_getch()一次,读到的只是224(0xE0),方向信息被丢掉了,于是蛇完全不动或者方向永远不变化。我调试的时候曾经一度以为是代码逻辑错了,排查了很久才发现是键盘读取的问题。
处理方式很简单:
int key = _getch(); if (key == 224 || key == 0) { // 某些键盘可能返回0而不是224 key = _getch(); }key == 0的情况也是存在的,这是历史兼容问题,把0和224都判断一下更稳妥。
5.6 动态食物生成死循环问题
用“随机重试”方式生成食物时,地图越大、蛇越短,越不容易出问题。但当游戏后期蛇身几乎占满地图时,随机碰运气的效率会急剧下降,更糟的是如果所有格子都被蛇占满,while循环会永远退不出去。
如果你用的是空白格子集合的方式,这个问题就不存在了。这也是我在前面强烈推荐空白格子集合方案的原因——用一个O(width*height)的遍历收集所有空白坐标,然后rand一个下标,效率和安全性都碾压随机重试法。
6. 从能跑到好玩:功能扩展与性能优化
6.1 计分、难度与公告牌
游戏主循环里,我加了三样东西:分数计数器、关卡升级、公告牌显示。分数每吃一个食物加10分,每吃5个食物关卡加1,速度提升10%。
公告牌放在游戏区域右侧,显示当前分数、关卡、蛇身长度和操作提示,这些信息用gotoxy写在固定位置即可。
6.2 暂停与重新开始
我在游戏循环里加了暂停功能,按空格键暂停,再按一次继续。这个功能看似简单,实际上需要把键盘监听从“每次循环只读一个按键”改成“循环里可以一直读直到没有按键输入”。
更重要的功能是Game Over后的重新开始。由于我的蛇和食物都封装在结构体里,重新初始化只需要调用initSnake()并重新生成食物即可。但要注意把之前分配的蛇内存先释放掉,否则就是内存泄漏。
6.3 更深层的优化:把地图做成二维逻辑数组
如果你的目标是进一步学习,可以试着把地图做成一个二维数组,0表示空地、1表示墙壁、2表示食物、3表示蛇身。这样做的好处是碰撞和食物生成判断都非常快,O(1)的时间复杂度直接查表。坏处是维护两份数据(链表和数组)需要同步更新,代码复杂度上升。
我个人建议是:第一版用链表,能跑通,理解数据结构了;第二版再改成二维数组,比较两种方案的差异,这种对比学习的效果比单纯看教程好得多。
6.4 面向对象的C语言写法
虽然题目要求的是C语言,但如果你已经学完C++,可以思考一下如何把Snake和Food封装成类,把渲染逻辑放入某个类的方法中。这也是锻炼代码组织能力的好方向。毕竟C语言学了链表,下一步就是理解“数据 + 操作 = 对象”的雏形,这对于后续学习C++、数据结构乃至整个编程思维都非常有价值。
7. 写在最后的一些私货
这个项目我前后大概重写了三次。第一次是照着网上教程抄,能跑但是不懂为什么;第二次自己从头写,结果被自撞检测的方向键扫描码折磨了一晚上;第三次才算真正理解每一行代码。我自己的体会是:如果你能独立完成从数据结构设计、逻辑实现、编译调试到功能扩展的全过程,你对C语言中指针、内存和程序结构的理解会有一个质的飞跃。
最后分享一个小技巧:调试贪吃蛇时,不要只在脑子里推演,善用打印日志。我写了一个debug函数,可以把蛇链表中所有节点的坐标依次打印出来,配合Sleep加长间隔,观察每帧的移动过程。这个方法帮我定位了至少三个逻辑错误,强烈推荐你也试试。
如果你在实现过程中卡住了,可以回到这篇文章,把对应小节的代码对照着看。项目不复杂,但每个细节都值得推敲。祝你能写出一个跑得流畅、玩起来有手感的贪吃蛇。