ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

操作系统课设六大实验工程化实现与二次开发指南

操作系统课设六大实验工程化实现与二次开发指南

简介:面向计算机科学专业学生与操作系统课程学习者,这份东北大学操作系统课设资料围绕进程管理、内存管理、文件系统与设备管理等核心模块展开,适合用于课程设计参考、实验复现与原理梳理。压缩包共24个文件,以21个cpp源码为主体,配有头文件、可执行程序和docx课程设计报告,整体约2.11MB,目录按文件系统功能拆分,便于对照阅读实现思路。现有280人学习,说明其具备一定的参考价值。读者可从中了解文件创建、读写、删除、目录维护、数据块分配与回收等功能的代码组织方式,也可借此理解调度算法、页面替换、中断处理与I/O调度等知识点在代码中的落点,并结合报告梳理设计流程、调试要点与系统调用实践,为完成同类操作系统课设提供较完整的功能实现框架和排错思路。

1. 一份能跑通六个实验的操作系统课设,比想象中更能说明问题

期末季的课设答辩现场,画面往往差不多:同学把程序跑起来,屏幕上打出一排进程的甘特图,导师追问一句“时间片改成 2 会怎样”,于是现场改常量、重新编译、再跑一遍。这份计科方向的操作系统课程设计就是按这个节奏组织的——进程调度、生产者消费者同步、银行家算法、页面置换、磁盘调度,每个实验编成一个独立可执行文件,共用同一套 PCB 结构和输出函数。

它不适合拿来当教学视频看,适合的是“明天要交、后天要答辩”的开发者:代码结构直白,输入靠命令行和数据文件,输出能直接截图进报告,修改算法只需要替换一个比较函数。下面从目录结构一路拆到二次开发,讲清楚怎么在本机跑通,以及哪几个地方最容易翻车。

2. 工程骨架拆解:四层目录如何装下六个独立实验

2.1 目录布局与 Makefile:一个实验一个二进制

六个实验各写一份 PCB、各写一份打印函数的做法,在第一次改字段时就会付出代价——加一个remain字段要同步改六处,漏一处就是编译错误。合格的做法是先立公共层,再让各实验只保留自己的算法逻辑,目录大致如下。

os-coursedesign/ ├── Makefile ├── include/ │ ├── pcb.h # PCB 结构、状态枚举、调度函数声明 │ ├── mem.h # 页表项、物理块、银行家算法用的矩阵上限 │ └── util.h # 打印、输入解析、计时工具 ├── src/ │ ├── common/ │ │ ├── util.c # print_gantt / print_metrics / load_pcb │ │ └── queue.c # 通用链表队列,就绪队列和阻塞队列共用 │ ├── sched/ │ │ ├── main.c # 菜单入口,把四个算法串起来 │ │ ├── fcfs.c │ │ ├── sjf.c │ │ ├── prio.c │ │ └── rr.c │ ├── sync/producer_consumer.c │ ├── banker/banker.c │ ├── page/page_replace.c │ └── disk/disk_sched.c ├── data/ │ ├── sched1.txt # 进程到达时间、运行时间、优先级 │ └── page1.txt # 页面引用串 └── bin/ # make 产物,不提交到版本库

include放声明、src放实现、data放测试数据、bin放产物,这个划分不是为了好看,是为了让每次编译只重新编译改动的那个.c。Makefile 里把公共源文件抽成变量,是避免漏链接的关键。

CC := gcc CFLAGS := -Wall -Wextra -g -Iinclude -std=c11 COMMON := src/common/util.c src/common/queue.c # 一个实验一个可执行文件,改哪个编哪个 all: bin/sched bin/sync bin/banker bin/page bin/disk bin/sched: $(COMMON) src/sched/main.c src/sched/fcfs.c \ src/sched/sjf.c src/sched/prio.c src/sched/rr.c @mkdir -p bin $(CC) $(CFLAGS) $^ -o $@ bin/banker: $(COMMON) src/banker/banker.c @mkdir -p bin $(CC) $(CFLAGS) $^ -o $@ clean: rm -rf bin

$^是全部依赖文件列表,$@是目标名,两个自动变量把“公共文件 + 私有文件”拼成一条编译命令,不用手写一长串路径。-Iinclude让源码里可以直接写#include "pcb.h"而不必写相对路径;-Wall -Wextra建议一直开着,课设里最常见的空指针解引用和未使用参数,编译器提前就指出来了。-g是给 gdb 留的,调度结果算错的时候单步进去看now和remain比盯着屏幕猜快得多。

