C++控制台循环链表实现贪吃蛇
发布时间:2026/11/5 5:39:16 锦皓数字建站

前言贪吃蛇的核心数据是蛇身一串按顺序排列的格子头部每次向一个方向增加一格尾部在没吃到食物时删除一格。这个「头插尾删」的模式与链表天然契合又因为蛇头撞到蛇身就游戏结束需要判断新头是否与身体重合把链表接成环形可以让遍历和判重更统一。常见的误解是「循环链表只是为了省一个尾指针」。实际上循环链表在这里的价值是tail-next永远指向头插入新头时不需要单独维护「尾指向空」删除尾时也能从任意节点绕一圈找到前驱。代价是遍历必须用「回到起点」作为终止条件而不能用nullptr一旦写成while (p ! nullptr)就会死循环。另一个误解是「吃到食物只需要不删尾」。不删尾确实让长度加一但新头已经插入尾指针仍指向原来的尾链表结构是对的真正容易错的是「没吃到食物时删尾」的前驱查找以及「新头撞到尾巴」的判定——尾巴马上要移走撞上去应当允许。本文以 C17 为准实现一个只含逻辑层的贪吃蛇循环链表、移动、增长、自撞检测、食物生成。控制台的实时按键属于平台相关部分单独说明。一、为什么用循环链表蛇身需要支持三种操作在头部插入新节点每次移动。在尾部删除节点没吃到食物时。判断某个坐标是否被身体占据自撞检测、食物生成。用std::vector也能做但头部插入是O(n)删除尾部虽然均摊O(1)整体不如链表直接。用单链表非循环时删除尾节点需要找前驱而只有头指针的话要遍历整条链循环链表让尾节点的next指回头配合一个尾指针插入和删除都只差一次前驱查找仍是O(n)但代码结构统一。结构头插尾删判重遍历终止条件std::vectorO(n)O(1)O(n)下标单链表O(1)O(n)找前驱O(n)nullptr循环链表O(1)O(n)找前驱O(n)回到头严格来说若想尾删也O(1)可以用双向循环链表或者额外维护尾节点的前驱。本文选择单向循环链表重点是把指针操作讲清楚对贪吃蛇这种规模O(n)的尾删完全可以接受。二、移动、增长与自撞设当前头节点为head、尾节点为tail新头坐标为(head-x dx, head-y dy)。移动步骤计算新头坐标先做自撞检测。newNode-next head; tail-next newNode; head newNode;把新头接在旧头前面。如果没吃到食物删除旧尾从head出发找到p-next oldTail的节点p令p-next headtail pdelete oldTail。自撞检测从head-next开始遍历回到head停止。若某个节点坐标与新头相同如果没吃到食物且该节点正是tail允许——尾巴这一步会移走新头占据它的位置是合法的。否则判定撞身。增长吃到食物时不执行第 3 步旧尾保留长度自然加一。这里有一个容易忽略的细节自撞检测必须在插入新头之前做。若先插入再检测新头自己也在链表里遍历时必然撞到自己。三、完整实现// 适用于 C17#include cstddef#include iostream#include random#include string#include vectorclass Snake {public:Snake(int x, int y, int length) {head_ new Node{x, y, nullptr};head_-next head_;tail_ head_;for (int i 1; i length; i) {Node* node new Node{x - i, y, head_};tail_-next node;tail_ node;}}~Snake() {if (head_ nullptr) return;Node* p head_-next;while (p ! head_) {Node* next p-next;delete p;p next;}delete head_;}Snake(const Snake) delete;Snake operator(const Snake) delete;bool move(int dx, int dy, bool eat) {const int nx head_-x dx;const int ny head_-y dy;if (hitsBody(nx, ny, eat)) return false;Node* node new Node{nx, ny, head_};tail_-next node;head_ node;if (!eat) {Node* oldTail tail_;Node* p head_;while (p-next ! oldTail) {p p-next;}p-next head_;tail_ p;delete oldTail;}return true;}bool occupies(int x, int y) const {const Node* p head_;do {if (p-x x p-y y) return true;p p-next;} while (p ! head_);return false;}int headX() const { return head_-x; }int headY() const { return head_-y; }private:struct Node {int x;int y;Node* next;};bool hitsBody(int x, int y, bool eat) const {const Node* p head_-next;while (p ! head_) {if (p-x x p-y y) {if (!eat p tail_) return false; // 尾巴会移走允许return true;}p p-next;}return false;}Node* head_ nullptr;Node* tail_ nullptr;};构造函数先建头再逐个把新节点接到尾部next指回head_最后tail_-next自然形成环。析构从head_-next开始删到回到head_最后删头避免边遍历边释放导致的悬垂。move中的顺序值得逐行核对。插入新头后链表是newHead - oldHead - ... - oldTail - newHead。删除旧尾时从newHead出发找p-next oldTail的p由于oldTail-next newHead这个查找一定能在环内完成。若长度为 1p就是newHead令p-next head_后形成自环仍是合法的单节点循环链表。四、游戏循环与渲染// 适用于 C17#include chrono#include threadvoid render(const Snake snake, int width, int height, int fx, int fy) {std::vectorstd::string canvas(height, std::string(width, ));for (int x 0; x width; x) {canvas[0][x] #;canvas[height - 1][x] #;}for (int y 0; y height; y) {canvas[y][0] #;canvas[y][width - 1] #;}canvas[fy][fx] ;canvas[snake.headY()][snake.headX()] O;for (int y 0; y height; y) {std::cout canvas[y] \n;}}int main() {const int width 20;const int height 12;Snake snake(width / 2, height / 2, 3);int fx 5;int fy 5;std::mt19937 rng(std::random_device{}());std::uniform_int_distributionint distX(1, width - 2);std::uniform_int_distributionint distY(1, height - 2);int dx 1;int dy 0;char cmd 0;while (std::cin cmd) {int ndx dx;int ndy dy;if (cmd w) { ndx 0; ndy -1; }else if (cmd s) { ndx 0; ndy 1; }else if (cmd a) { ndx -1; ndy 0; }else if (cmd d) { ndx 1; ndy 0; }else if (cmd q) break;else continue;if (ndx -dx ndy -dy) continue; // 禁止 180 度掉头const int nx snake.headX() ndx;const int ny snake.headY() ndy;if (nx 0 || nx width - 1 || ny 0 || ny height - 1) {std::cout 撞墙游戏结束\n;break;}const bool eat (nx fx ny fy);if (!snake.move(ndx, ndy, eat)) {std::cout 撞到自己游戏结束\n;break;}if (eat) {bool placed false;for (int attempt 0; attempt 200 !placed; attempt) {const int x distX(rng);const int y distY(rng);if (!snake.occupies(x, y)) {fx x;fy y;placed true;}}if (!placed) {std::cout 没有空位放食物胜利\n;break;}}dx ndx;dy ndy;render(snake, width, height, fx, fy);std::this_thread::sleep_for(std::chrono::milliseconds(100));}return 0;}std::mt19937与std::uniform_int_distribution来自random比rand() % n分布更均匀也不依赖全局种子。食物生成最多尝试 200 次若蛇几乎占满地图找不到空位就判定胜利。输入采用「每次读一个字符、回车确认」的模式因此不需要conio.h。若想要不按回车就响应必须使用平台 APIWindows 的_kbhit/_getch在conio.h中POSIX 需要 termios 修改终端模式。二者都不是标准 C移植时要单独封装输入层。常见坑点1. 遍历时用nullptr判断结束❌for (Node* p head_; p ! nullptr; p p-next)循环链表永远不返回空死循环。 ✅ 用p ! head_或do { ... } while (p ! head_)。2. 删尾时找不到前驱❌delete tail_; tail_ tail_-next;删完还访问已释放内存UB。 ✅ 先从新头遍历到p-next oldTail改p-next更新tail_再删。3. 吃到食物也删尾❌ 无条件删尾长度不增长蛇始终原地踏步。 ✅ 用eat分支控制吃到时保留旧尾。4. 自撞检测把尾巴算进去❌ 新头进入尾巴格直接判负但没吃到食物时尾巴本该移走。 ✅ 未吃食物且目标节点是tail_时放行。5. 允许 180 度掉头❌ 蛇向右时输入a新头直接撞上第二节身体。 ✅ 记录当前方向拒绝与当前方向完全相反的方向。6. 食物生成在蛇身上❌ 随机坐标不与蛇身判重食物被身体覆盖永远吃不到。 ✅ 生成后用occupies检查冲突则重试。7. 使用rand() % n生成坐标❌ 模运算带来分布偏差且全局种子影响其他模块。 ✅ 用std::mt19937std::uniform_int_distribution。8. 忘记释放链表❌ 只delete head_其余节点泄漏。 ✅ 析构沿环遍历逐个delete最后删头或改用std::unique_ptr管理。总结操作指针动作注意点构造逐个尾插next指回头保持环闭合移动新节点插在头前先判自撞再插入增长跳过删尾eat为真时保留尾巴缩短找尾前驱改链删尾不能用tail_-next直接前进遍历回到head_停止不要用nullptr析构从head_-next删到head_最后删头循环链表实现贪吃蛇关键是把「环的闭合」贯穿到插入、删除、遍历、析构四个动作里。插入时让tail-next指向新头删除时让前驱指回新头遍历时以回到头为终止析构时沿环走一圈。把这四条写对蛇身的增删就不会出现断链或悬垂剩下的方向控制、食物生成和渲染只是外围逻辑。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。