简介:围绕数据结构课程设计中的旅游区景点管理系统实践,这份项目资料面向正在完成课程设计、需要综合性案例参考的高校学生与编程学习者,重点解决景点信息存储、路线组织、查询排序与预约管理等场景中的数据结构选型问题。内容将链表、数组、树、堆、图、哈希表、排序算法、队列、栈及字符串处理等知识点,对应到景点管理、最优路径、热门推荐、排队预约和撤销回溯等模块,并延伸到程序设计中的设计模式、数据库增删改查与接口调用等工程环节。压缩包约1.37MB,资源页暂未给出有效的文件总数与类型明细,下载后可按实际目录核对。已有788人学习,适合用作课程设计思路参照、模块拆分借鉴和答辩前查漏补缺。
1. 从游客的一句问路说起:旅游区景点管理系统为什么值得用数据结构重做一遍
一位同学拿着打印的园区平面图来问:游客站在东门,问“去索道口怎么走最近”,这套旅游区景点管理系统该怎么回答?这个问题背后是一次带权最短路径求解,并非简单的查表。数据结构课程设计选这个题目,好处在于它能把线性表、图、查找、排序四类结构串成一条完整链路,而不是各写一个小 Demo 交差。景区里的景点是顶点,道路是带权的边,问路是最短路径,道路改扩建参考最小生成树,人气榜是排序,景点档案是线性表加文件持久化。它适合正在做课程设计、需要一份能跑能讲能验收的完整方案的人,也适合想把图算法真正落到业务场景里的开发者。
2. 需求拆解与存储选型:把景点、道路和游览路线压进四种结构
动手写代码之前,先把“谁会来点这个系统”想清楚。景点管理系统的功能不是拍脑袋列出来的,而是从游客和管理员两类角色的操作反推出来的。这一步做扎实,后面的结构选型和模块划分才有依据,答辩时也不至于被问“你这个功能为什么要做”时答不上来。
2.1 从两类角色的操作反推功能清单
游客侧的操作是高频、只读的:查某个景点在哪儿、从当前位置到目标景点的最短路径、半天时间能串起来的一条推荐游览路线、按人气或步行耗时排个榜。管理员侧的操作是低频、可写的:新增或下架景点、修改某条道路的长度和通行状态、临时封闭施工路段、把当前数据存盘、下次启动时重新加载。
把这两侧摊开,功能清单基本就定型了:
| 编号 | 功能 | 依赖的数据结构 | 典型操作 |
|---|---|---|---|
| F1 | 景点信息增删改查 | 顺序表 | 按编号定位、按名称模糊匹配 |
| F2 | 两点间最短路径 | 带权无向图 + Dijkstra | 输入起点终点,输出路径与总长 |
| F3 | 全源路径查询 | 带权无向图 + Floyd | 一次计算,多次查询 |
| F4 | 园区道路规划参考 | 最小生成树 | 输出连通所有景点的最小代价方案 |
| F5 | 连通性检查 | 图的遍历 | 判断是否存在孤立景点 |
| F6 | 景点人气榜 | 排序 | 按人气值降序,同分按停留时长 |
| F7 | 数据持久化 | 文件 | 启动加载、退出保存 |
这里有个容易被忽略的点:F5 连通性检查看起来是“凑数功能”,实际上它是 F2 的前置保障。如果园区图本身不连通,Dijkstra 返回的“不可达”提示会被当成程序 Bug,而不是数据问题。先做连通性检查,能在答辩演示时省掉很多解释成本。
2.2 邻接矩阵还是邻接表:一次基于规模的取舍
教科书上两种存储都讲,但课程设计只能主用一种。判断依据是图的稠密度和顶点规模:一个旅游区的景点数量通常在几十到两百之间,道路数量级也差不多,属于偏稀疏到中等稠密的图。
| 对比项 | 邻接矩阵 | 邻接表 |
|---|---|---|
| 空间开销 | O(n²),n=200 时约 160KB(按 4 字节整型权值算) | O(n+e),更省 |
| 判断两点是否直连 | O(1),直接查下标 | O(度数),需要遍历链表 |
| 枚举某点所有邻居 | O(n),要扫一整行 | O(度数),更精准 |
| 代码复杂度 | 低,二维数组即可 | 中,需要指针或数组模拟链表 |
| 修改一条边的权值 | O(1) | O(度数) |
我一般的做法是主存储用邻接矩阵:景点数量可控,空间完全撑得住,而“判断 A 到 B 有没有直达道路”这个操作在实际交互里调用极频繁(比如输入校验、路线分段展示)。同时保留一份邻接表用于稀疏的步行小径子图,这样课设里两种存储都能体现,讲解时也有对比素材。
提示:如果坚持只用一种结构,选邻接矩阵。它在演示环节的容错性更好——越界、断链这类问题在二维数组里几乎不会出现。
2.3 顶点、边与文件三处的结构体设计
结构体设计是这份方案的地基,改一处会牵动全篇。景点、边、整图三层结构分开定义,好处是序列化和反序列化时边界清晰。
#define MAX_SPOT 100 /* 景点数量上限,按园区实际规模留一倍余量 */ #define INF 0x3f3f3f3f /* 无穷大:10 亿级,两条边相加仍不溢出 int */ /* 顶点:一个景点 */ typedef struct { int id; /* 景点编号,从 0 开始,直接作为矩阵下标 */ char name[32]; /* 景点名称,中文按 UTF-8 存储 */ char type[16]; /* 类型:入口 / 观景 / 服务 / 游乐 */ int popular; /* 人气值,0-100,用于排行榜排序 */ int stay; /* 建议停留分钟数,用于行程时长估算 */ } Spot; /* 边:两点之间的一段道路,无向图只存上三角亦可,这里存满矩阵便于查 */ typedef struct { int dist; /* 道路长度,单位米 */ int walk; /* 步行耗时,单位分钟,-1 表示仅观光车可通行 */ int exist; /* 0 无道路,1 有道路;用独立标志位避免与 dist=0 混淆 */ } Edge; /* 整图:景点表 + 邻接矩阵 + 实际规模 */ typedef struct { Spot spot[MAX_SPOT]; Edge mat[MAX_SPOT][MAX_SPOT]; int n; /* 当前实际景点数量,所有循环的上界 */ } ScenicGraph;几个参数值得单独说。INF取0x3f3f3f3f而不是INT_MAX,是因为 Dijkstra 里会做dist[u] + w的加法,用INT_MAX一加就溢出成负数,松弛判断会全面失效——这是个经典到几乎每届都有人踩的坑。exist单独设一个标志位而不是用dist == 0表示“无路”,是为了给“两个景点重合在同一位置但确实有 0 米连接”这种边界留口子,实际项目里也会这么处理。
文件格式选择纯文本而不是二进制,理由是课设演示需要现场改数据:评委让你“把 3 号到 5 号的路临时封了”,你能直接用记事本改一行再重新加载,比写个隐藏入口方便得多。
# spots.txt 第一段:景点数量与景点表 12 0 东门 入口 88 5 1 观景台 观景 95 30 2 索道口 游乐 76 15 # spots.txt 第二段:边表,每行 起点 终点 长度 步行耗时 0 1 320 6 1 2 180 3读取时先读n,再循环读n行景点,最后读到文件末尾为止读边表,每读一条边同时写入mat[u][v]和mat[v][u],保证无向图对称。这个“读完为止”的写法比在文件头写死边数更抗折腾——加一条路不用回头改计数。
3. 总体架构与模块划分:一个主控加五个模块的职责边界
课程设计最容易出现的架构问题是所有代码堆在main里,main函数三百行,改一个功能要通读全文。合理的做法是把系统切成一个主控加五个功能模块,模块之间只通过结构体指针和少量全局约定通信。
3.1 分层结构与调用关系
整体分三层,从下往上依次是:
数据层:ScenicGraph结构体、LoadFromFile/SaveToFile两个持久化函数、InitGraph初始化函数。这一层不包含任何交互代码,也不打印任何提示语,只负责“数据进、数据出”,保证它可以被单独测试。
算法层:Dijkstra、Floyd、Prim、CheckConnectivity、SortByPopular。这一层的函数签名统一为“输入图指针 + 业务参数,输出结果或错误码”,不直接printf。把输出留给上层,是让算法能被复用的关键——排行榜要复用最短路径结果,如果算法里写死了打印,就没法复用。
交互层:MainMenu主控、SpotModule、PathModule、PlanModule、RankModule、AdminModule五个模块,每个模块一个.c文件配一个.h头文件,对外只暴露一个void XxxMenu(ScenicGraph *g)入口。
模块之间的调用方向是单向的:交互层调算法层,算法层调数据层,反向依赖一律禁止。这条约束在代码量超过两千行之后能救命,改数据结构时只需要自上而下检查一遍。
注意:不要在算法层里调用交互层的确认函数(比如“是否覆盖已有数据 Y/N”)。这类确认属于交互层职责,混进算法层会让单元测试无从下手。
3.2 主控流程与输入容错
主控用最朴素的一层循环加switch,但输入读取必须用fgets加sscanf,不要用裸scanf("%d")。
#include <stdio.h> #include <string.h> #include <stdlib.h> /* 读一行并解析成整数,返回 1 表示解析成功,0 表示输入非法 */ int ReadInt(const char *prompt, int *out) { char buf[64]; printf("%s", prompt); if (fgets(buf, sizeof(buf), stdin) == NULL) return 0; /* sscanf 返回成功赋值的项数;带了换行或字母都会小于 1 */ if (sscanf(buf, "%d", out) != 1) return 0; return 1; } void MainMenu(ScenicGraph *g) { int choice = -1; while (choice != 0) { printf("\n===== 旅游区景点管理系统 =====\n"); printf("1. 景点信息管理\n2. 路径查询\n3. 路线规划\n"); printf("4. 景点人气榜\n5. 数据维护\n0. 保存并退出\n"); if (!ReadInt("请选择:", &choice)) { printf("输入不是数字,请重新输入。\n"); choice = -1; /* 关键:置回非法值,避免沿用上一轮的选择 */ continue; } switch (choice) { case 1: SpotModule(g); break; case 2: PathModule(g); break; case 3: PlanModule(g); break; case 4: RankModule(g); break; case 5: AdminModule(g); break; case 0: SaveToFile(g, "spots.txt"); printf("已保存。\n"); break; default: printf("无此选项。\n"); break; } } }这段代码里有两个细节值得展开。一是fgets把整行读进来再sscanf,用户输入“2abc”时sscanf会匹配到 2,严格来说不算错;但如果用户只敲了个回车,sscanf返回 0,程序能识别出来并提示重输,而裸scanf("%d")在这种情况下会一直卡着等输入,看起来像死循环。二是失败分支里把choice重置成 -1,而不是保留原值——若保留原值,用户输错一次之后按回车会反复执行上一次的功能,这是新手调试时最困惑的现象之一。
SpotModule这类模块入口内部再套一层二级菜单,深拷贝出局部指针对g->spot操作。二级菜单返回时直接return,不要用break跳出内层后又落进外层switch,否则会出现菜单“跳两层”的错觉。
4. 核心算法落地:最短路径、最小生成树和人气榜的代码怎么写
算法部分是这个课设的评分重心,但真正拉开差距的不是算法本身,而是你有没有把算法和业务参数对齐。比如“最近”到底指距离最近还是耗时最短,这个定义不写清楚,代码写对了也讲不明白。
4.1 Dijkstra:先把“最近”定义清楚
园区里的道路有两个权值:长度(米)和步行耗时(分钟)。观光车通行的路段步行耗时记为 -1,表示不可步行。求最短路径时,权重取哪一个,取决于调用场景:游客问“走过去多久”,用耗时做权值;管理端问“两点间直线距离”,用长度做权值。方案里把权值类型做成参数传入,而不是写死。
/* weightSel: 0 用长度 dist,1 用步行耗时 walk;walk 为 -1 的边直接跳过 */ int Dijkstra(const ScenicGraph *g, int src, int dst, int weightSel, int path[], int *pathLen) { int dist[MAX_SPOT], visited[MAX_SPOT], prev[MAX_SPOT]; int i, k; if (src < 0 || src >= g->n || dst < 0 || dst >= g->n) return -2; /* 越界 */ for (i = 0; i < g->n; i++) { dist[i] = INF; visited[i] = 0; prev[i] = -1; } dist[src] = 0; for (k = 0; k < g->n; k++) { /* 第一步:在没有确定最短路的顶点里挑一个 dist 最小的 */ int u = -1, best = INF; for (i = 0; i < g->n; i++) if (!visited[i] && dist[i] < best) { best = dist[i]; u = i; } if (u == -1) break; /* 剩下的都不可达,提前结束 */ visited[u] = 1; if (u == dst) break; /* 只求单点最短路,到点即停 */ /* 第二步:用 u 松弛它的所有邻居 */ for (i = 0; i < g->n; i++) { int w; if (!g->mat[u][i].exist || visited[i]) continue; w = (weightSel == 0) ? g->mat[u][i].dist : g->mat[u][i].walk; if (w < 0) continue; /* 该路段不可步行,视为无边 */ if (dist[u] + w < dist[i]) { dist[i] = dist[u] + w; prev[i] = u; } } } if (dist[dst] == INF) return -1; /* 不连通,交给上层提示“暂无通路” */ /* 第三步:从 dst 沿 prev 回溯,再原地反转 */ *pathLen = 0; for (i = dst; i != -1; i = prev[i]) path[(*pathLen)++] = i; for (i = 0; i < *pathLen / 2; i++) { int t = path[i]; path[i] = path[*pathLen - 1 - i]; path[*pathLen - 1 - i] = t; } return dist[dst]; /* 返回总权值,失败时返回负数 */ }函数返回三态的设计很实用:正数或 0 是路径总权值,-1 是不连通,-2 是参数越界。上层拿到 -1 就打印“两点之间暂无步行通路,建议乘坐观光车”,拿到 -2 就说明是程序内部调用出了问题,提示语完全不同。这种“用返回值区分错误类型,而不是统一返回 -1”的习惯,在后期加功能时能省掉大量排查时间。
复杂度是 O(n²),n 取 100 时一万次比较,毫秒级完成,没有必要上堆优化。真要在答辩时提一句“稀疏图可以换优先队列降到 O((n+e)logn)”,属于加分项,但代码里不必实现。
4.2 Floyd 的取舍:一次算完,换查询 O(1)
如果系统要支持“任意两个景点之间”的路径查询,Dijkstra 每次调用都要重算一遍。方案里把 Floyd 作为一种可选模式:进入路径查询模块时先问一句“本次会话是否批量查询”,是则跑一次 Floyd 把结果存进全局的distMat和nextMat,后续查询直接查表。
int g_dist[MAX_SPOT][MAX_SPOT]; /* Floyd 结果:最短距离 */ int g_next[MAX_SPOT][MAX_SPOT]; /* 路径重建表:i 到 j 的下一跳 */ void Floyd(const ScenicGraph *g, int weightSel) { int i, j, k; for (i = 0; i < g->n; i++) for (j = 0; j < g->n; j++) { if (i == j) { g_dist[i][j] = 0; g_next[i][j] = -1; } else if (g->mat[i][j].exist) { int w = (weightSel == 0) ? g->mat[i][j].dist : g->mat[i][j].walk; g_dist[i][j] = (w < 0) ? INF : w; g_next[i][j] = (w < 0) ? -1 : j; } else { g_dist[i][j] = INF; g_next[i][j] = -1; } } /* 三层循环:k 必须放最外层,这是 Floyd 成立的前提 */ for (k = 0; k < g->n; k++) for (i = 0; i < g->n; i++) for (j = 0; j < g->n; j++) if (g_dist[i][k] + g_dist[k][j] < g_dist[i][j]) { g_dist[i][j] = g_dist[i][k] + g_dist[k][j]; g_next[i][j] = g_next[i][k]; } }k必须在最外层,这是 Floyd 的正确性前提,写在注释里比写在报告里管用——半年后回头看代码,注释能直接拦住“优化”成i在外层的冲动。复杂度 O(n³),n 取 100 是一百万次运算,仍在可接受范围;如果景点数会涨到 500 以上,就老老实实回到 Dijkstra 按需计算,别硬扛。
g_next表是路径重建的关键:查询 i 到 j 的路径时,从 i 开始反复取g_next[cur][j]直到 j,逐点输出即可。相比 Dijkstra 里的prev回溯,next表的逻辑更顺,因为方向是从起点往终点推。
4.3 最小生成树:给园区道路改造做参考
最小生成树在这个系统里的定位要说清楚——它不是“给游客用的”,而是给管理端的道路规划做参考:假设要修一批新路把目前不连通的片区全部打通,最小代价方案是什么样。用 Prim 实现,从入口景点开始逐步扩张。
/* 返回最小生成树总代价,picked 数组保存选中的边,边数写回 edgeCnt */ int Prim(const ScenicGraph *g, int start, int pickedU[], int pickedV[], int *edgeCnt) { int lowcost[MAX_SPOT], from[MAX_SPOT], visited[MAX_SPOT]; int i, k, total = 0; for (i = 0; i < g->n; i++) { lowcost[i] = INF; from[i] = -1; visited[i] = 0; if (g->mat[start][i].exist) { lowcost[i] = g->mat[start][i].dist; from[i] = start; } } lowcost[start] = 0; visited[start] = 1; *edgeCnt = 0; for (k = 1; k < g->n; k++) { int u = -1, best = INF; for (i = 0; i < g->n; i++) if (!visited[i] && lowcost[i] < best) { best = lowcost[i]; u = i; } if (u == -1) break; /* 图不连通,生成森林而非生成树 */ visited[u] = 1; pickedU[*edgeCnt] = from[u]; pickedV[(*edgeCnt)++] = u; total += lowcost[u]; for (i = 0; i < g->n; i++) /* 用新加入的点更新候选边 */ if (!visited[i] && g->mat[u][i].exist && g->mat[u][i].dist < lowcost[i]) { lowcost[i] = g->mat[u][i].dist; from[i] = u; } } if (*edgeCnt != g->n - 1) return -1; /* 边数不足 n-1,说明原图不连通 */ return total; }*edgeCnt != g->n - 1这个判断是 Prim 里最容易被删掉的一行。删掉之后,图不连通时函数依然返回一个正数,管理端看到“最小代价 1200 米”以为方案可行,实际上只连通了一半片区。用返回值 -1 显式告知不连通,比在界面上画个没连上的树要诚实。
4.4 排序与查找:人气榜和按名称检索
排行榜用qsort加自定义比较函数,规则是“人气降序,人气相同按停留时长降序,再相同按编号升序”。第三条是稳定性的保障,避免同一份数据多次排序出现不同顺序,演示时被问到会很难解释。
/* 比较函数:人气高的在前;同人气比停留时长;再同则按编号,保证结果稳定 */ int CmpByPopular(const void *a, const void *b) { const Spot *x = (const Spot *)a; const Spot *y = (const Spot *)b; if (x->popular != y->popular) return y->popular - x->popular; if (x->stay != y->stay) return y->stay - x->stay; return x->id - y->id; }排序前先把g->spot整体拷贝一份到临时数组再排,不要原地排原表。原表的下标就是矩阵下标,一旦顺序被打乱,mat里的边全对不上,整个图就废了。这个坑在后面第 5 章还会展开。
按名称检索用逐条strstr模糊匹配即可,一百来条数据没必要上哈希或字典树。如果要提现“查找”这个知识点,可以在管理员端加一个按编号的哈希索引,但务必先保证编号到下标的一一映射没被破坏。
5. 避坑与排查:这门课设里最容易翻车的五个地方
下面五条都是我在带课设时反复见到的,几乎每一届都有人踩中至少两个。写在这里不是让你事后补救,而是让你在写代码的顺序上就绕开它们。
5.1 排序把人排乱了,图跟着崩
现象:点过一次人气榜之后,路径查询全线报“不可达”,重启程序又恢复正常。
原因:排行榜里对g->spot数组做了原地qsort。而g->mat[i][j]的下标含义是“景点 i 到景点 j”,g->spot一换序,顶点和矩阵的对应关系就断了,等于把整张图的标签撕掉重贴。
解决:所有涉及重排的操作,一律先memcpy到局部数组再排。更彻底的做法是给Spot加一个不随排序变化的index字段,凡是要查矩阵的地方都用index而不是数组下标。这条经验放到任何“对象数组 + 邻接矩阵”的组合里都适用。
5.2 无穷大设小了,或者加法溢出了
现象:明明没有路的两点,Dijkstra 返回了一个很小的距离;或者路径长度算出负数。
原因:INF用了INT_MAX,松弛时dist[u] + w直接溢出;或者INF随手写成 9999,而园区总周长超过一万米,导致“不可达”被当成“距离 9999 米”。
解决:INF取0x3f3f3f3f(约 10.6 亿),保证两个 INF 相加仍在int范围内;同时在松弛前用exist标志位过滤掉无边的情况,不依赖dist的数值去判断连通性。校验方法很简单:造一组完全不连通的数据跑一遍,看输出是不是全是“不可达”。
5.3 文件读取时多读了一行,或者少了最后一行
现象:程序启动后景点数是 0,或者最后一个景点名称是乱码。
原因:fgets读到的字符串末尾带\n,直接拿去strcmp永远匹配不上;另一种情况是文件末尾没有换行符,while (fgets(...) != NULL)循环少跑一次。
解决:读完立即去掉尾部\r和\n,写个小函数统一处理;文件写入时保证最后一行也有换行;加载完成后立刻打印“已加载 n 个景点,m 条道路”,让数据量在界面上一目了然。这个方法看着笨,但每次启动都能第一时间发现文件问题。
5.4 输入缓冲区残留,菜单开始“自己跳”
现象:刚输完一个景点名称,菜单瞬间刷屏好几屏,或者直接跳到退出。
原因:混用了scanf("%d")和fgets。scanf只吃掉数字,把回车留在缓冲区里,紧接着的fgets读到这个空行立刻返回,循环一路空转。
解决:全篇统一用fgets读行 +sscanf解析,禁止出现裸scanf。已经写了一堆scanf的,在每次scanf之后补一个while (getchar() != '\n');清空,但这是权宜之计,从源头上统一读法才是正解。
5.5 只测了理想数据,没测边界
现象:演示一切正常,评委随手输了个不存在的景点编号,程序直接崩溃或者死循环。
原因:测试用例全是“正常路径”。边界情况——编号越界、起点等于终点、图为空、景点数为 1、所有道路都被封闭——一个都没覆盖。
解决:写一份固定的验收用例表(下一章给了一份可直接用的),每次改完代码从头跑一遍。特别是“起点等于终点”这一条,Dijkstra 里dist[src] = 0之后如果没做特判,回溯出来的路径可能只有起点没有终点,路径展示会缺一段。
6. 验收与答辩:用一组测试用例把方案讲圆
代码写完只是完成了一半,剩下的功夫在“怎么证明它能用”。我的习惯是先准备一张固定的验收用例表,每条用例写清楚输入、预期输出和实际输出,改一次代码跑一遍,跑完再上机演示。
| 编号 | 用例 | 输入 | 预期结果 |
|---|---|---|---|
| T1 | 正常最短路径 | 起点 0,终点 5 | 输出完整路径序列与总长,路径首尾正确 |
| T2 | 起点等于终点 | 起点 3,终点 3 | 路径长度为 0,只输出一个景点,不报错 |
| T3 | 不连通 | 封闭 2-4 路段后查 0→4 | 提示“暂无通路”,返回值为 -1 |
| T4 | 编号越界 | 起点 -1 或 200 | 提示编号非法,程序不崩溃 |
| T5 | 空图 | 景点数 0 时查路径 | 提示“暂无景点数据”,不进入算法 |
| T6 | 单景点 | 景点数 1 时跑最小生成树 | 返回 0,边数为 0,不报错 |
| T7 | 输入非数字 | 菜单输 “abc” | 提示重输,不进入任何功能 |
| T8 | 存盘再加载 | 改一条路长度后保存、重启 | 数据与修改后一致 |
| T9 | 排行榜稳定性 | 连续点两次人气榜 | 两次顺序完全一致 |
| T10 | 全封闭 | 所有道路设 exist=0 | 连通性检查报“存在孤立景点” |
答辩讲述顺序建议这样走:先用 T1 演示一遍完整流程,让评委看到从菜单到结果的全链路;再主动抛 T3 和 T4,说明你的程序对异常输入有设计而不是碰运气;最后用 T10 引出连通性检查这个模块,顺势讲一句“最短路径的前提是图连通,所以我把检查放在了算法之前”。这一套下来,评委的问题基本会被你自己提前答掉。
还有个很实用的小技巧:把算法的中间态打印出来。Dijkstra 每轮迭代后打印当前的dist数组和已确定集合,Floyd 打印每一层k结束后的距离矩阵。演示时不看最终结果,先看中间态,评委立刻能判断你是真懂还是背代码。这个开关用一个全局#define DEBUG_TRACE 0控制,正式提交时关掉,答辩时打开。
提示:中间态输出建议写到单独的文件里而不是刷屏。答辩现场屏幕小,刷屏会盖掉菜单,显得手忙脚乱。
从那以后我每次带课设,都会要求学生在交代码之前把这张验收表填满,实际输出一栏一个字都不许空着。填不出来的地方,往往就是还没测过的边界。希望这套结构能帮到你,把这份旅游区景点管理系统的课程设计从头到尾走通一遍,交上去的时候心里有底。
本文还有配套的精品资源,点击获取