提示:-Wextra会把main(int argc, char **argv)里没用到的argv报成 warning,课设报告里如果要求“零警告”,加一行(void)argv;即可,不要直接关掉这个选项。

2.2 公共数据结构:PCB 是所有实验的公共语言

调度要用 PCB,同步实验里的生产者消费者也要用 PCB 表示阻塞进程,输出函数要按 PCB 的字段算周转时间。所以pcb.h是这套代码里最不能随便改的文件,字段一次定全,后面六个实验都省事。

#ifndef PCB_H #define PCB_H typedef enum { READY, RUNNING, BLOCKED, DONE } pcb_state; typedef struct pcb { int pid; /* 进程号,从 1 开始,输出甘特图用 */ char name[16]; /* 显示名,报告里比 pid 直观 */ int arrive; /* 到达时间 */ int burst; /* 需要运行的总时间,算周转时间用 */ int remain; /* 剩余运行时间,只有时间片轮转会改它 */ int prio; /* 优先级,约定数值越小优先级越高 */ int start; /* 首次开始运行时刻,-1 表示还没开始 */ int finish; /* 完成时刻 */ int wait; /* 已累计等待时间,可用于优先级老化 */ pcb_state state; struct pcb *next; /* 挂到队列上时用 */ } PCB; #endif

几个字段的取舍值得说明。burst和remain分开存,是因为非抢占式调度直接now += burst就完事,而时间片轮转必须每次减掉一个时间片,如果把remain减在burst上,后面算周转时间就没基准了。start初始化为-1,第一次被调度时才赋now,这样算“等待时间 = start - arrive”不会把还没跑的进程算进去。wait字段在基础版里其实用不上,加它是为了做优先级老化——等待越久的进程优先级数值越小,否则低优先级进程在纯优先级调度里会饿死,答辩时经常被问这一句。

2.3 输出层:甘特图和指标表必须是两个独立函数

六个实验的报告截图高度雷同:一张横条的调度顺序图,一张平均周转时间、平均带权周转时间的表。把这两件事抽成print_gantt和print_metrics放进util.c,后面每个实验只要把自己的结果数组传进去。

/* 打印文本甘特图:seq 是执行顺序的 pid,dur 是每段持续时间 */ void print_gantt(const int *seq, const int *dur, int n) { printf("\n|"); for (int i = 0; i < n; i++) printf(" P%-3d|", seq[i]); printf("\n0"); int t = 0; for (int i = 0; i < n; i++) { t += dur[i]; printf("%5d", t); /* 每个块的右边界就是累计时间 */ } printf("\n"); } /* 计算并打印平均周转时间、平均带权周转时间 */ void print_metrics(PCB *ps, int n) { double sum_turn = 0, sum_wturn = 0; for (int i = 0; i < n; i++) { int turn = ps[i].finish - ps[i].arrive; /* 周转时间 */ sum_turn += turn; sum_wturn += (double)turn / ps[i].burst; /* 带权周转时间 */ } printf("平均周转时间 : %.2f\n", sum_turn / n); printf("平均带权周转时间 : %.2f\n", sum_wturn / n); }

print_gantt用累计时间做右边界,dur由调用方保证和执行顺序一一对应,这样时间片轮转里同一个进程出现多次也能正确画出多段。print_metrics里带权周转时间要强制转成double,turn / ps[i].burst如果两个都是int,小数部分会被直接截掉,平均下来数值明显偏小,这个坑几乎每届都有人踩。指标函数只读finish、arrive、burst三个字段,所以六个实验可以共用一份,不用为每个算法单独写统计。

3. 进程调度与同步:四个算法共用一条主循环

3.1 通用调度骨架:先选进程,再推进时间

非抢占式的三个算法(先来先服务、短作业优先、优先级)在结构上完全一致,差别只在“从已到达的进程里挑哪一个”。把挑选逻辑做成函数指针,主循环只写一遍。

typedef int (*cmp_fn)(const PCB *, const PCB *); /* 返回负值表示 a 更优先 */ void run_sched(PCB *ps, int n, cmp_fn cmp) { int now = 0, done = 0; int g_pid[MAX_PROC], g_dur[MAX_PROC], g = 0; while (done < n) { PCB *pick = NULL; /* 1. 在已到达且未完成的进程里,按 cmp 选最优的一个 */ for (int i = 0; i < n; i++) { if (ps[i].arrive <= now && ps[i].state != DONE) { if (!pick || cmp(&ps[i], pick) < 0) pick = &ps[i]; } } if (!pick) { now++; continue; } /* 当前时刻无进程就绪,CPU 空转 */ /* 2. 非抢占式:一口气跑到结束 */ if (ps[i].start < 0) pick->start = now; now += pick->burst; pick->finish = now; pick->state = DONE; g_pid[g] = pick->pid; g_dur[g] = pick->burst; g++; done++; } print_gantt(g_pid, g_dur, g); print_metrics(ps, n); }

这段骨架有三个参数要知道怎么改:cmp决定算法,MAX_PROC决定进程数上限(课设一般 10 到 20 够用,别用变长数组,老版本 gcc 下会翻车),now++那行处理的是进程到达时间存在空档的情况——比如第一个进程 3 时刻才到,now必须从 0 走到 3,否则周转时间会算少。start < 0判断保证只有首次运行时才记录开始时刻,这一点在抢占式调度里尤其重要。

3.2 四个算法只差一个比较函数

/* 先来先服务:到达早的优先,到达相同则 pid 小的优先 */ int cmp_fcfs(const PCB *a, const PCB *b) { if (a->arrive != b->arrive) return a->arrive - b->arrive; return a->pid - b->pid; } /* 短作业优先:运行时间短的优先 */ int cmp_sjf(const PCB *a, const PCB *b) { return a->burst - b->burst; } /* 优先级调度:数值小的优先,加 wait 就变成老化版本 */ int cmp_prio(const PCB *a, const PCB *b) { return a->prio - b->prio; }

三个函数都是一行减法,返回负值就是“a 应该先跑”。这种写法比返回bool好在不会因为相等情况乱序——相等返回 0,主循环里的cmp(...) < 0就不替换,选中的是数组里靠前的那个,结果稳定可复现。做优先级老化时把cmp_prio改成return (a->prio - a->wait / 5) - (b->prio - b->wait / 5);,每等 5 个时间单位优先级提升一级,答辩演示时能直观看出饥饿被缓解。

时间片轮转没法塞进这套骨架,因为它会在一个进程没跑完时切走,得单独写。

/* 时间片轮转:q 为时间片长度,remain 初值等于 burst */ void run_rr(PCB *ps, int n, int q) { int now = 0, done = 0; while (done < n) { int idle = 1; for (int i = 0; i < n; i++) { if (ps[i].arrive > now || ps[i].state == DONE) continue; idle = 0; if (ps[i].start < 0) ps[i].start = now; int slice = ps[i].remain < q ? ps[i].remain : q; /* 最后一片可能不足 */ now += slice; ps[i].remain -= slice; if (ps[i].remain == 0) { /* 正好跑完 */ ps[i].finish = now; ps[i].state = DONE; done++; } } if (idle) now++; /* 一轮下来没人可跑,时间推进 */ } }

slice取remain和q的较小值,是为了让最后一片不必填满整个时间片,否则甘特图的总长度会凭空多出一截,周转时间也跟着变大。这个写法按数组下标顺序轮转,而不是按到达顺序维护真正的就绪队列,在“所有进程同时到达”的课设数据下结果一致;但如果到达时间分散,严格来说应该先把新到达的进程入队再出队,差异在答辩追问时是会被抓住的。

3.3 生产者消费者:手搓一个精简信号量

同步实验要求用 P、V 操作实现有界缓冲区的互斥与同步。Linux 上可以直接用sem_t,但课设报告通常要求把 P、V 的逻辑写出来,所以一般自己定义一个最小结构。

typedef struct { int value; /* 大于 0 表示还有可用资源,小于 0 的绝对值表示阻塞进程数 */ int blocked; /* 仅用于统计,打印阻塞队列长度 */ } mysem; void P(mysem *s) /* 申请资源:先减再判 */ { s->value--; if (s->value < 0) s->blocked++; /* 简化版:只记数,不真的挂起线程 */ } void V(mysem *s) /* 释放资源:先加再判 */ { s->value++; if (s->value <= 0) s->blocked--; /* 唤醒一个阻塞进程 */ }

