
简介面向C语言学习者与并发编程入门者这份“电梯控制模拟程序”以电梯运行为载体完整展示如何用C语言设计多状态、事件驱动的控制系统。程序中包含电梯上下行、停靠、开关门以及乘客呼叫等核心模块作者赵昊伟借助条件变量和线程同步机制让学习者直观理解并发调度与状态转换在实际项目中的落地方式。压缩包共7个文件以cpp源码、头文件为主并附带Visual Studio工程配置和说明文档整体大小约6KB。核心源码呈现了电梯对象的定义、动作实现及请求交互逻辑同时也有预编译头文件供参考便于在VS环境中直接构建与调试。当前已有274人学习浏览。通过阅读和分析这些源码学习者既能巩固C语言编程技巧也能掌握通过条件变量实现同步与通信、设计基础调度算法的方法对培养程序设计和调试能力很有帮助。 先说明一下这个电梯程序我当年也写过后来带过不少学弟学妹做类似的课程设计看到“赵昊伟_C语言学习电梯程序Elevator”这种命名就特别亲切。这基本就是C语言学到指针、结构体、数组之后最常见的综合练习项目——模拟一个电梯的运行逻辑输入楼层请求输出电梯的移动方向和停靠楼层。别看只是个玩具级程序它把状态机、算法设计、输入处理、边界条件这些C语言基本功全串起来了。这个项目非常适合刚学完C语言语法、想找个综合练习的人也适合准备计算机二级或者嵌入式入门的朋友。很多做单片机、PLC的人也应该熟悉这套逻辑——电梯调度本质上就是个简化的任务调度系统搞懂了它对后续学RTOS、学工控都有帮助。这篇文章我就按自己复盘这个项目的思路从需求拆解到代码实现再到踩坑记录完整地过一遍。1. 这个项目到底在练什么1.1 核心需求解析先把需求说清楚。一般教材或OJ上的电梯程序不会要求你把电梯真的画出来而是要求你用控制台模拟电梯的行为。常见的要求是电梯初始在一楼门是关的乘客在某个楼层按下按钮向上或向下电梯根据当前楼层和目标楼层决定向上还是向下移动每次移动一层需要一定时间通常模拟为延迟或步数到达目标楼层后开门乘客进出然后关门等待下一个请求看起来很简单对吧但真正动手写的时候你会发现难点不在“移动一层”这个动作而在“决定下一步去哪”这个决策过程。1.2 涉及的知识点全景我一个一个数一下这项目里藏着的知识点数组楼层状态、按钮状态、乘客请求队列都要用数组存结构体用结构体描述电梯当前状态当前楼层、运行方向、目标楼层、门状态状态机电梯不是“正在动”就是“停着”怎么从一种状态迁移到另一种状态这是核心逻辑循环与条件判断主循环里不断扫描请求、更新状态指针可选如果有多个电梯用结构体数组或者指针来管理每个电梯输入处理scanf的格式控制、清空输入缓冲区模块化设计拆成电梯移动函数、按钮扫描函数、调度决策函数等这些点单独拿出来每个都不难但凑在一起很多人写着写着就变成一坨绕来绕去的if-else最后自己都看不懂。所以写之前一定要先设计别急着敲代码。2. 整体设计与状态机拆解2.1 为什么必须用状态机我当时犯过最大的错就是一上来就写逻辑电梯向上走每走一层检查一次有没有人要下楼如果有就停下来开门——结果楼层的请求一多逻辑就开始打架电梯刚往上走又因为一个“向下”的请求掉头乘客在电梯里疯狂怀疑人生。后来我意识到电梯本质上是一个离散状态系统。它的行为可以用几个状态描述清楚停止IDLE电梯停着门关上没有请求运行中MOVING电梯朝某个方向移动开门DOOR_OPEN电梯到达目标楼层门打开乘客进出待处理WAITING有请求但电梯还没决策要去哪或者正在等待新请求状态机的好处是你永远知道电梯当前在干什么下一步只有几个合法选择。不可能出现“又开门又在移动”这种矛盾状态。2.2 状态转移的条件设计我自己用的是最简单的一种——枚举 switchtypedef enum { IDLE, MOVING_UP, MOVING_DOWN, DOOR_OPEN } ElevatorState; typedef struct { int currentFloor; ElevatorState state; int targetFloor; int doorTimer; } Elevator;状态转移的规则是IDLE状态下如果发现某个楼层有向上或向下的请求就把目标楼层设为该楼层状态变成MOVING_UP或MOVING_DOWNMOVING_UP状态下每次向上走一层如果到达了某一层发现这一层有请求而且请求方向和当前移动方向相同或者这一层就是目标楼层就停车开门MOVING_DOWN逻辑同理DOOR_OPEN状态下门保持打开若干时间模拟乘客进出然后关门状态回到IDLE此时再检查还有没有其他请求这套状态机的关键在于电梯只在自己移动方向上顺路接客不顺路的请求先记着等电梯掉头回来再接。这也是真实电梯的调度原则之一写出来之后代码会清晰很多。2.3 数据结构的选型很多初学者喜欢用一堆零散变量int up1, up2, up3……10层楼就要写10个变量20层就崩溃了。正经做法是用数组#define MAX_FLOOR 10 int requestUp[MAX_FLOOR 1]; // 每层是否有向上请求 int requestDown[MAX_FLOOR 1]; // 每层是否有向下请求 int requestInside[MAX_FLOOR 1]; // 电梯内部乘客按的目标楼层数组下标就是楼层号1表示有请求0表示没有。这样不管是判断“当前楼层是否有人要上”还是“更高的楼层有没有请求”都只需要一个循环搞定。3. 核心算法与调度策略3.1 三种常见调度方式对比电梯调度的核心问题就是有多个楼层都有请求电梯先接谁我见过常见的三种策略策略思路优点缺点先来先服务FCFS按请求产生顺序逐个处理实现简单逻辑清晰电梯可能来回乱跑效率低扫描算法SCAN电梯固定沿一个方向移动扫到有请求的楼层就停直到最顶层再掉头效率高逻辑较简单需要考虑方向变更时机最短路径优先SSTF每次选离当前楼层最近的请求平均等待时间短可能造成“饿死”远端长时间无人服务逻辑较复杂我的建议是学习阶段先从FCFS下手然后升级到SCAN。FCFS能让你快速跑通整个流程SCAN能让你体会到“调度策略”的乐趣。直接上SSTF的人我见多了写着写着就把自己绕晕了。3.2 SCAN算法的简化实现思路SCAN的简化版实现思路是主循环里每次检查“当前方向上是否还有请求”如果有继续前进如果没有检查反方向是否有请求有就掉头如果两个方向都没有回IDLE伪代码大概是while (1) { if (elevator.state IDLE) { // 扫描所有楼层找到第一个有请求的楼层设为目标 for (int i 1; i MAX_FLOOR; i) { if (requestUp[i] || requestDown[i] || requestInside[i]) { elevator.targetFloor i; elevator.state (i elevator.currentFloor) ? MOVING_UP : MOVING_DOWN; break; } } } else if (elevator.state MOVING_UP) { // 先判断当前层需不需要停再判断要不要继续向上 if (needStopAtCurrentFloor(elevator)) { openDoor(elevator); continue; } // 检查更高楼层是否还有同向请求没有就掉头 if (!hasRequestAbove(elevator.currentFloor)) { elevator.state MOVING_DOWN; continue; } elevator.currentFloor; printf(电梯到达第 %d 层\n, elevator.currentFloor); } // MOVING_DOWN 类似 }很多人会很好奇为什么要先判断当前层要不要停再决定掉不掉头因为如果当前层有人按了向上的按钮而电梯正在向上走不停车直接走就是重大bug——这跟现实中坐电梯是一样的顺路必须停。3.3 关于“捎带”的处理SCAN算法里最容易漏掉的是“捎带”逻辑。举个例子电梯在3楼正在向上走目标是8楼。5楼有人按下“向上”按钮电梯正好经过5楼应该停下来让乘客上。但如果5楼按的是“向下”电梯就不应该停——因为电梯正在向上停下来接一个向下走的人方向冲突会让电梯来回折腾。判断顺路的方法很简单int isSameDirection(int floor) { if (elevator.state MOVING_UP) { return requestUp[floor] || requestInside[floor]; } else if (elevator.state MOVING_DOWN) { return requestDown[floor] || requestInside[floor]; } return 0; }这个函数看起来简单但实际写代码的时候很多人会漏掉 requestInside——电梯内部乘客按的目的楼层也算“当前方向上的需求”否则就会出现“有人在电梯里按了8楼电梯却在6楼就转头向下”的诡异情况。4. 关键实操环节与代码实现4.1 主循环与输入处理主循环是整个程序的骨架推荐用这种结构int main() { Elevator elevator {1, IDLE, 1, 0}; int inputFloor, inputDir; initRequests(); while (1) { // 1. 处理用户输入 printf(请输入请求楼层 方向方向1上 -1下 0退出); if (scanf(%d %d, inputFloor, inputDir) ! 2) { // 处理非法输入 while (getchar() ! \n); continue; } if (inputFloor 0 || inputDir 0) { break; } if (inputFloor 1 || inputFloor MAX_FLOOR) { printf(楼层无效\n); continue; } setRequest(inputFloor, inputDir); // 2. 运行电梯逻辑 stepElevator(elevator); // 3. 打印当前状态 printStatus(elevator); } return 0; }这里有个细节值得注意scanf的返回值一定要判断。如果不判断用户输入一个字母“q”之类的东西scanf会一直返回0inputFloor和inputDir就是未初始化的垃圾值程序就开始发疯了。用while(getchar() ! \n)清空输入缓冲区这样才能把错误输入完全清掉否则下一次scanf还是读到同样的坏数据形成死循环。4.2 电梯移动与时间模拟电梯“移动一层”的模拟最简单的方案是打印信息 sleep#include unistd.h // Linux 下用这个 void moveOneFloor(Elevator *e) { if (e-state MOVING_UP) { e-currentFloor; } else if (e-state MOVING_DOWN) { e-currentFloor--; } printf(电梯运行中当前到达 %d 楼\n, e-currentFloor); usleep(500000); // 模拟电梯移动一层耗时 0.5 秒 }Windows下的头文件是windows.h用Sleep(500)注意大写S。这个延时有个好处让程序看起来像真的电梯在动调试的时候也方便观察状态变化过程。如果编译环境不支持sleep也可以用空循环计数代替但不推荐因为空循环的时间不好控制。4.3 到站停车和按键去抖的模拟停站逻辑要解决一个常见问题电梯到了某一层按钮的“请求状态”什么时候清零我见过有人写的是“电梯一到达就直接清空该层的所有请求”这其实不对。正确做法是开门之前只清除与当前方向一致的请求开门后内部乘客按的“到达目标楼层”的请求也全部清除反向的请求保留等电梯掉头回来再接另外真实电梯里按钮是按一下触发一次如果一直按住会重复触发。控制台程序里可以用一个简单的“去抖”思路每层用一个标志位已经是1了就不再重复设1void setRequest(int floor, int dir) { if (dir 1) { requestUp[floor] 1; } else if (dir -1) { requestDown[floor] 1; } }这样电梯到站后把对应位清零下次再按才能重新触发。如果不清零或重复置1按钮状态就会一直“粘住”电梯就会反复在这一层开门实际运行起来会非常奇怪。4.4 打印调试日志的小技巧调试这种状态机程序最好的方式就是“每一步都打印日志”。我最开始写电梯程序时喜欢用printf把每个关键状态打出来后来发现太乱了就把日志封装成一个函数void logStatus(Elevator *e, const char *event) { printf([%s] 当前楼层%d, 状态%d, 目标%d\n, event, e-currentFloor, e-state, e-targetFloor); }在状态转移的关键节点调用 logStatus(elevator, 开门)、logStatus(elevator, 到达) 等等这样跑一遍程序哪里逻辑出了问题一目了然——比如明明目标是8楼结果电梯到9楼还没停那就能定位到是“停车判断”的条件漏了。5. 常见问题与排查技巧实录5.1 电梯卡死状态永远停在MOVING遇到最多的问题就是电梯一直往上走走到顶楼还在走或者一直显示“电梯运行中”却不响应任何输入。排查思路检查状态转移条件在MOVING_UP分支里如果“是否继续向上”的判断条件写成了永远为真就会死循环。我用过hasRequestAbove函数里面有一步是“从当前楼层1到MAX_FLOOR循环”忘记过滤掉当前楼层的那一层请求了结果电梯明明已经到达目标楼层循环里还查得到自己所在楼层的请求就一直不转头检查目标楼层的赋值IDLE状态下找目标楼层时如果循环里找的是“第一个非零请求”但目标楼层没有及时更新那么电梯的currentFloor永远追不上targetFloor也会卡死5.2 电梯向上却开向下的门逻辑bug里最搞笑的一种。我调试的时候遇到过电梯明明是向上走的到了5楼停下来了结果“门开了显示的是下行开门”。排查了很久发现是我把门状态写成了固定的没有跟方向关联。实际上电梯到站停车只需要判断“当前楼层是否有同向请求”开门的时候只需要打印“开门”跟方向没有直接关系——但我当时两个if写重了最后修掉重复判断就好了。遇到这种问题最好的办法是删掉复杂逻辑只用一个入口函数openDoor(Elevator *e)把开门、定时、关门做成一个独立的小函数确保所有停车路径都走同一个门控制流程。5.3 编译报错功能未声明我见过很多新手的报错是“undefined reference to XXX”或者“implicit declaration of function”。大部分原因是一个函数里直接调用了另一个还没定义的函数但函数原型函数声明没写在文件前面。C语言的编译是从上到下的调用前必须先声明。解决方案在文件头部统一声明所有函数或者把函数实现按调用顺序排列或者用头文件.h管理声明我个人的习惯是写一个elevator.h把所有的结构体定义、函数声明放在里面main.c只负责#include和主循环逻辑。这样不仅编译清爽后面想加功能也方便。5.4 调试时输入被“吞掉”另一个高频问题用户输入“5 1”之后电梯动了一下但接下来的输入还没敲电梯就已经自己走了好几层。原因是程序的主循环里每次循环都先处理输入再运行逻辑没有要求“等待用户输入下一组指令”。严格来说这不是bug是模拟逻辑的问题。如果你希望电梯自己在没有输入的情况下也运行自动响应之前的请求就要把主循环改成非阻塞检测输入用kbhit或select或者每循环一次延时让电梯先走完一个时间步再检测输入如果只是课程设计建议用阻塞式输入就够了跑一步停一步人肉控制每一步逻辑清晰。跑通之后再考虑自动运行。// 一个简单的非阻塞按键检测Linux下可用 #include termios.h #include unistd.h #include fcntl.h int kbhit(void) { struct termios oldt, newt; int ch; int oldf; tcgetattr(STDIN_FILENO, oldt); newt oldt; newt.c_lflag ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, newt); oldf fcntl(STDIN_FILENO, F_GETFL, 0); fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK); ch getchar(); tcsetattr(STDIN_FILENO, TCSANOW, oldt); fcntl(STDIN_FILENO, F_SETFL, oldf); if (ch ! EOF) { ungetc(ch, stdin); return 1; } return 0; }这段代码来自Unix经典的终端非阻塞实现我在Linux课程设计里实测过效果稳定。Windows下面就没有这么简洁了但一般课程设计都用Linux环境够用。5.5 楼层边界与数组越界这里的坑非常隐蔽。假如MAX_FLOOR是10数组定义为int request[11]下标0到10都能用但你输入“11楼”的时候程序不会崩溃——因为它访问的是紧挨着数组后面的一块内存恰好那块内存里可能是0也可能是个垃圾值。垃圾值是1的话电梯就会往根本不存在的11楼走。我的建议所有数组定义都多开一位MAX_FLOOR 1因为楼层号从1开始所有输入都做范围校验超出直接拒绝所有循环里凡是碰到底层边界都用“if (floor 1 floor MAX_FLOOR)”包一层另外如果你用了枚举状态不要用数字魔法值做判断尽量用枚举常量去比较。因为数字改起来容易漏比如你插了一个新状态后面的数字全变了原来写死“if (state 3)”的代码就会莫名其妙出错。代码可读性也会好很多。5.6 浮点数比较的隐藏问题这个项目理论上用不到浮点数但有的同学喜欢用float记录“电梯是否到达某层”或者在UI上显示位置百分比。如果真这么做会碰到典型的C语言浮点数比较问题0.1 0.2 ! 0.3。你写float pos 0.0; pos 0.1; if (pos 0.3) { // 永远不成立 }因为浮点数的二进制近似存储比较相等是个坑。解决办法就是别用浮点数做楼层定位直接用整数作为楼层号。只要记住楼层号是离散的别为楼层引入浮点位置。电梯的物理上被拉长、拉短、加加速度这些在这个学习项目里根本不需要纯属给自己找事。6. 项目扩展与后续思路写到这里这个电梯程序基本已经完整了。但对于想拿高分或者真正想练深一点的人我再多分享几个扩展方向多电梯调度把电梯结构体变成一个数组调度策略从“单梯扫描”变成“多梯协调”需要考虑怎么分配请求比如就近原则、分区原则。这个能让你更深刻理解任务分配的问题。动态生成请求写一个简单随机数生成器每隔几秒自动产生一个乘客请求让程序自己跑起来观察电梯调度效果可视化界面初学者可以考虑先用文本画个“电梯剖面图”用printf每层打印#号和电梯位置有基础了可以上图形库或者网页前端做个实时动画效果会很震撼算法性能统计记录乘客平均等待时间、最长等待时间用数据来判断不同调度策略的好坏——这个思路对以后做嵌入式、做系统设计都很有用我个人特别喜欢把电梯调度比作操作系统的进程调度每个乘客就是进程请求楼层就是I/O事件电梯就是CPU调度算法就是进程调度算法。学完电梯程序回头再看操作系统的PV操作、时间片轮转会觉得顺畅很多。如果你正在写这个课程设计我建议你沉下心先把状态机画清楚再动手写代码。读代码和写代码完全是两回事这个项目最大的价值不在于“做出来”在于你在设计过程中逼自己想清楚每一步该干什么为什么这么干。这才是C语言学习里最该练出来的能力。本文还有配套的精品资源点击获取