C语言电梯调度系统项目实战:状态机与SCAN算法从零实现
2026/9/9 6:33:29 网站建设 项目流程

简介:面向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 状态转移的条件设计

我自己用的是最简单的一种——枚举 + switch:

typedef enum { IDLE, MOVING_UP, MOVING_DOWN, DOOR_OPEN } ElevatorState; typedef struct { int currentFloor; ElevatorState state; int targetFloor; int doorTimer; } Elevator;

状态转移的规则是:

  • IDLE状态下,如果发现某个楼层有向上或向下的请求,就把目标楼层设为该楼层,状态变成MOVING_UP或MOVING_DOWN
  • MOVING_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的简化版实现思路是:

  1. 主循环里每次检查“当前方向上是否还有请求”
  2. 如果有,继续前进;如果没有,检查反方向是否有请求,有就掉头
  3. 如果两个方向都没有,回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会一直返回0,inputFloor和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了就不再重复设1:

void 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语言学习里最该练出来的能力。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询