简介:这份南邮数据结构实验源码包面向学习数据结构课程、需要完成上机实验或课程设计的高校学生,也可供教师作课堂案例参考。内容对应南京邮电大学数据结构课程的四次实验,覆盖线性表、栈与队列、二叉树与哈夫曼树、图的基本操作及最短路径等知识点,并包含多项式运算、飞机换乘次数等具体问题实现。压缩包共64个文件,以13个h头文件和10个cpp源文件为核心,辅以pdb、obj、dsp、dsw等工程与调试文件,整体约1.58MB,便于在VC环境中查看和运行。已有2640人学习,说明其在校内实验场景中具有一定参考价值。读者可对照实验要求理解顺序表与链表、栈队列应用、树结构构造及图算法实现思路,也可参考附带的实验报告文档整理实验过程;建议在理解基础上修改和调试,而非直接照搬。
1. 先看一个真实会遇到的返工现场
某同学课程设计验收前一天,图的邻接表都建好了,Dijkstra 跑出来的最短路径却每次都比答案多 1。他换了三套建图代码,最后发现问题在初始化:源点的距离写成了 0,其余全填了一个很大的数,但顶点下标和数组下标差了一位。数据结构实验的痛点几乎都在这种事上——不是不会写,而是没有一份能逐行对照的完整实现。这套某高校数据结构课程的实验源码包,覆盖顺序表、单链表、栈、循环队列、二叉树、哈夫曼树、邻接矩阵与邻接表两种建图、查找与内部排序,全部按实验编号拆成可独立编译的 C/C++ 源文件。它适合三类人:赶课设的学生、拿经典结构练 C 指针的新手、需要一份对照答案来定位自己实现错在哪的开发者。
2. 工程结构拆解:实验目录按数据结构主线怎么铺开
拿到一堆源码,第一件事不是打开 main 函数,是先把目录看明白。这套代码的组织方式比较典型:按实验编号分目录,每个目录一个独立可执行程序,公共部分抽出来放在上层。这么设计的直接好处是任何一个实验都能单独编译、单独跑、单独改,不会因为改了链表把图的代码编崩。
2.1 目录分层与命名约定
常见的目录形态是这样,我把它整理成可读的树:
ds-lab/ ├── include/ # 跨实验共用的头文件 │ ├── common.h # OK/ERROR 返回值、MAXV、INF 等公共宏 │ ├── list.h │ ├── stack.h │ ├── tree.h │ └── graph.h ├── lab01_list/ # 线性表 │ ├── main.c # 菜单交互入口 │ ├── seq_list.c # 顺序表实现 │ └── link_list.c # 带头结点单链表实现 ├── lab02_stack_queue/ │ ├── main.c │ ├── sq_stack.c │ └── circular_queue.c ├── lab03_tree/ │ ├── main.c │ ├── bitree.c # 建树、三种递归遍历、层序 │ └── huffman.c ├── lab04_graph/ │ ├── main.c │ ├── adj_matrix.c │ └── adj_list.c # BFS/DFS/Prim/Dijkstra ├── lab05_sort_search/ │ ├── main.c │ └── sort.c # 插入、冒泡、快排、堆排、归并 ├── test/ # 重定向输入用的 .in / .out └── Makefile命名上有个约定值得注意:seq_前缀表示顺序存储,link_表示链式存储,adj_表示图,circular_表示循环结构。看文件名就知道底层用的是数组还是指针,调试时省掉很多翻代码的时间。另外每个实验目录里只有一个main.c,实现文件全部不带main,这是多文件工程不打架的前提。
一个实验一个可执行文件,意味着你要验证单链表,只需要进lab01_list,其他目录的代码不会被链进来。很多课程设计把所有实验塞进一个大main.c,用switch菜单分发,最后几千行挤在一起,改一个函数要滚半天屏。分开之后,编译时间和心智负担都下来了。
2.2 编译单元与头文件组织
头文件里该放什么,是这套代码里最容易踩坑的地方。合格的做法是:类型定义、宏、函数声明放头文件,变量定义和函数实现放.c。把所有东西都塞进头文件,第一次编译能过,一旦两个.c同时#include就会报multiple definition。
/* include/common.h —— 全工程共用的宏与返回值约定 */ #ifndef DS_COMMON_H #define DS_COMMON_H #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 #define MAXV 100 /* 顶点数上限,图算法统一引用它 */ #define INF 0x3f3f3f3f /* 不用 INT_MAX:两个 INF 相加会溢出 */ #endifINF挑0x3f3f3f3f而不是INT_MAX是老手习惯。Dijkstra 里会写dist[u] + w,如果dist[u]是INT_MAX,加一个正权值直接整数溢出变成负数,松弛判断就失效了。0x3f3f3f3f约等于 10 亿,两个相加还在int范围内;它每个字节都是0x3f,还能用memset(dist, 0x3f, sizeof(dist))一次性初始化,比写循环快。
/* include/list.h —— 只放类型和声明,函数体一律留在 .c */ #ifndef DS_LIST_H #define DS_LIST_H #include "common.h" typedef struct Node { int data; struct Node *next; } Node; Node *list_create(void); /* 建一个带头结点的空表 */ int list_insert(Node *head, int i, int e); /* 第 i 个位置前插入 e */ int list_delete(Node *head, int i, int *e); /* 删除第 i 个结点,值带出 */ int list_length(Node *head); void list_destroy(Node *head); #endifNode结构体定义在头文件里,是因为调用方可能需要直接访问p->next做遍历;函数声明放这里,实现在link_list.c。这样main.c只管#include "list.h",不需要知道内部怎么分配内存。
编译时把-I指到include目录,就能跨目录引用:
gcc -std=c99 -Wall -Wextra -g -Iinclude \ lab01_list/main.c lab01_list/link_list.c lab01_list/seq_list.c \ -o build/lab01_list-Iinclude告诉编译器去哪找"list.h";-Wall -Wextra会揪出「未使用变量」「有符号与无符号比较」这类小毛病;-g保留调试符号,没有它 gdb 里看不到变量值。
2.3 工具链与依赖:为什么这套代码几乎零配置
纯 C 标准库,不依赖任何第三方库,是这类实验源码最大的便利。你不需要装 CMake、不需要拉包管理器、不需要配环境变量,一个 gcc 就能跑。常见做法是:
| 场景 | 工具链 | 说明 |
|---|---|---|
| Linux / macOS | gcc 或 clang + make | 最省事,Makefile 直接可用 |
| Windows 原生 | MinGW-w64 的 gcc | 命令行体验和 Linux 一致 |
| Windows 图形界面 | Dev-C++ / Code::Blocks | 教学环境常用,直接打开.c就能编 |
| 远程与容器 | WSL2 / 任意 Linux 镜像 | 挂载目录后代码原地跑 |
调试工具建议至少会一个:gdb用来单步看指针,valgrind --leak-check=full或-fsanitize=address用来抓链表节点的内存泄漏。链表、树、图这些结构,写的时候不觉得,跑十次八次之后节点的分配释放对不上,泄漏就积起来了,用工具一眼能看出来在第几行malloc。
3. 核心模块拆解:从单链表到最短路径的实现套路
这一章直接进代码。每个片段都是实验里你一定会写到的那几行,关键在边界条件怎么处理。
3.1 线性表:顺序表与单链表的插入删除
顺序表插入的核心是「从后往前挪」,漏掉这一步就会把数据覆盖掉:
/* seq_list.c —— 顺序表在第 i 个位置前插入 e */ #define MAXSIZE 100 typedef struct { int data[MAXSIZE]; int length; } SqList; int seq_insert(SqList *L, int i, int e) { if (i < 1 || i > L->length + 1) return 0; /* 合法位置是 1..length+1 */ if (L->length >= MAXSIZE) return 0; /* 表满,不能再塞 */ for (int j = L->length; j >= i; j--) L->data[j] = L->data[j - 1]; /* 从后往前挪,给 i-1 腾位 */ L->data[i - 1] = e; L->length++; return 1; }参数i从 1 开始计数,落到数组下标要减 1,这是最容易写错的地方。循环从L->length开始递减,因为最后一个元素要挪到length这个空位上;如果反过来从前往后挪,data[i]会先把data[i-1]覆盖掉,后面全是脏数据。返回值用0/1而不是直接exit,是为了让调用方决定怎么处理。
链表插入则讲究「先接后、再断前」:
/* link_list.c —— 带头结点的单链表,在第 i 个位置前插入 e */ int list_insert(Node *head, int i, int e) { if (i < 1) return 0; Node *p = head; int j = 0; /* 头结点记为第 0 个 */ while (p != NULL && j < i - 1) { /* 目标:走到第 i-1 个结点 */ p = p->next; j++; } if (p == NULL) return 0; /* i 大于表长 +1 */ Node *s = (Node *)malloc(sizeof(Node)); if (s == NULL) return 0; s->data = e; s->next = p->next; /* 先让新结点接住后继 */ p->next = s; /* 再让前驱指向新结点 */ return 1; }p->next = s和s->next = p->next这两句顺序写反,链表就断了——s->next拿到的是已经指向s自己的指针,形成自环,遍历直接死循环。带头结点的意义在这里体现得很清楚:插入和删除第一个位置时不需要单独判断head是否为空,head永远存在,代码路径统一。
i的合法范围是1..length+1,插入到第length+1位置相当于尾插。删除同理,只是最后要多一句free(p)。
3.2 栈与队列:循环队列的判空与判满
循环队列的判空判满,靠「少用一个存储单元」是最常见的解法:
/* circular_queue.c —— 少用一个单元区分队空和队满 */ #define MAXQSIZE 100 typedef struct { int data[MAXQSIZE]; int front, rear; /* rear 指向下一个空位 */ } SqQueue; int queue_empty(SqQueue *q) { return q->front == q->rear; } int queue_full(SqQueue *q) { return (q->rear + 1) % MAXQSIZE == q->front; } int enqueue(SqQueue *q, int e) { if (queue_full(q)) return 0; q->data[q->rear] = e; q->rear = (q->rear + 1) % MAXQSIZE; return 1; } int dequeue(SqQueue *q, int *e) { if (queue_empty(q)) return 0; *e = q->data[q->front]; q->front = (q->front + 1) % MAXQSIZE; return 1; }如果只用front == rear判断空,队满时rear绕一圈也会等于front,两种情况分不开。牺牲一个data槽位,判满改成(rear+1) % MAXQSIZE == front,逻辑就干净了。代价是容量从 100 变成 99,实验里完全可接受。
替代方案是用一个count计数器或tag标记最后一次操作是入队还是出队,两种写法各有拥趸,但约定必须全文件统一,不能enqueue按「rear 指向空位」写、dequeue按「front 指向队头元素」写,混着用就是玄学 bug 的源头。
栈相对简单,顺序栈top初始化为-1表示空,入栈data[++top] = e,出栈e = data[top--],注意判top == -1和top == MAXSIZE-1。
3.3 二叉树:建树、三种遍历与非递归写法
先序建树用#表示空结点,这是实验报告里最常见的输入格式:
/* bitree.c —— 按先序序列建树,'#' 表示空 */ typedef struct BiTNode { char data; struct BiTNode *lchild, *rchild; } BiTNode, *BiTree; BiTree tree_create(void) { char ch = getchar(); if (ch == '#' || ch == '\n') return NULL; BiTree t = (BiTree)malloc(sizeof(BiTNode)); t->data = ch; t->lchild = tree_create(); t->rchild = tree_create(); return t; }递归建树的顺序就是先序:先读根、再递归左、再递归右。getchar()会读进空格和换行,所以判断里必须把'\n'一起挡掉,否则输入AB#D##C##之后按回车,多读一个换行符会让整棵树错位。稳妥做法是在读之前加一句while ((ch = getchar()) == ' ' || ch == '\n');过滤空白。
非递归中序是考试和面试的高频点,靠一个显式栈模拟递归:
void inorder_iter(BiTree root) { BiTree stack[100]; int top = -1; BiTree p = root; while (p != NULL || top != -1) { while (p != NULL) { /* 一路向左,沿途压栈 */ stack[++top] = p; p = p->lchild; } p = stack[top--]; /* 弹出并访问 */ printf("%c ", p->data); p = p->rchild; /* 转向右子树 */ } }外层循环的条件是p != NULL || top != -1,两个都不能少:p非空说明还有左子树要下探,栈非空说明还有结点没访问。只写其中一个,遍历会提前结束或者死循环。层序遍历要配循环队列,出队一个就把它的左右孩子入队,正好复用 3.2 的结构。
3.4 图:邻接表建图与 Dijkstra
图这部分,邻接表的结构体定义要先立住:
/* adj_list.c —— 邻接表存储的有向带权图 */ typedef struct ArcNode { int adjvex; /* 边指向的顶点下标 */ int weight; struct ArcNode *next; } ArcNode; typedef struct VNode { int data; ArcNode *first; /* 该顶点第一条出边 */ } VNode, AdjList[MAXV]; typedef struct { AdjList vertices; int vexnum, arcnum; } ALGraph;ArcNode是边结点,VNode是顶点结点,两层结构让「一个顶点挂多条边」变成链表操作。建图时每读一条边<u, v, w>,就malloc一个ArcNode头插到vertices[u].first上,无向图要对称插两次。
Dijkstra 的完整实现:
void dijkstra(ALGraph *g, int src, int dist[], int path[]) { int visited[MAXV] = {0}; for (int i = 0; i < g->vexnum; i++) { dist[i] = INF; path[i] = -1; } dist[src] = 0; /* 源点到自己距离为 0,这行错了全盘错 */ for (int k = 0; k < g->vexnum; k++) { int u = -1, minv = INF; for (int i = 0; i < g->vexnum; i++) /* 选当前未访问的最小 dist */ if (!visited[i] && dist[i] < minv) { minv = dist[i]; u = i; } if (u == -1) break; /* 剩下的点都不可达 */ visited[u] = 1; for (ArcNode *p = g->vertices[u].first; p != NULL; p = p->next) { int v = p->adjvex; if (!visited[v] && dist[u] + p->weight < dist[v]) { dist[v] = dist[u] + p->weight; /* 松弛 */ path[v] = u; /* 记前驱,用于回溯路径 */ } } } }dist数组是输出参数,调用方负责分配;path记录每个顶点的前驱,最后从终点倒着走回源点就能打印路径。visited是局部数组,必须显式初始化——int visited[MAXV] = {0}里的{0}不能省,否则里面是栈上的随机值,未访问的点被当成已访问,结果直接离谱。u == -1的提前退出判断也别丢,图不连通时不加这句,后面的松弛会拿着u = -1去访问vertices[-1],段错误。
Prim 求最小生成树的骨架和这个几乎一样,区别是dist存的是「到已选集合的最短边权」,松弛时比较的是p->weight而不是dist[u] + p->weight。改的时候只动那一行就行。
3.5 排序:快排的划分与堆排的下沉
快排的partition是整套代码里被抄得最多的一段:
/* sort.c —— 以首元素为枢轴,一趟划分 */ int partition(int a[], int low, int high) { int pivot = a[low]; while (low < high) { while (low < high && a[high] >= pivot) high--; a[low] = a[high]; /* 右边比枢轴小的换到左边 */ while (low < high && a[low] <= pivot) low++; a[high] = a[low]; /* 左边比枢轴大的换到右边 */ } a[low] = pivot; /* 枢轴落位 */ return low; }内层两个while都必须带low < high条件,少了就是越界读。a[low] = a[high]是覆盖而不是交换,因为pivot已经把a[low]的值存下来了,最后再填回去,一趟划分只需要一次赋值。
堆排的关键在下沉:
/* 大根堆下沉:结点 k 的子树调整成堆,n 是有效长度 */ void sift_down(int a[], int k, int n) { int e = a[k]; for (int i = 2 * k + 1; i < n; i = 2 * i + 1) { /* 0 号下标起步,左孩子是 2k+1 */ if (i + 1 < n && a[i + 1] > a[i]) i++; /* 取左右孩子中较大的 */ if (e >= a[i]) break; /* 父亲已经最大,收工 */ a[k] = a[i]; k = i; } a[k] = e; }2*k+1对应 0 号下标起步的完全二叉树;如果数组从 1 开始存,左右孩子是2k和2k+1,两套公式不能混。建堆时从n/2 - 1倒着往下沉,排序时每次把堆顶换到末尾、长度减一、再从根下沉一次,整体 O(n log n),且不需要递归栈空间。
4. 跑起来:编译、多文件组织与测试数据构造
代码看懂了,下一步是让它真的在自己的机器上输出正确结果。这一章按「单文件 → 多文件 → 自动化验证」的顺序走。
4.1 单文件实验的编译与运行
最简单的实验目录只有一个.c,直接编译:
# 进到实验目录,编译时开全部警告并保留调试信息 gcc -std=c99 -Wall -Wextra -g lab02_stack_queue/main.c \ lab02_stack_queue/circular_queue.c \ -o build/lab02 # 运行,手动敲菜单里的数字 ./build/lab02-std=c99是为了让for (int i = 0; ...)这种写法合法,老编译器默认gnu89会报错。-o build/lab02指定输出路径,把可执行文件统一收进build/,源码目录保持干净,方便打包交作业。
Windows 上如果用的是 Dev-C++,直接「打开项目 → 编译运行」即可,但要注意它默认可能用 C++ 编译器编.c文件,遇到malloc返回值不强制转换也没事,换到 gcc 上就会警告。养成写(Node *)malloc(...)的习惯,跨编译器不翻车。
4.2 多文件工程的 Makefile 组织
实验一多,一条条敲gcc就不现实了。一个够用的 Makefile 长这样:
CC = gcc CFLAGS = -std=c99 -Wall -Wextra -g -Iinclude BIN_DIR = build TARGETS = $(BIN_DIR)/lab01 $(BIN_DIR)/lab02 $(BIN_DIR)/lab03 $(BIN_DIR)/lab04 all: $(TARGETS) $(BIN_DIR)/lab01: lab01_list/main.c lab01_list/link_list.c lab01_list/seq_list.c | $(BIN_DIR) $(CC) $(CFLAGS) $^ -o $@ $(BIN_DIR)/lab02: lab02_stack_queue/main.c lab02_stack_queue/circular_queue.c | $(BIN_DIR) $(CC) $(CFLAGS) $^ -o $@ $(BIN_DIR): mkdir -p $(BIN_DIR) clean: rm -rf $(BIN_DIR)几个细节:$^是全部依赖文件,$@是目标名,省得把源文件列两遍;| $(BIN_DIR)是 order-only 依赖,保证目录先建好但目录变动不触发重编;命令前面必须是 Tab,不能是空格,这是 Makefile 最经典的报错来源。
用的时候make编全部,make build/lab01单编一个,make clean清干净。改一个实验不会牵连其他目标,因为每个目标依赖的源文件是分开列的。
4.3 测试数据构造与调试入口
交互式菜单程序不好手动测,用输入重定向把操作序列喂进去:
# test/lab02.in 里逐行写菜单选项,比如 1 回车 10 回车 2 回车 ... ./build/lab02 < test/lab02.in > test/lab02.actual # 和期望输出对比,diff 无输出就是通过 diff test/lab02.expected test/lab02.actual<把文件当标准输入,>把结果落盘。.in和.expected成对放,改完代码重跑一遍diff,比肉眼核对快十倍。像排序实验这种输出很长的,直接diff能立刻告诉你在第几行开始不一致。
调试用 gdb 定位最直接:
gdb ./build/lab01 # (gdb) break list_insert 在插入函数下断点 # (gdb) run # (gdb) print *head 看头结点的值 # (gdb) next 单步执行在链表操作里下断点,print p和print p->next交替看,指针什么时候断的、malloc有没有返回NULL,一眼就清楚。段错误现场用gdb ./a.out跑一遍,bt打印调用栈,能直接看到崩在哪个函数的哪一行。
5. 避坑与排查:编译报错、段错误与结果对不上
实验代码跑不通,九成落在下面这几类。按「现象 → 原因 → 解决」整理,遇到问题照着对。
5.1 编译期问题
现象:一堆.c分开编都过,一起编报undefined reference to 'list_insert'。原因:链接阶段找不到函数体,通常是gcc命令里漏了某个实现文件,或者只编了main.c。解决:把该实验目录下所有.c都列进去,或者用gcc main.c *.c通配。Makefile 里检查目标依赖是不是少写了一个源文件。
现象:报multiple definition of 'maxsize'。原因:全局变量写在头文件里被多个.c包含,每个编译单元都产出一份定义。解决:头文件里改成extern int maxsize;,在任意一个.c里写int maxsize = 100;。更省事的做法是直接用#define或static const。
现象:Dev-C++ 能编过,换 gcc 报warning: implicit declaration of function 'malloc'。原因:漏了#include <stdlib.h>,C99 之后隐式声明是错误而不是警告。解决:用到的每个标准库函数都补上对应头文件,malloc/free对应stdlib.h,printf对应stdio.h,strlen对应string.h。
5.2 运行期问题
现象:程序随机崩溃,崩的位置每次还不一样,有时候能跑完。原因:典型的野指针或越界写。常见于malloc后没判空、数组下标差一位、free之后继续访问。解决:编译加-fsanitize=address重跑,它会在越界的第一现场打印完整调用栈。free(p)之后紧跟p = NULL;,让悬垂指针立刻变成空指针,下次解引用立刻崩而不是静默写脏数据。
现象:链表跑一百次没问题,跑一万次内存爆掉。原因:删除结点只调了p->next = p->next->next,没有free掉被摘下来的那个结点。解决:valgrind --leak-check=full ./build/lab01会列出泄漏发生在哪个函数的哪一行malloc。养成「谁分配谁释放」的习惯,list_destroy里循环free到NULL为止。
现象:图的程序输入规模稍大就崩,栈溢出。原因:DFS 或快排递归深度过大,或者函数里开了int a[1000000]这种大局部数组。解决:大数组改成static或malloc到堆上;递归改成显式栈的迭代写法,3.3 节的非递归中序就是模板。
5.3 逻辑类问题
现象:循环队列元素明明没满,enqueue却一直返回失败。原因:front和rear的语义没统一,一个按「指向当前元素」写,一个按「指向下一个空位」写,判满公式(rear+1)%N == front自然算错。解决:全文件统一约定「front指向队头元素、rear指向下一个空位」,判空front == rear,判满(rear+1)%N == front,初始时两个都设 0。
现象:scanf("%d", &n)之后紧跟gets(str),字符串读进来是空的。原因:scanf只吃掉了数字,回车符还留在输入缓冲区,gets读到回车就返回。解决:scanf之后加一句getchar();吃掉回车;或者用scanf(" %[^\n]", str),格式串开头的空格会跳过所有空白字符。
现象:快排在小数据上正确,喂一组全相同的元素就退化成极慢。原因:固定取首元素做枢轴,有序或大量重复数据时每趟划分都极不均衡,复杂度掉到 O(n²)。解决:改成三数取中或随机化选枢轴,swap(a[low], a[low + rand() % (high-low+1)])再走原逻辑。递归的子区间较小时(比如长度小于 16)切到插入排序,实测能快一截。
6. 二次开发:把实验代码改成能复用的结构库
实验代码能跑只是起点,真正有价值的是把它改成「下次还能用」的形态。我的做法是抽一个统一的接口层,让顺序表和链表对外暴露同一组函数签名:
/* include/list_adt.h —— 两种实现共用一套调用约定 */ typedef struct ListOps { void *(*create)(void); int (*insert)(void *ctx, int i, int e); int (*erase)(void *ctx, int i, int *out); int (*length)(void *ctx); void (*destroy)(void *ctx); } ListOps; extern const ListOps seq_list_ops; /* seq_list.c 里定义 */ extern const ListOps link_list_ops; /* link_list.c 里定义 */调用方只拿一个ListOps *,业务代码不用关心底层是数组还是指针。写测试时同一批用例跑两遍,能立刻发现哪种实现在哪个下标上处理不一致。
第二步是补测试。建一个tests/目录,每个模块一个test_xxx.c,用断言代替肉眼看输出:
#include <assert.h> #include "list.h" int main(void) { Node *h = list_create(); assert(list_insert(h, 1, 10) == 1); /* 空表头插 */ assert(list_insert(h, 2, 20) == 1); /* 尾插 */ assert(list_insert(h, 1, 5) == 1); /* 中间插 */ assert(list_insert(h, 9, 99) == 0); /* 越界必须失败 */ assert(list_length(h) == 3); int out; assert(list_delete(h, 2, &out) == 1 && out == 10); list_destroy(h); return 0; }assert在条件不成立时直接中止并打印行号,比printf完再人眼比对可靠。把每个结构都配一份这样的用例,改完代码跑一遍全绿,才敢动下一处。
第三步看数据。把各种排序的时间用clock()打出来,随机数组、有序数组、逆序数组各测一遍,你会直观看到冒泡在有序数据上其实很快、快排在逆序上的退化有多明显。这些数字放进课程设计报告里,比只写一句「时间复杂度 O(n log n)」有说服力得多,也是面试时能讲出来的东西。
有个习惯我一直保持:改任何一处核心结构之前,先把当前能跑通的版本用git commit存一下,再在tests/里补一条能复现新需求的用例,确认它在旧代码上是红的,然后才动实现。当年做课设时直接改链表删除逻辑、改崩了又没备份,通宵重写了两百行——从那以后我每次动指针代码,都强制先跑一遍测试用例再提交。希望这套代码也能帮你少走这段弯路。
本文还有配套的精品资源,点击获取