P里必须先减后判,V里必须先加后判,顺序反过来语义就错了:如果先判再减,两个进程可能同时判断“还有空位”然后一起进去,互斥失效。这个细节是同步实验的核心考点,写反了在单线程模拟里看不出来,但报告里的伪代码会被逐行检查。

#define N 5 int buf[N], in = 0, out = 0; mysem empty = { N, 0 }; /* 空位数 */ mysem full = { 0, 0 }; /* 已放产品数 */ mysem mutex = { 1, 0 }; /* 缓冲区互斥锁 */ void producer(int item) { P(&empty); /* 有空位才能放,注意顺序:先资源信号量再互斥量 */ P(&mutex); buf[in] = item; in = (in + 1) % N; V(&mutex); V(&full); } void consumer(int *item) { P(&full); P(&mutex); *item = buf[out]; out = (out + 1) % N; V(&mutex); V(&empty); }

P(&empty)必须写在P(&mutex)前面,这是一个经典死锁点:如果先拿互斥锁再等空位,缓冲区满的时候生产者会抱着锁一直等,消费者永远进不来,整个系统卡死。把这条写进报告的“死锁分析”小节,比单纯贴代码更能说明理解到位。

4. 内存与存储:银行家算法、页面置换、磁盘调度

4.1 银行家算法:安全性检查写成独立函数

银行家算法分两段:请求合法性检查(请求是否超过声明最大值、是否超过当前可用),以及安全性检查(试分配后能否找到一个安全序列)。把安全性检查拆成独立函数,主流程只负责打印,逻辑清楚得多。

/* 返回 1 表示安全,并把安全序列写进 safe_seq */ int is_safe(int n, int m, int avail[], int max[][MAX_RES], int alloc[][MAX_RES], int safe_seq[]) { int work[MAX_RES], finish[MAX_PROC] = { 0 }; for (int j = 0; j < m; j++) work[j] = avail[j]; /* 工作向量 = 当前可用 */ for (int k = 0; k < n; k++) { int found = -1; for (int i = 0; i < n; i++) { if (finish[i]) continue; int ok = 1; for (int j = 0; j < m; j++) { /* 判断需求是否都能满足 */ if (max[i][j] - alloc[i][j] > work[j]) { ok = 0; break; } } if (ok) { found = i; break; } } if (found < 0) return 0; /* 找不到可满足的进程,不安全 */ for (int j = 0; j < m; j++) work[j] += alloc[found][j]; /* 假设它跑完并归还资源 */ finish[found] = 1; safe_seq[k] = found; } return 1; }

work数组不能用avail直接改,试分配要能回滚,所以必须复制一份。finish用数组而不是复用 PCB 的state,是因为安全性检查可能在一次请求里被调用多次,状态要能重置。MAX_RES和MAX_PROC建议在mem.h里定义成宏(比如 10 和 20),比写死成[][3]更安全——资源数改成 4 的时候不会越界。

资源请求的入口函数要先把“请求 > 需求”和“请求 > 可用”这两种情况直接拒绝,再进入试分配:avail -= request; alloc += request; need -= request;,调一次is_safe,安全就保留分配,不安全就把三个数组减回去。回滚这段代码是报告里最容易写漏的部分。

4.2 页面置换:三种算法共用一套物理块数组

FIFO、LRU、OPT 的差别只在“选哪一页淘汰”,命中判断和缺页统计完全一样。用一个replace()函数按算法编号分派,公共部分只写一次。

/* algo: 0=FIFO, 1=LRU, 2=OPT;返回缺页次数 */ int page_replace(int *ref, int len, int frames, int algo, int *faults) { int blk[MAX_FRAME], load_t[MAX_FRAME]; /* 物理块内容与装入/访问时刻 */ int cnt = 0, fidx = 0; /* cnt 当前占用块数,fidx 为 FIFO 指针 */ for (int i = 0; i < frames; i++) { blk[i] = -1; load_t[i] = 0; } *faults = 0; for (int i = 0; i < len; i++) { int p = ref[i], hit = -1; for (int j = 0; j < cnt; j++) if (blk[j] == p) { hit = j; break; } if (hit >= 0) { /* 命中,只更新访问时刻 */ load_t[hit] = i; continue; } (*faults)++; if (cnt < frames) { /* 还有空块,直接装入 */ blk[cnt] = p; load_t[cnt] = i; cnt++; } else { int victim = 0; if (algo == 0) { /* FIFO:轮转指针 */ victim = fidx; fidx = (fidx + 1) % frames; } else if (algo == 1) { /* LRU:访问时刻最早的 */ for (int j = 1; j < frames; j++) if (load_t[j] < load_t[victim]) victim = j; } else { /* OPT:向后找最远才用的,永不使用给 len */ int far = -1; for (int j = 0; j < frames; j++) { int k = i + 1; while (k < len && ref[k] != blk[j]) k++; if (k - i > far) { far = k - i; victim = j; } } } blk[victim] = p; load_t[victim] = i; } } return *faults; }

