简介这是一份基于C语言实现的外来人员进出监控与管理系统适用于C语言课程设计或期末项目实践。系统分为访客与管理两个端访客可完成申请、入校登记、离校登记管理员可审批申请、增加管理员并查询记录个人与团体访客均有对应处理流程整体逻辑完整覆盖控制台应用的典型增删改查与文件操作。压缩包内共25个文件包含C源文件、头文件、实验报告文档、演示PPT、多张运行截图及说明笔记压缩包大小约1.58MB既能阅读源码与实验报告也能通过截图直观了解运行界面。目前已有128人学习下载适合正在设计同类管理系统或学习C语言文件处理、菜单交互的同学参考。资源附带详细的实验报告和过程截图可帮助读者快速理解系统模块划分、函数调用与数据结构组织方式有效节省课程设计开发与调试时间。1. 用 C 语言做外来人员进出监控先想清楚边界在哪企业园区、工地、学校的访客系统第一反应往往是网页后台加 Excel 报表。但如果部署环境是一台只有串口和按键的采集终端或者是个不能跑数据库的嵌入式板子那 C 语言写一个轻量进出监控程序反而是最可靠的落地方案。这类系统不追求炫酷界面核心就两件事记录每一个外来人员的入场、离场时间以及随时回答“现在有多少人在场、谁超时了”。本文顺着这个需求讲清楚用 C 语言结构体、链表和文件读写实现这套流程的完整做法从数据结构定义到日志落盘再到超时告警和控制台监控页最后给出参数调优和自测方法适合能写出基础 C 程序、但还没系统做过管理类小项目的开发者。2. C 语言结构体、链表和进出状态字段的数据模型设计做一个进出监控系统最容易犯的错误是一上来就写登记和离场逻辑结果数据结构没定好后面查“谁还在场内”时只能一遍遍遍历整个文件。正确顺序是先把内存模型设计明白再考虑文件和界面。2.1 外来人员记录最少需要哪几个字段一个外来人员的完整轨迹至少包含身份信息、来访信息、场内状态三组内容。用 C 语言结构体表达每个字段的取舍要提前想清楚。身份证号、手机号这类字段哪怕全是数字也建议用字符数组而不是 int 或 long long 存储因为读卡器输出的可能带校验位或前缀转成整数会导致前导零丢失和潜在溢出。时间字段统一用time_t表示绝对时间戳不要直接保存 2025-06-20 09:30:00 这种字符串否则离场时长计算要反复解析字符串。字段类型必要性说明card_nochar[24]必选身份证号或访客卡号统一按字符串处理namechar[32]必选姓名中文 UTF-8 编码时单字最长 3 字节companychar[64]可选所属单位用于事后追查contactchar[16]可选被访部门或接待员工号reasonchar[64]可选来访事由便于管理方判断访问合理性enter_timetime_t必选入场时间由登记动作写入leave_timetime_t必选离场时间离场动作写入在场时为 0statusint必选当前进出状态见 2.3 的状态枚举nextstruct visitor*必选链表指针连接下一条记录字段不在多够用就行。company和contact在实际现场可能因为访客不愿填写而留空但字段位置必须预留否则后期加字段要改文件格式和所有加载函数。2.2 用链表承接“随时可能进出”的流动数据进出监控系统最典型的特征就是记录条数不可预见。高峰期一小时可能进来一两百人深夜可能一整个小时没人登记。用固定数组会面临两个麻烦数组开小了要扩容开大了浪费内存。C 语言里链表结构很自然地解决这个问题每次入场登记时malloc一个节点离场或清理时free内存占用与当前记录量成正比。#include stdio.h #include stdlib.h #include string.h #include time.h #define MAX_STAY_SECONDS (2 * 60 * 60) // 默认超时阈值2 小时 typedef struct visitor { char card_no[24]; char name[32]; char company[64]; char contact[16]; char reason[64]; time_t enter_time; time_t leave_time; int status; // 0 在场1 正常离场2 超时离场 struct visitor *next; } visitor_t;链表的插入操作在头部完成O(1) 时间复杂度。查询时从头遍历平均复杂度 O(n)对于单日几千条的校园或园区记录完全够用。内存管理上所有malloc出来的节点必须配对free程序退出前写一个free_all(visitor_t *head)遍历释放整条链避免被系统判定内存泄漏。MAX_STAY_SECONDS用宏定义而不是魔法数字后边调参时只需要改一行。2.3 进出状态用整型枚举不要用字符串标记状态字段是最容易被低估的部分。有人习惯存 in_site、left 这样的字符串读起来直观但每次判断都要strcmp而且一旦拼写不一致比如某条记录写成 leaved整个状态统计就错乱了。C 语言里用枚举常量和整型状态码更合适。enum { STATUS_IN_SITE 0, // 已入场尚未离场 STATUS_LEFT_NORMAL 1, // 正常离场未超时 STATUS_LEFT_TIMEOUT 2 // 离场时已超时 };把状态码定义成 0、1、2 还有个额外好处文件日志里直接存数字体积小、解析快。监控界面展示时再通过一个status_text(int status)函数转换成中文显示逻辑和数据存储解耦。判断超时只需要比较当前时间与入场时间之差是否大于阈值整数减法比字符串时间比较快得多。这个设计在后续查询“当前在场人数”时也方便统计时排除status ! 0的节点即可。3. C 语言文件读写与数据持久化日志追加和启动加载流程内存链表只能服务程序运行期间断电或退出后数据就没了。进出监控系统要求留下可追溯记录所以数据持久化是必不可少的环节这也是 C 语言文件读写操作代码发挥作用的地方。3.1 文本日志与快照文件的分工常见做法是把存储分成两个角色一个追加式文本日志一个按需生成的快照文件。文本日志每发生一次进出动作就追加一行天然支持事后审计快照文件记录当前在场人员的完整状态用于程序重启后恢复现场。二者不要混用否则每次启动重建链表时还要回放全部历史记录数据量大了之后启动会越来越慢。文本日志采用紧凑的管道分隔格式每行一个字段不同字段用|分隔避免使用逗号因为公司名和来访事由里可能出现逗号。常见格式如下IN|card_no|name|company|contact|reason|unix_enter_time OUT|card_no|unix_leave_time|status离场记录只写卡号、离场时间和状态码身份信息在查询时回跳到入场记录里关联。这样日志精简同时保留完整时间线。要注意的是文本日志是追加写不能在中间插入或修改这符合“登记信息不可篡改”的审计需求。3.2 追加写日志的代码实现写入日志用fopen的追加模式打开配合fprintf格式化输出。每次登记和离场动作都调用一次文件句柄用完后立即关闭防止数据停留在缓冲区里没落盘。int append_log(const char *type, const visitor_t *v) { FILE *fp fopen(visitor.log, a); if (fp NULL) { perror(fopen visitor.log); return -1; } if (strcmp(type, IN) 0) { fprintf(fp, IN|%s|%s|%s|%s|%s|%lld\n, v-card_no, v-name, v-company, v-contact, v-reason, (long long)v-enter_time); } else { fprintf(fp, OUT|%s|%lld|%d\n, v-card_no, (long long)v-leave_time, v-status); } fclose(fp); return 0; }这里type参数控制写入场行还是离场行。time_t在 64 位系统上是 long 类型但为了跨平台稳妥格式化时强转long long。fclose之后失败时返回 -1调用方要检查返回值文件写失败意味着审计链路断了最稳妥的做法是同步停止登记操作并告警。3.3 启动时加载日志重建内存链表程序启动时读取日志文件恢复链表。因为入场行和离场行混在同一个文件里加载流程必须分两步第一遍扫描所有IN行把还在场内的记录加入链表第二遍同样扫描根据卡号把匹配的在场记录标记为离场。第二遍只能用“在场状态且卡号匹配”的记录避免早期离场后又再次入场的同人记录被错误关闭。visitor_t *load_from_log(visitor_t *head) { FILE *fp fopen(visitor.log, r); if (fp NULL) return head; char line[512]; while (fgets(line, sizeof(line), fp) ! NULL) { if (strncmp(line, IN|, 3) ! 0) continue; visitor_t *v malloc(sizeof(visitor_t)); if (v NULL) break; memset(v, 0, sizeof(visitor_t)); char enter_str[32]; if (sscanf(line, IN|%23[^|]|%31[^|]|%63[^|]|%15[^|]|%63[^|]|%31s, v-card_no, v-name, v-company, v-contact, v-reason, enter_str) 6) { v-enter_time (time_t)strtoll(enter_str, NULL, 10); v-status STATUS_IN_SITE; v-next head; head v; } else { free(v); // 解析失败的行直接丢弃不中断启动 } } fclose(fp); return head; }代码里的%23[^|]是scanf系列函数的字符集匹配最多读 23 个非管道符字符天然防止card_no溢出。fgets每次读取一行最多 512 字节字段超长的行只截断到缓冲区大小以内。加载完成后链表中所有节点默认为在场状态离场匹配在一个单独的回放函数里处理这里不展开。日志文件里最末尾几行可能因为断电写了一半解析失败时free掉节点并继续保证程序能正常启动。4. 进出登记流程与超时监控界面的实现模型和持久化准备好之后核心就是操作函数。登记进场的函数负责创建节点、写入日志离场函数负责更新时间戳、计算是否超时、更新状态监控界面负责把在场人员实时展示出来。4.1 check_in 与 check_out 两个核心函数进场登记时最关键的检查是防重复入场。同一张卡号如果已经在场内再次登记必须拒绝。查找函数遍历链表比较card_no且状态为STATUS_IN_SITE的记录找到即返回重复错误。int check_in(visitor_t **head, visitor_t *input) { for (visitor_t *cur *head; cur ! NULL; cur cur-next) { if (strcmp(cur-card_no, input-card_no) 0 cur-status STATUS_IN_SITE) { return -1; // 该人员已在场内 } } visitor_t *node malloc(sizeof(visitor_t)); if (node NULL) return -2; memcpy(node, input, sizeof(visitor_t)); node-enter_time time(NULL); node-leave_time 0; node-status STATUS_IN_SITE; node-next *head; *head node; append_log(IN, node); return 0; }input是调用方填好身份信息和来访事由的副本函数内部复制到新节点。memcpy一次复制整个结构体比逐字段赋值简洁但要求结构体里没有指针成员当前定义恰好符合这个约束。返回码 0 表示成功-1 表示重复入场-2 表示内存分配失败调用方需要区分处理。离场函数在时间戳上做超时判断然后把状态写入节点并落盘日志。int check_out(visitor_t *head, const char *card_no) { for (visitor_t *cur head; cur ! NULL; cur cur-next) { if (strcmp(cur-card_no, card_no) 0 cur-status STATUS_IN_SITE) { cur-leave_time time(NULL); if (cur-leave_time - cur-enter_time MAX_STAY_SECONDS) { cur-status STATUS_LEFT_TIMEOUT; } else { cur-status STATUS_LEFT_NORMAL; } append_log(OUT, cur); return cur-status; } } return -1; // 找不到在场记录 }这里MAX_STAY_SECONDS是宏定义的全局阈值默认 7200 秒。注意判断用的是差值大于阈值而不是大于等于边界情况是恰好 2 小时整离场时不算超时。无法找到在场记录时返回 -1可能是卡号输错也可能是重复离场监控页面需要把这类情况单独显示出来不能静默忽略。4.2 超时检测的触发时机与告警输出超时检测不只是离场时算一次账更重要的场景是“这个人该走了但还没走”。监控程序需要周期性地扫描在场记录一旦发现在场时长超过阈值就需要告警。这个扫描不需要高频率每 5 到 10 秒一次足够因为超时告警关心的是分钟级别的事不是秒级。void check_timeout(visitor_t *head, time_t now) { for (visitor_t *cur head; cur ! NULL; cur cur-next) { if (cur-status STATUS_IN_SITE now - cur-enter_time MAX_STAY_SECONDS) { printf([超时告警] %s %s 已在场 %lld 秒超过 %d 秒限额\n, cur-card_no, cur-name, (long long)(now - cur-enter_time), MAX_STAY_SECONDS); } } }now由调用方通过time(NULL)获取后传入而不是函数内部每次重新取时间这样同一个扫描周期内所有超时计算基于同一基准避免边界抖动。这套逻辑运行在一个简单的死循环里读入命令、执行操作、调用check_timeout、刷新界面用sleep()控制节奏。真实生产环境可以考虑把超时检查和命令处理拆到两个线程但单人操作的管理终端完全没必要引入并发复杂度。4.3 控制台监控界面与在场人员排序显示监控界面要回答两个问题现在场内有谁、按入场时间谁该排在前面。链表的自然顺序是入场登记的反序越晚入场越靠前。展示前用 C 语言里常见的冒泡排序或选择排序把在场节点按enter_time升序排列排序只发生在展示时链表结构本身不动。void print_in_site_list(visitor_t *head) { printf(%-8s %-16s %-20s %-12s\n, 卡号, 姓名, 单位, 入场时间); for (visitor_t *cur head; cur ! NULL; cur cur-next) { if (cur-status ! STATUS_IN_SITE) continue; char time_buf[32]; struct tm *tm_info localtime(cur-enter_time); strftime(time_buf, sizeof(time_buf), %Y-%m-%d %H:%M:%S, tm_info); printf(%-8s %-16s %-20s %-12s\n, cur-card_no, cur-name, cur-company, time_buf); } printf(当前在场人数: %d\n, count_in_site(head)); }localtime返回的是静态缓冲区指针多线程环境下不安全但这里单线程用没问题。strftime把时间戳格式化成可读字符串。列表刷新用 ANSI 转义序列清屏printf(\033[2J\033[H)让每次刷新都在同一屏幕位置显示视觉上就是个简单的监控大屏。注意清屏转义在 Windows 控制台不生效如果目标环境是 Windows 需要改用system(cls)跨平台项目通常把这部分抽成一个clear_screen()函数。5. 防崩溃细节、参数调优和内置自测验证最后一章收在实用的进阶细节上包括脏数据处理、可调参数和自测方法。这部分内容直接影响系统在弱网、断电、误操作场景下能不能扛住。5.1 三类输入异常必须拦截第一类是内存分配失败。每次malloc后判断是否为 NULL不能直接解引用。嵌入式环境下堆内存可能只有几十 KB链表一旦膨胀就可能分配失败至少要做到失败时不崩溃、提示后跳过本次登记。第二类是脏日志行。日志文件可能因为断电写了一半可能出现类似IN|card|name|com这样缺少结尾字段的行加载函数要用sscanf返回值判断是否全部解析成功解析失败的行直接跳过并计数最后输出日志提示有多少行被跳过。第三类是重复操作。同一卡号重复入场、未入场的卡号尝试离场都要返回明确错误码界面显示“该人员已在场内”或“找不到在场记录”避免数据错乱。5.2 三个必调参数与调参依据参数名默认值设置依据MAX_STAY_SECONDS7200按来访目的区分工地施工人员可以放宽到 8 小时LOG_LINE_MAX512按字段最长可能长度计算姓名加备注超长也不截断REFRESH_INTERVAL5 秒监控界面刷新频率太密浪费 CPU太疏延误告警MAX_STAY_SECONDS如果按来访目的分档就更实用结构体里加一个allow_duration字段默认取全局值特定事由单独覆盖。LOG_LINE_MAX看起来是缓冲区长度实际上是安全边界所有fgets都按这个长度读取超长行会被截成两段截断部分可能污染下一行此时要判断右括号字符遇到不完整的行直接丢弃。REFRESH_INTERVAL调短可以更快发现超时但如果程序还同时承担登记功能刷新越频繁越容易在输入时打断操作实际部署建议 5 秒以上。5.3 用编译宏内置自测模式最后给一个可以抄走的验证方案通过宏控制编译单元让程序既能在自测模式下运行也能切回正常模式。自测函数里构造几条模拟进出记录调用完check_in和check_out后断言状态和日志行数任何一步失败都返回非零退出码。#ifdef TEST_BUILD int run_all_tests(void) { visitor_t *head NULL; visitor_t v {A001, 测试员, 测试单位, 1001, 设备检修, 0, 0, 0, NULL}; if (check_in(head, v) ! 0) return 1; if (check_in(head, v) ! -1) return 2; // 重复入场应被拒绝 if (check_out(head, A001) ! STATUS_LEFT_NORMAL) return 3; if (check_out(head, A001) ! -1) return 4; // 重复离场应失败 return 0; } int main(void) { return run_all_tests(); } #else int main(void) { /* 正常菜单循环入口 */ } #endif编译时用-DTEST_BUILD即可切换到自测模式日常运行不加这个宏。测试用例覆盖了正常登记、重复入场、正常离场、重复离场四条路径超时场景可以把MAX_STAY_SECONDS临时改小或把enter_time往回拨验证。这个自测体系还能在后续增加“在场人数统计”“历史查询”等功能时持续回归保证新功能不破坏既有行为。真正部署时日志文件的每日切割策略、异常状态记录补录功能也应该是上述这套流程之上的增量优化方向。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?