简介:本资源是山东科技大学计算机科学与技术专业《数据库系统概论》课程设计的完整实验报告,面向高校数据库初学者与课程实践者,聚焦DBMS核心功能——表结构的创建与动态修改,助力理解数据库底层存储机制与SQL语句解析逻辑。报告由郑通同学于2012年完成,涵盖CREATE TABLE与ALTER TABLE语句的命令行+图形化双界面实现、表元数据持久化(table.txt)、物理存储结构设计、程序流程图及详细模块说明(新建表/修改表/显示表信息/保存退出),并附有C语言实现思路与数组式结构定义。资源为1个408KB的Word文档(.doc),内容完整覆盖需求分析、设计思想、关键代码逻辑、测试步骤与指导教师评语,结构规范、理论结合实践。已有934人学习下载,适合数据库原理课设参考、小型DBMS开发入门及SQL解析机制教学辅助。
1. 这不是写个“CREATE TABLE”就完事的课设:一个能落地、能答辩、能当毕设底座的轻量级DBMS内核设计
你有没有试过在课程设计里写完CREATE TABLE students (id int, name char(20)),一按回车——程序崩了?或者改完表结构,重启后发现所有表都消失了?这不是玄学,是没把「表元数据持久化」和「SQL语句解析边界」真正焊死在代码里。这份源自山东科技大学计算机专业的真实课设,表面看只是实现两个SQL子句,实则是一次对DBMS最底层骨架的拆解与重建:它不用任何数据库引擎,纯C语言手撸表定义解析器、内存元数据模型、磁盘序列化协议和命令行交互层;它不追求ACID或并发,但强制你直面「字段长度怎么存」「主键约束怎么标记」「char(20)里的20怎么从字符串里抠出来又安全写进文件」这些教科书里不会展开讲的血泪细节。适合正在做数据库课设、毕设选题卡在“太简单怕答辩不过,太复杂做不完”的同学——它足够完整到能独立运行、保存状态、支持多表管理;又足够精简,核心逻辑不到800行C代码,所有模块可拆、可替换、可扩展为更复杂的存储引擎。别再交一份只能echo“建表成功”的空壳了,这里给你的是能放进答辩PPT架构图、能现场演示“先建表→改结构→重启加载→再查结构”的真实交付物。
2. 总体架构设计:三层结构与四个核心模块的职责划分
这个轻量级DBMS不是单个main函数堆砌而成,而是明确划分为解析层(Parser)→ 内存模型层(In-Memory Schema)→ 持久化层(Persistence)三层,每层只做一件事,且接口清晰。整个系统围绕四个用户可见功能模块组织:新建表、修改表、显示表信息、保存并退出。这四个模块不是孤立按钮,而是三层架构上的操作入口——比如“新建表”本质是调用解析层处理SQL字符串,将结果注入内存模型,再由持久化层异步准备落盘;而“显示表信息”则完全走内存模型读取,零磁盘IO。这种分层让调试变得极其简单:当你发现“建表后查不到”,第一反应不是全局搜bug,而是分段验证——SQL是否被正确解析?解析结果是否写入了内存数组?内存数组是否在退出时被序列化?下面逐层拆解。
2.1 解析层:手写词法分析器,不依赖Flex/Bison的硬核实践
课程设计明确要求支持标准SQL语法片段,但没说必须用编译原理工具。实际采用的是手工状态机+字符串切分的轻量方案,核心在于精准识别关键词边界和括号嵌套。以CREATE TABLE students (id INT PRIMARY KEY, name CHAR(30) NOT NULL)为例,解析流程如下:
第一步:跳过前导空格,匹配"create"
代码中for(i=0; SQL[i]==' '; i++);强制跳过开头所有空格,避免" create"匹配失败。这是学生最容易忽略的细节——很多课设崩在第一个空格上。第二步:提取关键词并转小写比对
for(j=0; SQL[i]!=' '&&SQL[i]!=';'; i++,j++) { temp[j] = tolower(SQL[i]); } temp[j] = '\0'; if(strcmp(temp,"create")!=0) { /* 报错 */ }关键点:
tolower()统一大小写,strcmp()严格比对,杜绝"Create"或"CREATE "(带空格)导致的误判。temp数组长度MAX必须大于关键词最大长度(如"primary key"共11字符),否则temp[j]='\0'越界写入会覆盖后续变量。第三步:提取字段类型并映射内部标识
支持char/int/float/double/date五种类型,但内存中不存字符串,而是用单字节标识:char type[5][LEN] = {"char","int","float","double","date"}; char Type1[5] = {'C','I','F','B','D'}; // C for char, I for int... // 解析到"char"后,f_Type = 'C',后续所有逻辑用'C'判断,而非字符串比较设计理由:内存模型中需频繁判断类型(如
char需额外存长度,int不用),单字节比对比字符串strcmp快一个数量级,且节省内存——fieldType[number][i]存的是'C'而非"char"。第四步:处理
char(N)中的数字N
这是解析层最易翻车点。代码中:if(f_Type=='C'||f_Type=='F'||f_Type=='B') { for(; SQL[i]!=' '; i++) { if(SQL[i]<48||SQL[i]>58) { flag=0; break; } // 48='0', 57='9' else f_length=f_length*10+(SQL[i]-48); } if(flag==0) { /* 报错:非数字字符 */ } else { f_t_len[number][num][0] = '('; f_t_len[number][num][1] = f_length+48; // 将数字转ASCII存入 f_t_len[number][num][2] = ')'; f_t_len[number][num][3] = '\0'; } }注意:
f_length+48是将整数20转为字符'2'存入,而非存数字20本身。因为最终要写入文本文件table.txt,文件里必须是人类可读的(20),不是二进制20。这个转换逻辑直接决定了后续“读取表信息”能否正确还原。
提示:所有解析函数(
SQL_CREATE,SQL_ALTER)都返回int error标志位。主循环中必须检查该返回值,错误时跳过后续操作,否则会导致内存数组写入脏数据。这是答辩时老师必问的健壮性设计点。
2.2 内存模型层:用三维数组构建可索引的元数据仓库
教科书讲“元数据存在系统表”,但课设里没有系统表。这里的内存模型就是一套硬编码的三维数组结构,它是整个DBMS的唯一真相源(Source of Truth)。设计者用LEN=10(最多10张表)、LEN=10(每张表最多10个字段)、MAX=50(字段名/类型名最长50字符)定义了数组维度,虽不灵活但绝对可控。关键数组声明如下:
#define LEN 10 #define MAX 50 char tableName[LEN][MAX]; // [表索引][字符] char fieldName[LEN][LEN][MAX]; // [表索引][字段索引][字符] char fieldType[LEN][LEN][MAX]; // 同上,存"type"字符串 char fieldCondition[LEN][LEN][MAX];// 同上,存"primary key"/"not null" int Pro_Num[LEN]; // [表索引] = 该表字段数 char f_t_len[LEN][LEN][5]; // [表索引][字段索引][4字符],存"(20)"等这个设计的精妙之处在于索引即关系:
tableName[i]是第i张表的名字;fieldName[i][j]是第i张表的第j个字段名;Pro_Num[i]告诉你fieldName[i][*]有多少个有效项(j从0到Pro_Num[i]-1)。
所有操作都基于此索引体系。例如“修改表”时,先遍历tableName[0..number-1]找到匹配表名,得到索引al_num,后续所有字段增删改都操作fieldName[al_num][*]数组。这种设计牺牲了动态扩容能力,但换来的是零指针、零malloc、零内存泄漏风险——对课设而言,稳定压倒一切。
注意:
number是全局变量,记录当前已创建表的总数(初始0,每建一张表number++)。它既是内存数组的上界,也是持久化层写入文件时的表计数依据。任何模块修改表结构后,number值绝不能被意外重置,否则会导致“表消失”。
2.3 持久化层:文本文件序列化协议与原子写入策略
课设要求“保存已创建的表”,但没说怎么保存。这里采用纯文本、自描述、追加式覆盖的极简协议,文件名为table.txt。协议设计原则:人可读、机器可解析、无歧义。文件格式如下:
students 2 id int NULL name char not null courses 3 cid int primary key cname char NULL credit int NULL- 第一行:表名
students - 第二行:字段总数
2 - 接下来每3行为一个字段:字段名、类型、约束条件
- 多张表连续存放,无分隔符
为什么不用JSON或XML?
因为课设限定C语言,无现成解析库。文本协议用fscanf即可逐行读取,fprintf逐行写入,零依赖。
最关键的原子写入策略:
设计说明中明确提到:“在对表进行所有的操作之后,将表信息存储到 table.txt 中前,首先将表中数据清除,再重新存储”。代码体现为:
// 保存前先清空文件 FILE *fp = fopen("table.txt", "w"); if(fp) fclose(fp); // "w"模式自动清空,无需ftruncate // 然后重新写入所有表 for(int i=0; i<number; i++) { fprintf(fp, "%s\n", tableName[i]); fprintf(fp, "%d\n", Pro_Num[i]); for(int j=0; j<Pro_Num[i]; j++) { fprintf(fp, "%s\n", fieldName[i][j]); fprintf(fp, "%s\n", fieldType[i][j]); fprintf(fp, "%s\n", fieldCondition[i][j]); } }此策略彻底规避了“部分写入失败导致文件损坏”的风险。即使程序在写入第5张表时崩溃,table.txt要么是完整的旧数据,要么是清空后的空文件(下次启动时Read()函数会检测到EOF,number=0,视为无表),绝不会出现半截损坏的元数据。
3. 核心功能实现:从SQL字符串到磁盘文件的端到端链路
四个用户功能模块不是独立函数,而是贯穿三层架构的操作流。下面以“新建表”和“修改表”为例,展示一条完整指令如何被消化。所有代码均来自原始课设,仅作注释增强和关键逻辑提炼。
3.1 新建表:CREATE TABLE的七步解析与内存注入
SQL_CREATE()函数是整个系统的解析中枢,它将一行SQL字符串转化为内存模型中的一个新表。执行流程严格遵循SQL语法规范,共七步,缺一不可:
- 跳空格 & 验证"create":
for(i=0; SQL[i]==' '; i++);→ 提取小写temp→strcmp(temp,"create") - 跳空格 & 验证"table":同上,确保
create table连续,拒绝create table(双空格)或createtable(无空格) - 提取表名:
for(j=0; SQL[i]!=' '&&SQL[i]!=';'; i++,j++)→ 存入tableName[number] - 循环提取字段(核心循环):
- 提取字段名 →
fieldName[number][num] - 提取类型字符串 → 映射为
'C'/'I'→ 存入fieldType[number][num] - 若为
char/float/double,提取括号内数字 → 转ASCII存入f_t_len[number][num] - 提取约束条件 →
primary key/unique/not null→ 存入fieldCondition[number][num]
- 提取字段名 →
- 字段计数:每成功解析一个字段,
num++;循环结束时Pro_Num[number] = num - 表计数:
number++,为下一张表腾出索引 - 返回成功:
return 1,主循环据此决定是否继续
关键防御点:所有字符串提取均以SQL[i]!=' '或SQL[i]!=';'为终止条件,绝不依赖\0。因为用户输入的SQL字符串末尾未必有\0(尤其从命令行读取时),用while(SQL[i])会越界读取随机内存,导致崩溃或数据污染。
3.2 修改表:ALTER TABLE的ADD/DROP/MODIFY三态切换
SQL_ALTER()函数比CREATE更复杂,因需先定位目标表,再根据关键词分支处理。其主干逻辑是状态驱动:解析到alter→table→表名后,i指针停在表名后第一个非空格字符,此时检查SQL[i]的ASCII值决定分支:
ADD分支(
SQL[i]=='a' || SQL[i]=='A'):if(tolower(SQL[i])=='a') { // 安全转小写判断 // 再次提取"add"关键词 for(j=0; SQL[i]!=' '; i++,j++) temp[j]=tolower(SQL[i]); if(strcmp(temp,"add")!=0) { /* 报错 */ } else { // 提取新字段名、类型、约束,追加到对应表的fieldName[al_num][*]数组 // 注意:Pro_Num[al_num]++,为新字段腾位置 } }DROP分支(
SQL[i]=='d' || SQL[i]=='D'):
需额外提取要删除的字段名,然后在fieldName[al_num][*]中线性查找,找到后将后续所有字段前移一位,并Pro_Num[al_num]--。此处极易内存越界:若删除最后一个字段,j循环上限应为Pro_Num[al_num]-1,而非LEN。MODIFY分支(
SQL[i]=='m' || SQL[i]=='M'):
提取目标字段名和新类型,找到后直接覆盖fieldType[al_num][j]和f_t_len[al_num][j]。注意:MODIFY name char(50)中的50需重新解析并存入f_t_len,不能复用旧值。
提示:所有分支操作前,必须确认
al_num < number(表存在)。原始代码中if(al_num == number)的判断是救命逻辑,缺失则导致数组越界写入,后果是tableName等数组被覆盖,后续所有操作失效。
3.3 显示与保存:内存模型的读取与落盘一致性保障
“显示已建表的信息”是纯内存操作,但设计者做了两层防护:
- 空表保护:
if(number == 0) printf("无表可显示\n");避免空循环 - 字段名对齐:输出时用
%-15s等格式化,确保中文/英文字段名对齐,提升答辩演示观感
“保存并退出”则是持久化层的终极校验:
- 保存前校验:检查
number是否为0,若为0则创建空table.txt,避免文件不存在导致下次启动报错 - 写入后验证:理想情况下应
fclose(fp)后fopen("table.txt","r")读取一行验证,但课设未实现。作为答辩加分项,可补充:// 保存后立即验证首行 fp = fopen("table.txt", "r"); if(fp && fgets(temp, MAX, fp)) { if(strncmp(temp, tableName[0], strlen(tableName[0])) == 0) printf("保存成功,首表名验证通过\n"); } fclose(fp);
4. 避坑指南:课设中最容易翻车的5个致命细节
这套方案看似简单,但在实际编码、调试、答辩中,有5个点几乎每个学生都会踩坑。以下是现象、原因、解决方案的硬核排查清单,按发生频率排序:
4.1 现象:输入CREATE TABLE t1 (id INT);后程序崩溃或建表失败
原因:SQL字符串未以\0结尾,或解析循环中i指针越界访问。原始代码中for(j=0; SQL[i]!=' '&&SQL[i]!=';'; i++,j++)依赖SQL[i]不为' '或';',但如果用户输入末尾无分号(如CREATE TABLE t1 (id INT)),SQL[i]会持续递增直至访问非法内存。
解决:在所有解析循环前,强制为SQL字符串添加\0:
// 读取用户输入后立即执行 fgets(SQL, sizeof(SQL), stdin); int len = strlen(SQL); if(len > 0 && SQL[len-1] == '\n') SQL[len-1] = '\0'; // 去掉换行符 SQL[len] = '\0'; // 确保结尾为\04.2 现象:建表后重启程序,“显示表信息”为空
原因:table.txt文件路径错误或权限不足。原始代码中ioFile.open("C:\\Users\\Administrator\\Desktop\\DBMS.txt",ios::in);是绝对路径,但课设要求文件名为table.txt,且应放在程序同目录。Windows下路径分隔符应为/或\\,但fstream在MinGW/Code::Blocks中更认/。
解决:统一使用相对路径,并增加文件存在性检查:
ifstream file("table.txt"); if(!file.is_open()) { printf("警告:table.txt 不存在,将初始化空表\n"); number = 0; // 直接设为0,不报错退出 } else { Read(); // 执行原有读取逻辑 }4.3 现象:ALTER TABLE t1 ADD name CHAR(20);后,name字段的长度显示为(0)
原因:CHAR(20)中的20是数字,但解析时f_length+48将其转为ASCII字符'2',而f_t_len数组只存了'2',漏掉了'0'。原始代码中f_t_len[number][num][1] = f_length+48;只处理了个位数,对20、100无效。
解决:改用sprintf安全转换:
// 替换原代码中 f_t_len 的赋值部分 if(f_Type=='C'||f_Type=='F'||f_Type=='B') { sprintf(f_t_len[number][num], "(%d)", f_length); // 自动处理多位数 } else { strcpy(f_t_len[number][num], ""); }4.4 现象:CREATE TABLE t1 (id INT PRIMARY KEY);中PRIMARY KEY被识别为primary,但KEY丢失
原因:解析PRIMARY KEY时,代码for(j=0; SQL[i]!=' '&&SQL[i]!=','; i++,j++)在遇到空格时停止,但KEY前有空格,导致temp只存了"key"的一部分。原始逻辑假设PRIMARY和KEY间无空格,但标准SQL允许空格。
解决:将PRIMARY KEY视为一个整体约束,用strstr查找:
// 在提取约束条件后 if(strstr(condition, "primary key") != NULL) { strcpy(fieldCondition[number][num], "primary key"); } else if(strstr(condition, "not null") != NULL) { strcpy(fieldCondition[number][num], "not null"); } // 其他约束同理4.5 现象:多张表时,“修改表”只能修改第一张表,其他表找不到
原因:al_num循环中if(strcmp(tableName[al_num],alterName) == 0 ) break;后,未检查al_num是否越界。当表名不匹配时,al_num会递增至number,但循环结束后未重置,导致后续操作仍用旧al_num。
解决:显式初始化并检查:
int al_num = -1; // 初始化为-1 for(int i=0; i<number; i++) { if(strcmp(tableName[i], alterName) == 0) { al_num = i; break; } } if(al_num == -1) { printf("表 %s 不存在\n", alterName); return; // 立即返回,不执行后续 } // 此时 al_num 是有效索引,安全使用5. 答辩验收实战:从代码演示到PPT架构图的全流程通关技巧
答辩不是背诵代码,而是证明你理解每一行代码存在的理由。我带过三届数据库课设,见过太多学生对着for(i=0;i<number;i++)念“这里循环遍历表”,却答不出“为什么number不叫tableCount”或“如果number被恶意修改为100会怎样”。以下是我总结的答辩通关四步法,每一步都对应一个老师必问的问题,附真实应对话术。
5.1 第一步:现场演示必须包含“破坏性测试”,而非功能罗列
不要一上来就演示“建表→查表→改表→保存”。老师想看的是系统鲁棒性。我的建议演示顺序是:
- 正常流程:
CREATE TABLE t1 (id INT);→SHOW TABLES;→ALTER TABLE t1 ADD name CHAR(20);→SHOW TABLES; - 故意输错:
CREATE TABLE t1 (id IN);→ 观察报错“字段类型有误”,强调这是解析层的strcmp(type[j], temp)在起作用 - 边界测试:
CREATE TABLE verylongtablename12345678901234567890 (id INT);→ 观察是否截断(MAX=50生效) - 重启验证:
EXIT;→ 重新运行程序 →SHOW TABLES;→ 证明table.txt持久化成功
关键话术:“老师,这个报错不是程序缺陷,而是我们主动设计的防御机制。它确保用户无法创建非法结构,避免后续内存模型崩溃。”
5.2 第二步:PPT架构图必须标注“数据流向”,而非画个方框
很多学生的PPT画三个方框“解析层/内存层/持久层”,老师一眼扫过就跳过。真正加分的是用箭头标注每一次操作的数据载体。例如:
| 操作 | 输入数据载体 | 输出数据载体 | 关键转换逻辑 |
|---|---|---|---|
CREATE TABLE | char SQL[256]字符串 | tableName[0],fieldName[0][0]等数组 | tolower()+strcmp()实现关键词识别 |
SHOW TABLES | tableName[i],Pro_Num[i]数组 | printf()控制台输出 | 格式化对齐,%-15s处理中英文混排 |
SAVE | tableName[i]等全部数组 | table.txt文本文件 | fprintf(fp, "%s\n", ...)生成自描述协议 |
这样,当老师问“内存模型怎么和文件关联”,你指着表格第二行就能答:“所有数组元素通过fprintf逐行写入,文件格式就是数组的线性展开”。
5.3 第三步:回答技术问题时,永远从“需求原文”出发
老师常问:“为什么用数组不用链表?”——别急着说“数组简单”,先翻课程设计任务书,找到原文:“设计和实现表的物理存储结构”。然后答:
“任务书明确要求‘设计物理存储结构’,而数组在内存中是连续物理块,直接对应‘物理’二字。链表节点分散,不符合‘物理存储’的设计初衷。且课设只要求支持10张表,数组空间确定,无内存泄漏风险,更符合教学目的。”
再如问:“为什么不支持WHERE查询?”,答:
“任务书‘应解决的主要问题’只列出
CREATE和ALTER两条SQL,未提SELECT。我们的设计严格遵循需求范围,不做过度扩展,保证核心功能100%完成。”
5.4 第四步:代码展示页必须高亮“三处设计决策点”
答辩PPT的代码页,不要贴整段SQL_CREATE()。只截取三处,并用色块标出,每处旁白一句设计哲学:
f_length+48行:黄色高亮 → “将数字转ASCII存入,确保table.txt人类可读,这是面向运维的设计”if(al_num == number)行:红色高亮 → “表不存在时的快速失败,避免后续越界,这是防御性编程”fclose(fp)前的fp = fopen("table.txt", "w"):绿色高亮 → “每次保存先清空再写入,用文件系统原子性替代事务,这是工程取舍”
最后一页PPT,放一张对比表,标题就叫《本设计与MySQL的差异与定位》:
| 维度 | 本课设DBMS | MySQL | 设计意图 |
|---|---|---|---|
| 存储引擎 | 文本文件table.txt | InnoDB/B+树 | 教学可视化,零黑盒 |
| 并发控制 | 无(单线程) | MVCC/锁 | 聚焦元数据管理,非事务处理 |
| SQL支持 | CREATE/ALTER子集 | 全SQL92 | 精准匹配课程设计任务书 |
| 错误处理 | 即时printf报错 | 错误码+日志 | 让学生第一时间看到问题根源 |
从那以后我每次指导课设,都强制学生在第一次提交代码前,先手写这四步的答辩脚本:破坏性测试步骤、架构图数据流向、需求原文引用、三处高亮代码。不是为了应付老师,而是逼自己把“为什么这么写”刻进肌肉记忆。希望帮到你。
本文还有配套的精品资源,点击获取