load_t一个数组兼两个用途:LRU 里存最近一次访问的时刻,FIFO 里其实用不到(靠fidx指针),OPT 里也用不到(靠向后扫描)。这样写省掉了一套额外结构,代价是看代码的人要理解“同一个数组在三种算法下语义不同”,报告里最好加一句注释说明。OPT 里k - i在“后面不再出现”时会等于len - i,天然比任何真实距离都大,正好当选淘汰对象,不需要额外写哨兵。

4.3 磁盘调度:SSTF 与 SCAN 的磁头移动量

磁盘调度实验的核心指标是“总寻道长度”,算法本身不复杂,难点在 SCAN 要判断移动方向。

/* SSTF:每次挑离当前磁头最近的请求 */ int sstf(int *req, int n, int head) { int used[MAX_REQ] = { 0 }, total = 0; for (int k = 0; k < n; k++) { int best = -1, bd = 1 << 30; for (int i = 0; i < n; i++) { if (used[i]) continue; int d = abs(req[i] - head); if (d < bd) { bd = d; best = i; } } total += bd; head = req[best]; used[best] = 1; } return total; }

bd初始化成一个足够大的数而不是req[0],是因为req[0]可能已经被访问过。SCAN 则要先把请求排序,从head位置分成两段,按当前方向先服务一侧,到头再折返扫另一侧。折返点取最大磁道号还是 0,取决于题目约定,报告的“算法说明”里要写清楚,否则算出来的总寻道长度和参考实现对不上。

5. 跑起来与踩坑:编译、数据文件和结果校验

5.1 三分钟跑通:从 clean 到看到甘特图

拿到源码后别急着看代码,先把程序跑起来,心里有个“正常输出长什么样”的基准。

# 1. 清掉可能残留的旧产物 make clean # 2. 全部编译,出现 error 先解决再往下走 make # 3. 逐个实验跑一遍,调度模块直接读数据文件 ./bin/sched # 4. 其他模块是菜单式交互,跑起来后按提示输入 ./bin/banker ./bin/page ./bin/disk

编译这一步如果报undefined reference,九成是 Makefile 里某个目标漏了$(COMMON);如果报'MAX_PROC' undeclared,是include路径没生效,检查-Iinclude有没有写。运行调度模块前先打开data/sched1.txt看一眼格式,通常是每行“进程号 到达时间 运行时间 优先级”四列,改数据的时候保持列数和分隔符一致。

输出验证有个简单办法:手算一遍 FCFS。第一个进程 0 时刻到达、运行 3,第二个 2 时刻到达、运行 6,那么完成时间分别是 3 和 9,周转时间是 3 和 7,平均 5。程序算出来的平均周转时间如果对不上,问题一定在now的推进或者start、finish的赋值上,用 gdb 在while循环里打now就能定位。

5.2 五条踩坑记录:从现象倒推原因

现象一:make报undefined reference to 'print_gantt'。原因很直接,某个目标的依赖列表里只写了src/sched/xx.c,忘了$(COMMON),链接阶段找不到util.c里的符号。解决是把公共源文件统一放进COMMON变量并拼到每个目标后面,不要手写路径,手写早晚漏。

现象二:时间片轮转算出来的平均周转时间比先来先服务还差。第一反应是代码写错了,实际上是时间片设成了 8,而所有进程的运行时间都不超过 6,等于每个进程一片就跑完,退化成了 FCFS 加大量空转。把q改成 2 或 3 再跑,指标立刻符合预期。这类结论不要直接写进报告,容易被反问“那时间片趋于无穷时会怎样”。

