资讯详情

资讯详情

从CREATE TABLE到落盘:C语言迷你DBMS表结构模块设计

简介这份山东科技大学《数据库系统概论》课程设计实验报告面向计算机科学与技术等专业学生及需要完成数据库课程设计的读者围绕实现一个简易DBMS的建表与改表功能展开。内容涵盖课程设计任务书、需求分析、设计思想、程序流程图与物理存储结构说明重点讲解CREATE TABLE与ALTER TABLE语句的解析与实现以及“新建表、修改表、显示已建表信息、保存并退出”四个模块并讨论用数组保存表名、字段名、类型和列级约束借助table.txt持久化表结构、启动时加载的设计思路。压缩包共1个doc文件约408KB适合直接查阅报告结构、功能划分与实现细节。已有934人学习可为课程设计选题、报告撰写、命令行与图形界面交互设计及表结构存储方案提供较完整的参考。1. 从一条 CREATE TABLE 说起这个课程设计到底做了什么数据库系统概论这门课考试时能默写 B 树和三级模式可真让你把CREATE TABLE student(sno char(10) primary key, ...)这句 SQL 落到磁盘上多半就卡住了。这个课程设计做的就是这件事用 C 写一个迷你 DBMS 的结构定义模块能建表、改表、显示已建表的结构、把表结构存成文件下次再读回来命令行和菜单式交互两条路都留着。它不适合当生产力工具但对刚啃完数据库原理、想找个几百行小工程把手感练起来的人很合适——一句 SQL 从字符串变成磁盘上一行行文本的全过程自己写一遍才算真看懂。下面按我拆项目的顺序讲先看骨架再逐段啃 CREATE 和 ALTER 的解析最后说怎么在这个骨架上继续加东西。2. 整体架构与存储结构几组字符数组撑起一套表管理流程2.1 从交互层到落盘四个功能模块怎么串起来这个程序的骨架非常朴素没有分层框架、没有设计模式就是一个main里的while循环加上四个职责清晰的函数。它对应任务书里的四项要求新建表、修改表、显示已建表的信息、保存并退出。我把它整理成下面这张职责表读源码时对着看会快很多。函数职责输入输出SQL_CREATE解析建表语句把表名、列名、类型、约束写进内存数组一条以;结尾的 CREATE 语句成功返回 1失败打印原因返回 0SQL_ALTER解析改表语句定位目标表后增删改列一条以;结尾的 ALTER 语句同上Read/Show启动时从 table.txt 回填内存运行时打印所有表结构无或文件路径屏幕输出Save把内存里的全部表结构整体重写回 table.txt文件路径覆盖写主循环大致是这样的结构int main(void) { char sql[512]; loadAll(table.txt); // 启动先把上次的表结构读回来 while (1) { showMenu(); // 1 新建 2 修改 3 显示 0 保存退出 int choice readChoice(); if (choice 0) break; switch (choice) { case 1: printf(请输入 CREATE 语句(以;结尾): ); readLine(sql, sizeof(sql)); SQL_CREATE(sql); break; case 2: printf(请输入 ALTER 语句(以;结尾): ); readLine(sql, sizeof(sql)); SQL_ALTER(sql); break; case 3: Show(); break; default: printf(无效选项\n); } } saveAll(table.txt); // 退出前统一落盘 return 0; }这里有个值得说一句的设计取舍程序不在每次建表或改表后立刻写文件而是退出时一次性重写。原作者在说明书里也写了这个理由——对已存在的表做修改后想直接在文件里定位到那一行再改位置计算很麻烦干脆整体清空重写逻辑简单一百倍。对一个几百行的教学项目来说这个选择是对的代价是程序被强杀时这次改的动作会丢。真要做成能二次开发的东西我一般会在每次结构变更后调一次saveAll代价只是多几次几百字节的写盘。另一个容易被忽略的点是“演示顺序”。任务书里要求“演示建立了一个表并修改了表结构”所以Show()必须能在任意时刻打印出当前内存里的完整结构包括类型后面那个(20)长度修饰和约束条件。这个函数写得好不好直接决定你答辩时能不能一眼看出改对了没有。2.2 表结构在内存里怎么摆整个程序的核心是几组全局的定长字符数组。原始代码里写成了char tableName[LEN][MAX]其中LEN和MAX都是同一个量级的常量这个维度顺序其实是反的tableName[number]被当作“第 number 张表的表名”来用那第一个维度就必须是表的数量上限第二个才是名字长度。搞反了不会编译报错但你在strcpy(tableName[number], t_name)的时候会踩到越界的玄学问题——有时候跑得好好的加第三张表就崩。我按正确的维度顺序重新整理了一版加上了注释#define MAX_TABLE 16 /* 最多支持几 张表 */ #define MAX_FIELD 32 /* 每张表最多几 列 */ #define LEN 64 /* 名字/类型串的最大长度 */ char tableName[MAX_TABLE][LEN]; /* 表名 */ int proNum[MAX_TABLE]; /* 每张表当前有几列 */ char fieldName[MAX_TABLE][MAX_FIELD][LEN]; /* 列名 */ char fieldType[MAX_TABLE][MAX_FIELD][LEN]; /* 类型带长度修饰如 char(20) */ char fieldCond[MAX_TABLE][MAX_FIELD][LEN]; /* 约束编码PK / UQ / NN / NULL */ int tableCount 0; /* 当前已建表数量 */这五组数组就是整个 DBMS 的“数据字典”。tableCount是全局游标建表时tableCount改表时通过findTable拿到下标。注意proNum是每张表独立的列数不能用一个全局变量代替——这是最容易犯的错我见过有同学写了个全局int num建第二张表时列数直接串到第一张表上去。类型我选择把长度修饰直接并进fieldType存成char(20)这样一个完整字符串而不是像原始代码那样单独开一个f_t_len数组存(、2、0、)四个字符。原始写法的问题在于它只取了一位数字。/* 原始代码片段只看不改 */ f_t_len[number][num][0] (; f_t_len[number][num][1] f_length 48; /* 长度大于 9 就废了 */ f_t_len[number][num][2] ); f_t_len[number][num][3] \0;f_length是解析出来的整数长度这里用48把它转成字符只转了个位数。写char(20)的时候长度是两位f_length算出 202048得到 68转成字符是D最后屏幕上显示的是char(D)。这个 bug 在演示char(10)的时候完全看不出来一旦老师让你建个char(50)当场翻车。合并成字符串加sprintf就没这个问题/* 长度解析与拼接digits 至少为 1 才合法 */ int len 0, digits 0; while (*p 0 *p 9) { len len * 10 (*p - 0); p; digits; } if (digits 0 || *p ! )) { printf(长度写法有误\n); return 0; } p; sprintf(fieldType[t][n] strlen(fieldType[t][n]), (%d), len);sprintf的目标地址用fieldType[t][n] strlen(...)也就是追加到已有类型名后面char加(20)拼成char(20)。这样长度多大都对后面写文件、打印、比较都是一整个字符串省掉一堆下标运算。2.3 存储文件格式一行一列别让空格咬你落盘用的是纯文本这也是最省事的选择——出问题了直接打开记事本就能看不用写十六进制查看器。格式我设计成“一行一列”2 student 3 sno char(10) PK sname char(20) NN age int NULL course 2 cno char(8) PK cname char(30) UQ第一行是表的数量之后每张表两段先是“表名 列数”一行然后按列数跟若干行列定义。每行列定义三个字段列名、类型含长度、约束编码。约束用编码而不是原文是这份格式最关键的一个决定原因在避坑那一章细说。约束编码对照表SQL 里的写法存盘编码显示时还原成PRIMARY KEYPKprimary keyUNIQUEUQuniqueNOT NULLNNnot null不写约束NULL无这样设计之后loadAll和saveAll就是一对严格对称的函数读的时候按同样的行序推回去即可。对称性是这个模块唯一的正确性保证——只要saveAll写出来的东西loadAll一定读得回来中间就不会出现“存了但读不出”的黑匣子状态。3. CREATE TABLE 解析一个字符指针走到底的手写词法器3.1 先把扫描器抽出来skipSpace 与 readToken正规做法是先做词法分析把 SQL 切成 token 数组再做语法分析。对这个规模的项目我一般不走那套直接用一个游标指针char *p在原始字符串上向前推进配两个小函数就够了。/* 跳过空白返回第一个非空白字符的位置 */ static char *skipSpace(char *p) { while (*p || *p \t || *p \n) p; return p; } /* 从 *pp 开始读一个 token转小写后写入 out返回长度 token 的分隔符空格、逗号、左右括号、分号、字符串结束 */ static int readToken(char **pp, char *out, int cap) { char *p skipSpace(*pp); int n 0; while (*p *p ! *p ! , *p ! ( *p ! ) *p ! ;) { if (n cap - 1) out[n] (char)tolower((unsigned char)*p); p; } out[n] \0; *pp p; return n; }readToken有两个设计点值得说明。一是它统一转小写这样用户在终端敲CREATE TABLE还是create table都能认SQL 关键字本来就不区分大小写。二是它把(、)、,、;全部当分隔符这几个符号在 CREATE 语句里只起结构作用不参与内容让它们在读 token 时天然被切开主流程就不用自己做if (*p ()这种逐字符判断了。cap参数是防溢出的调用方传LEN进来函数内部做n cap - 1判断。定长数组项目里最怕的就是用户敲一长串字符把栈冲了加这么一个上限判断成本几乎为零。3.2 主流程create → table → 表名 → 括号 → 列定义循环有了readToken主流程就变成一串“读一个、比一个”的动作。int SQL_CREATE(char *sql) { char kw[LEN], tname[LEN]; char *p sql; int t; if (!readToken(p, kw, LEN) || strcmp(kw, create) ! 0) { printf(\nCREATE 拼写有误请重新输入\n); return 0; } if (!readToken(p, kw, LEN) || strcmp(kw, table) ! 0) { printf(\nTABLE 拼写有误请重新输入\n); return 0; } if (!readToken(p, tname, LEN)) { printf(\n缺少表名\n); return 0; } if (findTable(tname) 0) { printf(\n表 %s 已存在\n, tname); return 0; } if (tableCount MAX_TABLE) { printf(\n表数量已达上限\n); return 0; } t tableCount; strcpy(tableName[t], tname); proNum[t] 0; p skipSpace(p); if (*p ! () { printf(\n表名后必须跟左括号\n); return 0; } p; while (1) { /* 一列一轮 */ char fname[LEN], ftype[LEN]; if (!readToken(p, fname, LEN)) { printf(\n列定义不完整\n); return 0; } if (!readToken(p, ftype, LEN)) { printf(\n缺少列类型\n); return 0; } if (!addColumn(t, fname, ftype, p)) return 0; p skipSpace(p); if (*p ,) { p; continue; } /* 还有下一列 */ if (*p )) { p; break; } /* 列定义结束 */ printf(\n列定义之间缺少逗号或右括号\n); return 0; } tableCount; /* 只有整条语句都成功才提交 */ return 1; }这段代码最值得学的是“提交时机”。t tableCount先占位但tableCount放在最后中间任何一步失败都直接return 0表数量不增加等于这次建表被丢弃。原始代码里是在函数开头就Pro_Num[number] num; number;解析到一半发现类型写错退出时表和列数已经算进去了后面的表就全错位。这是典型的事务思路在几十行代码里的最小应用——不要求你写日志和回滚至少做到“要么全成要么不动”。addColumn负责一列内部的细节签名为addColumn(t, fname, ftype, p)最后一个参数是游标指针的地址函数内部要继续往后读长度和约束读完了把游标位置回写给调用方。3.3 类型、长度与约束三种约束怎么落到编码上类型只认五个关键字任务书里已经给死了char、int、float、double、date。做法是一张静态表加一个循环匹配。static const char *kLegalType[] {char, int, float, double, date}; static int addColumn(int t, char *fname, char *ftype, char **pp) { int i, n proNum[t]; char *p; if (findField(t, fname) 0) { printf(\n列名 %s 重复\n, fname); return 0; } for (i 0; i 5; i) if (strcmp(ftype, kLegalType[i]) 0) break; if (i 5) { printf(\n不支持的类型 %s\n, ftype); return 0; } strcpy(fieldName[t][n], fname); strcpy(fieldType[t][n], ftype); strcpy(fieldCond[t][n], NULL); p skipSpace(*pp); if (*p () { /* char(20) 这类长度修饰 */ int len 0, digits 0; p; while (*p 0 *p 9) { len len * 10 (*p - 0); p; digits; } if (digits 0 || *p ! )) { printf(\n长度写法有误\n); return 0; } p; sprintf(fieldType[t][n] strlen(fieldType[t][n]), (%d), len); } if (!parseConstraint(t, n, p)) return 0; *pp p; proNum[t]; /* 这一列确认完整列数才加一 */ return 1; }长度解析用累加len len * 10 (*p - 0)这样char(100)能算出 100char(5)算 5两位三位都行。如果长度写成字母digits会是 0直接报错如果数字后面不是)比如用户漏了右括号也报错。这两种错误如果不拦后面游标位置就全乱了。约束解析单独抽成parseConstraint因为它要处理带空格的组合词static int parseConstraint(int t, int n, char **pp) { char tok[LEN]; char *p skipSpace(*pp); if (*p , || *p ) || *p ;) return 1; /* 没写约束 */ if (!readToken(p, tok, LEN)) return 1; if (strcmp(tok, primary) 0) { if (!readToken(p, tok, LEN) || strcmp(tok, key) ! 0) { printf(\nPRIMARY 后面必须跟 KEY\n); return 0; } strcpy(fieldCond[t][n], PK); } else if (strcmp(tok, unique) 0) { strcpy(fieldCond[t][n], UQ); } else if (strcmp(tok, not) 0) { if (!readToken(p, tok, LEN) || strcmp(tok, null) ! 0) { printf(\nNOT 后面必须跟 NULL\n); return 0; } strcpy(fieldCond[t][n], NN); } else { printf(\n不认识的约束条件 %s\n, tok); return 0; } *pp p; return 1; }这里我把primary key和not null都拆成两个 token 读而不是去字符串里找子串。为什么不用strstr因为strstr会被列名骗到——你建一个叫primary_count的列strstr一样能匹配上。按 token 顺序读是唯一稳妥的方式代价只是多写两行。写完之后回头看一眼函数链SQL_CREATE管语句级结构addColumn管列级结构parseConstraint管约束级结构三层各管各的加新约束只要动最后那个else if。这种拆法比原始代码里那个一百多行、所有逻辑堆在一个函数里的版本舒服太多出问题时也知道该去哪个函数打printf。4. ALTER TABLE 与读表回填改结构比建结构难在哪4.1 先找表findTable 与大小写改表的第一步是确认表存在。写个findTable返回下标找不到返回 -1这个返回值约定要在所有调用点保持一致。/* 返回表在数组中的下标不存在返回 -1 */ int findTable(const char *name) { int i; for (i 0; i tableCount; i) if (strcmp(tableName[i], name) 0) return i; return -1; } /* 返回某张表里列的下标不存在返回 -1 */ int findField(int t, const char *name) { int i; for (i 0; i proNum[t]; i) if (strcmp(fieldName[t][i], name) 0) return i; return -1; }大小写问题在这里会咬人。readToken把 token 转小写了所以建表时存下来的表名和列名全是小写如果用户在 ALTER 里敲ALTER TABLE Student ADD ...也走readToken转小写能对上。但如果哪天为了“显示得好看”把表名按用户输入的原样存下来那 ALTER 就永远找不到表。我的做法是全流程统一小写显示时也不还原宁可难看也不出错。4.2 ADD / DROP / MODIFY 三条分支的实现差异ALTER 的入口和 CREATE 结构相似读完alter、table、表名之后靠下一个 token 分发到三条分支。int SQL_ALTER(char *sql) { char kw[LEN], tname[LEN]; char *p sql; int t; if (!readToken(p, kw, LEN) || strcmp(kw, alter) ! 0) { printf(\nALTER 拼写有误\n); return 0; } if (!readToken(p, kw, LEN) || strcmp(kw, table) ! 0) { printf(\nTABLE 拼写有误\n); return 0; } if (!readToken(p, tname, LEN)) { printf(\n缺少表名\n); return 0; } t findTable(tname); if (t 0) { printf(\n表 %s 不存在\n, tname); return 0; } if (!readToken(p, kw, LEN)) { printf(\n缺少 ALTER 子句\n); return 0; } if (strcmp(kw, add) 0) return alterAdd(t, p); if (strcmp(kw, drop) 0) return alterDrop(t, p); if (strcmp(kw, modify) 0) return alterModify(t, p); printf(\n不支持的 ALTER 子句 %s\n, kw); return 0; }ADD 分支实际上是复用addColumn因为“往表里加一列”和“建表时定义一列”在结构上完全一样。static int alterAdd(int t, char **pp) { char fname[LEN], ftype[LEN]; if (proNum[t] MAX_FIELD) { printf(\n列数已达上限\n); return 0; } if (!readToken(pp, fname, LEN)) { printf(\nADD 缺少列名\n); return 0; } if (!readToken(pp, ftype, LEN)) { printf(\nADD 缺少类型\n); return 0; } return addColumn(t, fname, ftype, pp); /* 类型校验、长度、约束全走同一条路 */ }DROP 要麻烦一点因为它涉及数组元素的“删除”——把目标列之后的所有列整体前移一格然后把列数减一。static int alterDrop(int t, char **pp) { char name[LEN]; int idx, k; if (!readToken(pp, name, LEN)) { printf(\nDROP 缺少列名\n); return 0; } if (strcmp(name, column) 0) /* 兼容 DROP COLUMN x 的写法 */ if (!readToken(pp, name, LEN)) { printf(\nDROP COLUMN 缺少列名\n); return 0; } idx findField(t, name); if (idx 0) { printf(\n列 %s 不存在\n, name); return 0; } if (proNum[t] 1) { printf(\n至少要保留一列\n); return 0; } for (k idx; k proNum[t] - 1; k) { /* 后面整体前移 */ strcpy(fieldName[t][k], fieldName[t][k 1]); strcpy(fieldType[t][k], fieldType[t][k 1]); strcpy(fieldCond[t][k], fieldCond[t][k 1]); } proNum[t]--; printf(列 %s 已删除\n, name); return 1; }前移循环的边界是k proNum[t] - 1最后一列特殊——它不需要被复制直接靠proNum[t]--把它“切掉”。这个写法比先memset清理最后一格再减一更简洁因为被切掉的那格数据反正不会再被访问。MODIFY 只改类型不动列的位置static int alterModify(int t, char **pp) { char name[LEN], type[LEN]; int idx, i; if (!readToken(pp, name, LEN)) { printf(\nMODIFY 缺少列名\n); return 0; } if (!readToken(pp, type, LEN)) { printf(\nMODIFY 缺少新类型\n); return 0; } idx findField(t, name); if (idx 0) { printf(\n列 %s 不存在\n, name); return 0; } for (i 0; i 5; i) if (strcmp(type, kLegalType[i]) 0) break; if (i 5) { printf(\n不支持的类型 %s\n, type); return 0; } strcpy(fieldType[t][idx], type); { /* 新类型如果带长度修饰一起拼上去 */ char *p skipSpace(*pp); if (*p () { int len 0, digits 0; p; while (*p 0 *p 9) { len len * 10 (*p - 0); p; digits; } if (digits 0 *p )) { sprintf(fieldType[t][idx] strlen(fieldType[t][idx]), (%d), len); } } } printf(列 %s 的类型已改为 %s\n, name, fieldType[t][idx]); return 1; }三条分支的差别其实就三句话ADD 是末尾追加DROP 是中间挖洞后前移MODIFY 是原地改字。理解了这个差异再去看那些把 ALTER 写了一百多行、三种情况全揉在一个if-else瀑布里的代码就知道该往哪切了。4.3 载入与保存loadAll / saveAll 的对称性存取这一对函数是全文最需要小心的地方因为它决定了程序能不能“记住”上次建的表。int saveAll(const char *path) { FILE *fp fopen(path, w); int t, i; if (!fp) { printf(\n无法写入 %s\n, path); return 0; } fprintf(fp, %d\n, tableCount); /* 第一行表数量 */ for (t 0; t tableCount; t) { fprintf(fp, %s %d\n, tableName[t], proNum[t]); /* 表名 列数 */ for (i 0; i proNum[t]; i) { fprintf(fp, %s %s %s\n, fieldName[t][i], fieldType[t][i], fieldCond[t][i]); } } fclose(fp); return 1; }saveAll用的是“全量覆盖”先fopen(path, w)把文件清空再从头写一遍。原始代码也是这个思路注释里写得很明白改一张表之后想定位到原位置再局部覆盖太麻烦不如全部重写。代价是如果要存十万张表会很慢但这个规模最多十几张表无所谓。loadAll必须严格按saveAll的行序读回来而且要用“整行读 sscanf”的方式不能直接fscanf(fp, %s)一个个 token 读。int loadAll(const char *path) { FILE *fp fopen(path, r); char line[256]; int t, i; if (!fp) return 0; /* 首次运行没有文件不算错误 */ if (!fgets(line, sizeof(line), fp)) { fclose(fp); return 0; } tableCount atoi(line); if (tableCount 0 || tableCount MAX_TABLE) { tableCount 0; fclose(fp); return 0; } for (t 0; t tableCount; t) { if (!fgets(line, sizeof(line), fp)) break; /* 表头行 */ if (sscanf(line, %63s %d, tableName[t], proNum[t]) ! 2) break; if (proNum[t] 0 || proNum[t] MAX_FIELD) { proNum[t] 0; break; } for (i 0; i proNum[t]; i) { if (!fgets(line, sizeof(line), fp)) break; /* 一列一行 */ if (sscanf(line, %63s %63s %63s, fieldName[t][i], fieldType[t][i], fieldCond[t][i]) ! 3) break; } } fclose(fp); return 1; }fgets拿整行、sscanf再从行里切三个%63s这个组合的好处是每一列占据固定的一行读错一行不会像 token 流那样把后面全带歪。%63s的数字 63 对应LEN - 1一定要写不写就是标准的缓冲区溢出。5. 避坑与排查五个调了整晚的问题5.1 现象重启后第二张表读不出来第三张表的列全串了原因存储格式里约束条件带空格比如not null、primary key。用fscanf(fp, %s)或ioFile str这类按空白切分的方式读会把not读进约束字段把null当成下一行的开头从此整个文件的行列对齐全部错位。原始代码里正是用ioFile _condition1[i]再判断是不是not的方式补救但补救本身又引入了“读到什么算对”的歧义。解决约束存盘时编码成无空格的单 tokenPK/UQ/NN/NULL显示的时候再翻译回去。或者退一步用fgets整行读、sscanf按位置切这样即使约束里带空格只要它固定在第三个字段也能用%63[^\n]把整行剩余部分吃掉。两种都行我倾向前者因为编码后的文件肉眼可读性更好也方便 diff。5.2 现象编译报 “cannot convert const char* to char”原因代码里出现了这种写法fieldCondition[number][i] primary key;——把一个字符串字面量的地址赋给一个char元素或者在某些老编译器里被隐式截断成指针编译期直接报错就算强行用(char)转过去编译通过运行时也是一次野指针访问。解决字符串赋值一律用strcpy/strncpy/sprintf并且保证目标有足够空间。/* 错 */ fieldCond[t][n] PK; /* 对 */ strcpy(fieldCond[t][n], PK);顺带说一个同类问题char name[20]; name abc;也一样不合法数组名是常量地址不能重新赋值。这类错误在老版本的 C 教材里经常被一笔带过真上手写才会撞上。5.3 现象程序读回文件后最后一张表重复出现一次原因循环条件写成while (!ioFile.eof())或while (!feof(fp))。eof标志不是“读到末尾就为真”而是“上一次读取尝试失败之后才为真”。所以文件最后一个字符读完后循环体还会再执行一轮此时所有变量都是上一轮的残留值于是数据被重复写了一遍。解决不要用eof做循环条件改用读取函数的返回值做前置判断。/* 错 */ while (!feof(fp)) { fscanf(fp, %s, buf); /* 最后一次 buf 是上一轮的残留 */ } /* 对 */ while (fgets(line, sizeof(line), fp) ! NULL) { /* 只有真读到内容才进循环 */ }同理C 里的while (!ioFile.eof())换成while (ioFile str)前者是坑后者是标准写法。5.4 现象建char(50)的列显示出来是char(D)原因长度转换只处理了个位。f_t_len[...][1] f_length 48;当f_length是 50 时50 48 98对应字符b当f_length是 20 时是 68对应D。位数越多错得越离谱。而演示时大家都习惯随手写char(10)104858是:看起来也怪但很容易被当成下标偏移忽略过去。解决不要用48手工转字符直接sprintf或者itoa。如果真要手工转得先算出位数从高位到低位逐位取余代码量反而更多。/* 我一般这么写省事且没有位数限制 */ sprintf(fieldType[t][n] strlen(fieldType[t][n]), (%d), len);5.5 现象换一台机器跑之前建的表全没了还静悄悄地建了个空文件原因路径写死成绝对路径比如C:\\Users\\Administrator\\Desktop\\DBMS.txt。换台机器这个目录不存在fopen或fstream::open返回失败但代码里没判断返回值程序认为“文件是空的”于是从tableCount 0开始退出时saveAll用w模式打开创建一个新文件把原来的数据目录彻底覆盖成空的。解决路径改成相对路径table.txt和工作目录绑定同时在fopen之后必须判断返回值读失败就把tableCount清零并提示写失败就打印错误并放弃保存而不是继续往下跑。更进一步保存时可以写成table.txt.tmp写成功后再rename覆盖正式文件这样中途崩了也不会毁掉上一份数据。6. 二次开发在这个骨架上加数据行与图形界面这个骨架最值得继续做的一件事是把“只存结构”推进到“能存数据”。任务书里只要求建表和改表但从建表到插数据只差一步CREATE TABLE已经把列数、类型、长度都记下来了完全可以按定长记录往另一个文件里写行。具体做法是给每张表加一个数据文件表名.dat每行对应一条记录各字段按fieldType里的长度定义补齐。int固定 4 字节char(n)固定 n 字节写的时候用fwrite按字节写读的时候按同样的偏移量fread回来。这样ALTER TABLE ADD一列之后旧文件就会读不了得在表头里记一个版本号或者列数读到不匹配就提示“结构已变更数据文件需要重建”。这是个很真实的工程问题比单纯解析 SQL 有意思得多。改造点我整理成这张表按难度从低到高改造点主要改动位置验证方式结构变更后立即落盘SQL_CREATE/SQL_ALTER末尾加saveAll建表后强杀进程重启看表还在不在支持多列约束新增tableCond数组解析完所有列后再扫一遍表级约束建primary key(sno, cno)的复合主键表加数据行存储新增insertRow/selectRow每表一个.dat文件建表、插三条、重启、再查数据不丢图形界面把四个函数原样搬到按钮回调里输入框取文本丢给SQL_CREATE按钮点一遍结果和命令行一致图形界面这块我要单独说一句。任务书写了“命令行和图形化界面两种形式”很多人卡在这里是因为把解析逻辑和界面绑在一起了。只要SQL_CREATE(char *sql)的入参是一个字符串、返回是一个 0/1界面就只是往这个函数里传字符串而已。VC6 里的 Win32 对话框程序或者 Code::Blocks 里的 wxWidgets 都行核心一行都不用改。当年用 VC6 写的时候还踩过一个纯编译器层面的坑VC6 默认按 C89 走变量必须在块的开头声明代码中间写int _i 0;会报 C2143。改法很简单把声明提到块首或者直接把工程编译选项切到 C。验证方法我习惯用一个笨办法写一个test.sql把十来条语句按顺序列好——建两张表、给其中一张加一列、改一列类型、删一列、再故意写两条错的——然后写个脚本逐行喂给程序跑完把table.txt拷一份手工核对一遍再重启程序Show()一次两边的输出必须一模一样。这个流程走一遍存取不对称、游标错位、覆盖丢数据这几类问题基本都能炸出来。从那以后我每次接这种“字符串进、结构出”的小工具都强制先把存和取写成一对、跑通一次往返再往下写功能绝不先写本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →