资讯详情

基于C语言的贪吃蛇游戏源码解析:链表与指针实战

📅 2026/9/23 17:57:01 | 华诺云谱 👁 阅读
基于C语言的贪吃蛇游戏源码解析:链表与指针实战
简介一套用C语言编写完成的经典贪吃蛇游戏源码项目主要面向C语言初学者、游戏开发爱好者以及需要课程设计或毕业设计参考的高校学生。整个资源包共57个文件、约18.11MB里面既包含4个C源代码文件、7个PDB调试文件、4个可执行文件也包含dsw/dsp等工程配置、.gitignore与LICENSE等配套内容从源码到调试信息再到可直接运行的程序都一一覆盖目录结构清晰便于开发者编译、运行和调试。项目内收录了贪吃蛇1.0、2.0、3.0等多个功能的版本便于对比学习游戏从基础到完善的迭代思路也可尝试基于这些源码扩展双人对战、障碍物、加速等玩法。源码实现涉及游戏循环、用户输入处理、碰撞检测以及链表数据结构等C语言核心知识配合PDB文件还能体验借助调试工具分析程序状态、定位逻辑错误的完整流程。现在已有116人学习下载是一份兼具教学和实践价值的C语言入门项目资料。1. 从课程设计到“能跑”为什么经典贪吃蛇是C语言最好的练手项目基于C语言的经典贪吃蛇游戏设计源码是C语言学习者绕不开的一个坎。它不算难但恰好把指针、结构体、链表、函数封装、随机数和控制台交互全部串起来做完你就知道自己对C语言的掌握是“看得懂”还是“写得出”。这个项目主要解决两类问题一是课程设计要交付一份能演示、能打分、逻辑完整的源码二是你想在动手过程中把链表和内存管理练扎实。适合正在补C语言基础、准备计算机二级、或者想给单片机项目做预热的同学。下面从数据结构讲起一步步拆到能编译运行的最小版本。2. 数据结构先行用链表还是数组装下那条蛇2.1 为什么单链表是贪吃蛇的最佳选择很多课程设计用二维数组加一个“长度变量”来实现贪吃蛇蛇每移动一格就把整条蛇的坐标整体平移一次。这个做法能跑通但有两个硬伤第一蛇每次吃到食物变长数组长度就要跟着动要么开大数组浪费空间要么频繁 memcpy第二整体平移的写法绕代码改起来容易出边界问题。用单链表来存蛇身每个节点就是蛇的一节蛇移动一次只需要“头插一个新节点、尾部删掉一个旧节点”时间复杂度是 O(1)吃食物的时候不删尾就行逻辑和蛇的实际形态完全对应。从 C 语言学习的角度看链表也正好把指针和内存管理这两块硬骨头带出来了。节点里存 x 和 y 坐标再加一个指向下一节的指针申请节点用 malloc游戏结束用 free 把整条链释放。做完这个项目再去做翁恺老师的链表练习题你会有种“这题我写过”的熟悉感。很多资料里说的 c语言链表、c语言指针在这个项目里不再是抽象概念而是每帧都在跑的代码。2.2 地图建模二维数组、边界墙与食物生成贪吃蛇的地图我习惯用一个 int 类型的二维数组 map[H][W] 来建模每个格子用数字区分角色0 表示空地1 表示墙2 表示食物3 表示蛇身。墙放在数组的最外圈蛇的活动范围是 (1,1) 到 (W-2, H-2) 的矩形区域。这样判断撞墙时只需要看新头的坐标是否越界不用单独维护边界逻辑绘制时也可以直接按 map 里的值输出对应字符状态和显示共用一份数据。#define MAP_W 20 #define MAP_H 20 #define EMPTY 0 #define WALL 1 #define FOOD 2 #define SNAKE 3 int map[MAP_H][MAP_W]; void initMap(void) { for (int i 0; i MAP_H; i) { for (int j 0; j MAP_W; j) { if (i 0 || i MAP_H - 1 || j 0 || j MAP_W - 1) map[i][j] WALL; else map[i][j] EMPTY; } } }这里 MAP_H 和 MAP_W 是地图的行列数用宏定义方便后续改难度。EMPTY、WALL 这些状态建议用宏或枚举而不是裸数字因为后面所有读 map 值的地方看到 SNAKE 比看到 3 更好理解。初始化时把四周填成墙内圈留空地贪吃蛇只能在内圈活动。食物生成要满足两个条件位置在空地内、不能压在蛇身上。我用一个 do-while 循环随机生成坐标直到落在 EMPTY 格子上。因为地图不算大实际循环次数很少。随机数种子要在程序启动时用 srand 配上 time(NULL) 设置一次否则每次运行食物位置都一样这也是 c语言基础 里特别爱考的随机数知识点。void spawnFood(void) { int x, y; do { x rand() % (MAP_W - 2) 1; y rand() % (MAP_H - 2) 1; } while (map[y][x] ! EMPTY); map[y][x] FOOD; }2.3 蛇节点的定义与内存管理蛇身节点用一个结构体定义typedef struct SnakeNode { int x, y; struct SnakeNode *next; } SnakeNode;全局维护 head 和 tail 两个指针初始时长度为 3方向向右。创建节点用 malloc赋值 x、ynext 置空每次移动头插一个新节点、尾删一个旧节点。这里有个细节值得注意tail 指针不能等到链表遍历到最后一个才发现尾巴在哪而是在插入和删除时同步维护。去掉尾巴时要先找到倒数第二个节点让它成为新尾巴然后把旧尾巴的坐标在 map 上清成 EMPTY再 free。很多初学 c语言内存管理 的同学会在这里踩坑先 free 了节点再通过它去拿 next这就是典型的 use-after-free。释放顺序反过来从 head 开始逐个往后 free每释放一个节点前先保存 next再 free 当前节点。这部分代码会放在第 4 章的完整源码里第 5 章也会说一个 double free 的典型报错。3. 移动逻辑与碰撞检测别让蛇倒着走3.1 方向控制的经典陷阱方向反转与按键缓冲贪吃蛇的方向控制有个很经典的“反转翻车”蛇正在向右走玩家快速按了“上”再按“左”如果代码是拿到键就更新方向第二下会把方向从“上”硬掰成“左”而实际上蛇头刚向上掉头再往左就相当于直接倒车穿进自己身体。根因在于一次帧循环里只允许方向改变一次不能连续改两次。我的处理方式是方向变量只由当前实际运动方向决定每帧先读一次键盘读到一个合法转向后就做判断判断依据是当前方向 dirX/dirY而不是待生效的方向。比如按“上”时检查 dirY ! 1也就是当前如果正在向下走就拒绝掉头这样就不会出现 180 度反转。int newDirX dirX, newDirY dirY; if (_kbhit()) { int key _getch(); if (key w dirY ! 1) { newDirX 0; newDirY -1; } else if (key s dirY ! -1) { newDirX 0; newDirY 1; } else if (key a dirX ! 1) { newDirX -1; newDirY 0; } else if (key d dirX ! -1) { newDirX 1; newDirY 0; } } dirX newDirX; dirY newDirY;这一小段就解决了最容易被扣分的“蛇自己撞自己”问题。逻辑说明_kbhit 检测键盘缓冲区是否有输入_getch 读取单个字符且不回显。每帧只调用一次 _kbhit天然限制了“一帧只转向一次”。参数上w/s/a/d 分别对应上/下/左/右这是我在本机调试时定的键位。你也可以改成方向键但方向键会多一个前缀字节读取逻辑要额外处理新手建议直接用 WASD。3.2 身体跟随头插尾删与“贪吃”判断贪吃蛇的移动本质是“蛇头前进一格蛇尾收缩一格或不收缩”。用链表实现时我每次把新坐标作为新节点插到头部再根据这一步有没有吃到食物决定是否删除尾节点。吃到食物时尾节点保留蛇身长度加 1没吃到就删尾总长度不变。这样写出来的逻辑树只有两个分支非常清晰。移动前先根据 dirX、dirY 计算出新头的坐标 nx head-x dirX、ny head-y dirY然后判断这个位置是墙、蛇身还是食物。判断撞墙看坐标是否落在边界外判断撞自己看 map[ny][nx] 是否为 SNAKE。int updateGame(void) { int nx head-x dirX; int ny head-y dirY; if (isCollide(nx, ny)) // 撞墙或撞自己 return 0; if (map[ny][nx] FOOD) { // 吃到食物长一节 addHead(nx, ny); score; spawnFood(); } else { // 普通移动头进尾退 addHead(nx, ny); removeTail(); } return 1; }这里 addHead 负责 malloc 新节点、接到 head 前面、把 map 对应位置标成 SNAKEremoveTail 负责清掉尾节点在 map 上的标记、free 节点并让倒数第二个节点成为新 tail。注意 removeTail 里 map[tail-y][tail-x] EMPTY 这行必须在 free 之前执行否则就犯了 use-after-free。3.3 死亡判定与计分参数isCollide 要排除一个“合法情况”蛇头下一步要走进的位置恰好是当前蛇尾所在。因为普通移动时蛇尾这一格马上会被释放蛇头可以安全地从蛇尾的位置经过。如果不排除蛇在绕小圈时会把“即将释放的尾巴”误判成“撞到自己”这是很多贪吃蛇版本莫名死亡的另一个原因。int isCollide(int x, int y) { if (x 0 || x MAP_W - 1 || y 0 || y MAP_H - 1) return 1; // 越界撞墙 if (map[y][x] SNAKE !(tail x tail-x y tail-y)) return 1; // 撞到蛇身但蛇尾除外 return 0; }计分参数我放在全局score 每次吃食物加 1speedMs 控制每帧间隔比如 200 表示 200 毫秒走一格数值越小蛇越快。难度递增可以简单写成 speedMs - 5但下限要兜住比如 80避免快到没法操作。分数、速度和地图大小这三个参数是贪吃蛇解析里最常见的调试点自己重写时先把这三个参数做成宏或变量后面改难度就不用动逻辑代码。4. 跑通最小版本源码结构与编译运行4.1 文件结构拆三个文件还是写一个 main.c课程设计的源码我建议拆成三个文件game.h 放结构体、宏定义和函数声明game.c 放所有核心逻辑main.c 只放主循环。拆文件的好处是逻辑边界清楚答辩时老师问“游戏循环在哪”你直接说 main.c问“地图怎么初始化”说 game.c 的 initMap比在几百行单文件里翻半天体面得多。如果你只是自己练手、或者老师要求交单文件也可以把三块内容合并不影响运行。单文件版还有一个实际好处复制到 VSCode 或者用 gcc 编译时不用考虑头文件路径。我下面的最小源码就是单文件用 VSCode 配置好 c 语言环境之后直接编译运行。4.2 一个可直接编译运行的控制台版贪吃蛇源码下面这是我在 Windows 下用 MinGW 的 gcc 编译验证过的最小版本地图 20×20初始蛇长 3WASD 控制吃到食物长度和分数增加速度固定 200ms 一帧。整体代码不到 160 行适合做课程设计底子。#include stdio.h #include stdlib.h #include time.h #include conio.h #include windows.h #define MAP_W 20 #define MAP_H 20 #define INIT_LEN 3 #define EMPTY 0 #define WALL 1 #define FOOD 2 #define SNAKE 3 typedef struct SnakeNode { int x, y; struct SnakeNode *next; } SnakeNode; SnakeNode *head NULL, *tail NULL; int dirX 1, dirY 0; int score 0; int speedMs 200; int map[MAP_H][MAP_W]; SnakeNode *createNode(int x, int y) { SnakeNode *p (SnakeNode *)malloc(sizeof(SnakeNode)); p-x x; p-y y; p-next NULL; return p; } void initMap(void) { for (int i 0; i MAP_H; i) for (int j 0; j MAP_W; j) { if (i 0 || i MAP_H - 1 || j 0 || j MAP_W - 1) map[i][j] WALL; else map[i][j] EMPTY; } } void initSnake(void) { int sx MAP_W / 2, sy MAP_H / 2; head tail createNode(sx, sy); for (int i 1; i INIT_LEN; i) { SnakeNode *p createNode(sx - i, sy); p-next head; head p; } for (SnakeNode *p head; p; p p-next) map[p-y][p-x] SNAKE; } void spawnFood(void) { int x, y; do { x rand() % (MAP_W - 2) 1; y rand() % (MAP_H - 2) 1; } while (map[y][x] ! EMPTY); map[y][x] FOOD; } void addHead(int x, int y) { SnakeNode *p createNode(x, y); p-next head; head p; map[y][x] SNAKE; } void removeTail(void) { if (!head) return; if (head tail) { map[tail-y][tail-x] EMPTY; free(tail); head tail NULL; return; } SnakeNode *p head; while (p-next ! tail) p p-next; map[tail-y][tail-x] EMPTY; free(tail); tail p; tail-next NULL; } int isCollide(int x, int y) { if (x 0 || x MAP_W - 1 || y 0 || y MAP_H - 1) return 1; if (map[y][x] SNAKE !(tail x tail-x y tail-y)) return 1; return 0; } int updateGame(void) { int nx head-x dirX; int ny head-y dirY; if (isCollide(nx, ny)) return 0; if (map[ny][nx] FOOD) { addHead(nx, ny); score; spawnFood(); } else { addHead(nx, ny); removeTail(); } return 1; } void drawMap(void) { system(cls); for (int i 0; i MAP_H; i) { for (int j 0; j MAP_W; j) { switch (map[i][j]) { case WALL: putchar(#); break; case FOOD: putchar(*); break; case SNAKE: putchar(O); break; default: putchar( ); break; } } putchar(\n); } printf(score: %d speed: %dms\n, score, speedMs); } void freeSnake(void) { SnakeNode *p head; while (p) { SnakeNode *n p-next; free(p); p n; } head tail NULL; } int main(void) { srand((unsigned)time(NULL)); initMap(); initSnake(); spawnFood(); while (1) { drawMap(); int newDirX dirX, newDirY dirY; if (_kbhit()) { int key _getch(); if (key w dirY ! 1) { newDirX 0; newDirY -1; } else if (key s dirY ! -1) { newDirX 0; newDirY 1; } else if (key a dirX ! 1) { newDirX -1; newDirY 0; } else if (key d dirX ! -1) { newDirX 1; newDirY 0; } } dirX newDirX; dirY newDirY; if (!updateGame()) break; Sleep(speedMs); } printf(Game Over! final score: %d\n, score); freeSnake(); return 0; }这段代码的运行逻辑是主循环先画地图再处理按键转向生效后走 updateGame 移动一步然后 Sleep 控制帧间隔。drawMap 放在按键之前保证移动完下一秒先看到最新状态。updateGame 返回 0 表示撞墙或撞自己循环退出后打印最终分数并释放链表。几个参数说明MAP_W 和 MAP_H 控制地图大小改大之后食物生成和撞墙判断会自动适配。INIT_LEN 是初始长度改太大会出现初始蛇身重叠的异常建议保持 3。speedMs 是每帧毫秒数200 是新手友好的速度练习时可以降到 100。随机数 rand 的种子在 main 开头用 time 设置所以每次运行食物位置都不一样。4.3 编译命令与运行环境配置我在 Windows 上最常用的编译命令是gcc snake.c -o snake.exe -Wall-Wall 打开所有警告写链表时特别建议开着。malloc 忘记检查返回值、类型转换不匹配之类的隐患警告里都能看到。编译成功后直接运行 snake.exe。用 Visual Studio 打开 .c 文件时控制台中文乱码是常见问题把源文件另存为 UTF-8 带 BOM 或 GBK 编码即可。如果报 scanf 不安全在文件最顶上加#define _CRT_SECURE_NO_WARNINGS5. 避坑贪吃蛇源码里最常见的问题与排查5.1 蛇移动一顿一顿按键按下去没反应现象蛇能跑但速度不稳定有时候按方向键要过一两秒才有反应重按又变灵敏。原因一是 Sleep 和键盘读取串行Sleep 期间按键被积压在系统缓冲区下一帧 _kbhit 一次读一个键积压的键会在后面连续几帧被消费二是部分终端里 putchar 输出没有刷新缓冲屏幕绘制延迟。解决把 Sleep 放在 drawMap 和按键处理之后确保每帧固定间隔。对按键处理做“每帧只处理一次”积压键可以清空缓冲区丢弃。这个问题的本质是键盘缓冲和游戏循环的频率不一致理解了这一点就不会再对着 _kbhit 玄学调参。5.2 蛇吃到食物后长度没变分数却涨了现象屏幕显示吃到了 *分数也 1但画面里蛇的长度没有变长。原因addHead 和 removeTail 的顺序写反了。先 removeTail 再 addHead结果新头加进来尾巴同时被删掉长度始终不变只是位置前移一格。这种问题最容易出现在边贴代码边改的时候。解决严格按“先加头、后删尾”的顺序写吃到食物分支不要调用 removeTail。还有一个排查技巧把 map 某格的角色打出来调试食物格在被吃掉后应该变成 SNAKE如果看到 EMPTY 说明 addHead 没生效。5.3 程序退出时报 double free 或段错误现象游戏能正常玩但关闭窗口时控制台弹出错误或者直接在 freeSnake 处崩溃。原因freeSnake 释放链表时如果 tail 指针还指向已释放节点后面再次 free 就会 double free。另一种情况是 removeTail 里 free 之后没有把 tail 置空头部节点的 next 还挂着旧地址freeSnake 顺着访问到非法内存。解决removeTail 里让 tail p; tail-next NULL; 并把 head tail 的边界单独处理。freeSnake 里用一个临时变量保存 next再 free 当前节点。这个错误每写一次链表就会碰到一次遇到先看是不是哪个节点没置空。5.4 蛇会直接穿墙或者从墙里冒出来现象蛇头走到地图边界没有死亡镜头外的格子出现了一个蛇头。原因isCollide 里撞墙判断用了 而不是 比如 y MAP_H 时已经越界但写成 y MAP_H - 1 就把最下面一行当成了合法区域。还有一种情况是地图边界并没有全部被 WALL 填满初始化时循环边界没闭合。解决把撞墙条件写成 x 0 || x MAP_W - 1 || y 0 || y MAP_H - 1保证只有内圈坐标是合法活动区。再用一个临时的 for 循环打印 map检查整圈墙是否都填上了。5.5 画面闪烁严重蛇身有重影现象每次刷新屏幕都能看到明显的闪动蛇的身体像拖了条尾巴。原因system(cls) 清屏后逐行重新绘制控制台光标每次都回到左上角重新输出刷新率不够时视觉上就是闪烁。重影则是因为旧帧没有完全清除通常是输出行数不够历史残影留在了屏幕上。解决临时方案是把系统控制台缓冲区高度调大或者在 initMap 之后先清一次屏。更好的做法是用 Windows API 的 SetConsoleCursorPosition 函数把光标定位到左上角再绘制不清屏直接覆盖。这个技巧在第 6 章有示例做完之后画面会顺滑很多。6. 进阶从“能跑”到“值得玩”——速度曲线、验证手法与嵌入式移植方向最后这章讲一个我常用的收尾动作把固定速度改成“难度曲线”。做法是每吃 5 个食物把 speedMs 下调 15 毫秒到 80 毫秒封底。对应分数和难度会同时增长玩起来才有挑战性。核心代码只有三行if (score 0 score % 5 0 speedMs 80) { speedMs - 15; }这个写法要注意条件顺序score % 5 0 每次都会触发所以必须用 speedMs 80 兜底否则速度会掉到 0 导致死循环。实际调的时候可以打印 speedMs观察每档速度的手感差异再决定 15 毫秒的步长要不要改成 10。地图绘制的进阶方案把 system(cls) 换成光标定位。Windows 下用 SetConsoleCursorPositionLinux 终端可用 ANSI 转义序列。每次刷新前把光标移回初始位置再逐格覆盖输出闪烁问题基本消失。如果还想加存档把分数和蛇身坐标按链表顺序写进文件读档时重建链表并调用 spawnFood这套逻辑和操作系统课程里常讲的内存映像恢复思路是同源的。想往嵌入式方向走的话把控制台输入换成按键扫描、把 Sleep 换成定时器中断或 RTOS 延时地图输出换成 OLED 或点阵屏整份源码的框架可以直接平移。这也是为什么我一直建议用链表而不是数组在内存只有几 KB 的单片机上链表按需分配比固定数组更省空间边界判断和释放逻辑又能复用同一套。我的习惯是每个游戏工程跑通最小版后先记下三个最容易出问题的点——方向反转、尾巴判定、double free下次新项目开工前翻一遍能少踩好几轮坑。希望这篇基于C语言的经典贪吃蛇游戏设计源码拆解能帮到你动手把这 150 行代码敲一遍比看十遍解析都好使。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。