现象三:银行家算法输入 4 个资源类型就崩溃。数组声明成int max[][3],但输入里资源数填了 4,写越界把栈上的其他变量踩了,程序不一定当场崩,可能只是安全序列算错,属于典型的黑匣子问题。解决办法是把资源数做成变量传入,同时在读入时校验m <= MAX_RES,超了直接报错退出,而不是让它悄悄越界。

现象四:OPT 的缺页率算出来比 LRU 还高。OPT 理论上是最优的,出现这种结果一定是实现有问题,最常见的是页面引用串用0结尾、循环条件写成ref[i] != 0,而页面号本身包含 0,读到第一个 0 就提前结束。把长度单独传参,别依赖哨兵值,这是血泪经验。另一种情况是物理块数改过但MAX_FRAME没跟着改。

现象五:数据文件从 Windows 拷过来,解析出来的到达时间全是 0。用fscanf("%d")读数字其实不受换行符影响,但如果代码里混用了fgets再手工strtok,行尾的\r会让atoi返回 0。解决方式是编译前跑一次dos2unix data/*.txt,或者解析时把\r一起当分隔符处理。

现象六(附带):甘特图在终端里对不齐。进程名里用了中文或者全角括号,printf的宽度按字节算,一个汉字占三字节但显示两列,必然错位。输出层统一用 ASCII 的P1、P2,中文留在报告正文里。

6. 二次开发:把菜单程序改成能批量评测的小框架

这套代码最实用的改造方向,不是加算法,而是把“人工点菜单、肉眼对结果”变成“读文件、批量跑、输出结构化结果”。答辩现场被要求换三组数据重跑的时候,这个改造能省下大量时间。

第一步是给每个可执行文件加命令行参数,把交互式输入变成可选。以调度模块为例,用getopt支持-f指定数据文件、-a指定算法、-q指定时间片:

/* ./bin/sched -f data/sched1.txt -a rr -q 3 */ int opt, algo = 0, quantum = 2; const char *file = "data/sched1.txt"; while ((opt = getopt(argc, argv, "f:a:q:")) != -1) { switch (opt) { case 'f': file = optarg; break; case 'a': algo = parse_algo(optarg); break; /* fcfs/sjf/prio/rr */ case 'q': quantum = atoi(optarg); break; default: usage(); return 1; } }

第二步是把print_metrics的输出从printf改成写文件,格式用 CSV,一行一个算法一组参数,字段是“算法, 时间片, 进程数, 平均周转时间, 平均带权周转时间, 缺页次数”。这样同一份数据可以循环调用不同参数,结果直接丢进表格工具出对比图,报告里的“算法性能对比”一节就不用一个个手抄了。

第三步是补一组回归用例。做法很土但有效:把当前正确输出存成expected/*.txt,写个 shell 脚本跑一遍diff,改代码之后先跑这个脚本。改造过程中最容易破坏的是“所有进程同时到达”和“存在空闲时间段”这两个边界,各留一组数据,改完立刻知道有没有踩坏。

改造点改动位置收益
命令行参数各模块main.c免去手工输入,可脚本调用
CSV 输出common/util.c结果可直接做对比表
回归脚本根目录test.sh改算法后快速验证没跑偏
参数校验load_pcb等入口越界输入直接报错,不静默算错

第四步,如果想让报告更有说服力,可以给调度模块加一个随机数据生成器:给定进程数范围和到达时间分布,生成 100 组数据,每个算法跑一遍取平均。单个例子容易被质疑“你这组数据刚好对某个算法有利”,跑一百组取均值就没这个问题了。生成器本身二十行代码,比手工编数据快得多。

注意:加参数解析之后,原来的菜单交互路径最好保留,用一个-i开关切过去。答辩时导师往往还是习惯看交互式演示,把菜单删了反而尴尬。

这套代码我前后改过几版,最大的教训是别在答辩前一晚动公共结构体。有次为了加一个“进程类型”字段改了pcb.h,结果六个实验里有两个忘了重新编译,演示时数据对不上,现场找了十分钟才发现是旧的二进制在跑。从那以后我每次改完公共头文件,都强制走一遍make clean && make,再用两组边界数据跑一遍确认输出没变,最后才敢改下一个模块。希望这些能帮到你,把课设跑通只是第一步,把它改成自己能随手复用的工具,才是这份代码后面真正用得上的地方。

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

返回列表