news 2026/10/31 15:06:39

C语言手搓迷你DBMS:从SQL解析到元数据存储的课程设计实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言手搓迷你DBMS:从SQL解析到元数据存储的课程设计实战

简介:这份资源是山东科技大学信息科学与工程学院数据库系统概论课程设计的实验报告,面向计算机科学与技术专业学生及需要完成数据库课程设计的学习者,聚焦“实现一个简单DBMS——创建表与修改表”这一实践任务。压缩包内共1个doc文档,约408KB,完整呈现课程设计说明书,涵盖任务书、设计要求、需求分析、设计思想与程序流程等章节。报告围绕CREATE TABLE与ALTER TABLE两条核心语句展开,讲解表名、列名、数据类型及完整性约束的解析与存储,并说明用数组保存表结构、借助table.txt持久化表信息、实现新建表、修改表、显示表信息与保存退出四个模块的思路,还涉及命令行与图形化界面两种交互方式及物理存储结构设计。目前已有934人学习,适合参考其需求分析、模块划分与存储方案,用于课程设计撰写与DBMS入门实践。

1. 从一份课程设计说起:用 C 语言手搓一个能建表改表的迷你 DBMS

很多人学完《数据库系统概论》,能背出CREATE TABLE的语法,却说不清一条 SQL 从敲下回车到落盘到底发生了什么。这份课程设计资源干的就是把黑匣子拆开的事:用 C 语言实现一个简易 DBMS,支持CREATE TABLE建表、ALTER TABLE增删改列、读取已建表结构、保存表定义到文件,并且要求命令行和图形化界面两种交互方式。它适合正在做数据库课程设计的学生,也适合想搞懂 SQL 解析与元数据存储底层逻辑的开发者。代码不复杂,但把“词法切分—语法校验—结构体存储—文件持久化”这条链路完整走了一遍,拿来当数据库内核的入门脚手架正合适。

2. 技术选型与整体架构:为什么用 C 加文本文件就够了

2.1 技术栈拆解:VC6++ / CodeBlocks 加标准 C

这个项目的技术栈非常朴素,没有任何第三方库依赖。开发环境是 VC6++ 或 CodeBlocks,语言是标准 C,文件操作走fstream或FILE*,界面部分命令行用printf/scanf,图形化用控制台菜单模拟。选这套组合的原因很直接:课程设计周期短,重点在理解 DBMS 的表定义管理逻辑,而不是搭框架。用 C 的好处是内存布局可控,数组、指针、字符串处理都能直接对应到“元数据怎么存”这个问题上。

我一般会把这类项目的依赖收敛到零,因为一旦引入图形库或数据库驱动,编译环境就成了新的坑。这份资源里连string.h、fstream都是标准库自带,换台机器只要编译器支持 C89 就能跑,这对交作业和二次开发都友好。

2.2 四模块架构:新建、修改、显示、保存退出

程序主体围绕四个功能模块展开,主循环里用菜单驱动:

// 主菜单骨架(根据原文设计思想还原) int main() { int choice; Read(); // 启动时先加载已存在的表定义 while (1) { printf("\n1.新建表 2.修改表 3.显示表信息 4.保存并退出\n"); scanf("%d", &choice); switch (choice) { case 1: SQL_CREATE(); break; // 解析并执行 CREATE TABLE case 2: SQL_ALTER(); break; // 解析并执行 ALTER TABLE case 3: ShowTables(); break; // 遍历数组打印表结构 case 4: Save(); return 0; // 全量重写 table.txt } } }

这段骨架的关键在于Read()在循环之前调用,保证程序启动时先把磁盘上的表定义灌进内存数组;Save()放在退出分支,做一次全量落盘。参数上没有花哨配置,choice就是菜单编号,输入非法值时靠default分支兜底。这种“内存数组为主、文件为辅”的设计,本质是把表定义当成一份可整体读写的配置,而不是真正的页式存储,对课程设计来说够用,对理解元数据管理也够直观。

2.3 元数据怎么存:四组三维数组撑起表结构

原文的核心存储结构是四组字符数组,分别存表名、字段名、字段类型、列级约束:

#define LEN 20 #define MAX 50 char tableName[LEN][MAX]; // 表名 char fieldName[LEN][LEN][MAX]; // 字段名:表索引 -> 字段索引 -> 名称 char fieldType[LEN][LEN][MAX]; // 字段类型:int/char/float/double/date char fieldCondition[LEN][LEN][MAX]; // 列级约束:primary key/unique/not null int Pro_Num[LEN]; // 每张表的字段个数 int number; // 当前已建表数量

第一维是表序号,第二维是字段序号,第三维是字符串内容。Pro_Num单独记录每张表有多少列,这样遍历字段时不用靠空字符串判断边界。这种设计的优点是实现快、调试直观,缺点是维度写死,表数量和字段数量都有上限。二次开发时如果要把LEN调大,记得同步检查所有循环边界,否则很容易出现数组越界这种血泪翻车。

2.4 持久化格式:先清空再全量重写的 table.txt

保存逻辑采用了一个很实用的简化策略:每次退出时先把文件内容清空,再把内存里所有表定义重新写一遍。原文的说法是“对表进行修改之后直接在原位置存储比较麻烦”,所以选择全量重写。写入顺序是:先写表名,再写该表字段个数,然后逐个字段写名称、类型、长度标记、约束条件。

// 保存逻辑示意(依据原文保存策略还原) void Save() { FILE *fp = fopen("table.txt", "w"); // "w" 模式即清空重写 for (int i = 0; i < number; i++) { fprintf(fp, "%s %d\n", tableName[i], Pro_Num[i]); for (int j = 0; j < Pro_Num[i]; j++) { fprintf(fp, "%s %s %s %s\n", fieldName[i][j], fieldType[i][j], f_t_len[i][j], // 如 "(20)",无长度则为空 fieldCondition[i][j]); } } fclose(fp); }

这里f_t_len存的是长度标记,比如char(20)会拆成类型char加长度(20)两段。读取时按同样顺序反向解析,遇到primary就读下一个词确认是不是key,遇到not就确认是不是null。这种“写读对称”的格式设计,是这份代码里最值得抄作业的部分:格式简单、可读、出错时直接打开文本文件就能肉眼排查。

3. 核心代码拆解:一条 CREATE TABLE 是怎么被吃进去的

3.1 词法切分:逐字符扫描加 tolower 归一化

SQL_CREATE()的第一件事不是解析表名,而是校验关键字。它用两个指针i、j配合tolower()把输入逐字符转小写后塞进临时缓冲区:

for (i = 0; SQL[i] == ' '; i++); // 跳过前导空格 for (j = 0; SQL[i] != ' ' && SQL[i] != ';'; i++, j++) { temp[j] = tolower(SQL[i]); // 统一转小写,兼容 CREATE/create } temp[j] = '\0'; if (strcmp(temp, "create") != 0) { printf("你输入的'create'有误,请重新创建!\n"); return 0; }

这段代码的意图是做一个极简词法分析器:以空格和分号为分隔符,逐个 token 取出并归一化。tolower保证了大小写不敏感,temp[j] = '\0'保证字符串可比较。参数上SQL是全局输入缓冲区,i是扫描游标,j是当前 token 长度。失败时直接打印中文提示并返回,没有异常机制,靠返回值让上层决定是否继续。这种写法在课程设计里很常见,但要注意SQL缓冲区必须足够大,否则长语句会截断。

3.2 语法校验:create、table 两级关键字确认

关键字校验是串行的:先确认create,再跳过空格确认table,任何一级失败都直接返回错误。原文代码里error变量贯穿整个函数,但实际执行路径上多数分支是直接return或break,error更多是给阅读者看的标记。真正起作用的是strcmp的比较结果和break跳出。

for (; SQL[i] == ' '; i++); // 跳过 create 和 table 之间的空格 for (j = 0; SQL[i] != ' ' && SQL[i] != ';'; i++, j++) { temp[j] = tolower(SQL[i]); } temp[j] = '\0'; if (strcmp(temp, "table") != 0) { printf("你输入的'table'有误,请重新创建!\n"); return 0; }

这里有个容易忽略的细节:table后面必须跟空格再跟表名,如果用户写成createtable,第一级create校验就会失败,因为扫描到空格或分号才停。这种“以空白为界”的切分方式对格式要求较严,用户多敲一个逗号或少一个空格都可能触发误报,这也是后面避坑章节要展开的点。

3.3 表名与字段名提取:游标推进加数组拷贝

关键字通过后,游标i停在table之后,接下来提取表名并写入tableName[number]:

for (; SQL[i] == ' '; i++); // 跳过空格 for (j = 0; SQL[i] != ' ' && SQL[i] != ';'; i++, j++) { t_Name[j] = SQL[i]; // 表名保留原大小写 } int _i = 0; for (_i = 0; _i < j; _i++) tableName[number][_i] = t_Name[_i]; tableName[number][_i] = '\0'; // 手动补结束符

表名提取没有转小写,因为表名通常要求原样保留。字段名提取在do-while循环里做,每轮先跳过空格和逗号,再读到下一个空格为止。这里number是当前表索引,num是当前字段索引,两个游标分开推进,避免互相污染。参数说明:t_Name是临时表名缓冲,fieldName[number][num]是目标存储位置,num每处理完一个字段自增一次。

3.4 类型与长度解析:五种类型白名单加数字校验

字段类型只认char、int、float、double、date五种,用type数组做白名单匹配,匹配成功映射成单字符标记C/I/F/B/D:

char type[5][LEN] = {"char", "int", "float", "double", "date"}; char Type1[5] = {'C', 'I', 'F', 'B', 'D'}; // ... 读取类型 token 到 temp ... for (j = 0; j < 5; j++) { if (strcmp(temp, type[j]) == 0) { f_Type = Type1[j]; break; } } if (j == 5) { printf("字段类型有误(务必是 char,int,float,double,date)!\n"); return 0; }

长度解析只对char、float、double生效,逐字符判断是不是数字,是数字就累加f_length = f_length * 10 + (SQL[i] - 48),最后拼成(长度)存进f_t_len。这里有个硬伤:长度只支持一位数拼接,如果写char(100),f_length会算成 100,但存进f_t_len[1]时只放了一个字符,高位会丢。二次开发时这块必须改成sprintf格式化写入,否则长度超过 9 就会静默出错。

3.5 约束条件识别:primary key、unique、not null 三态

约束解析是整段代码里分支最多的部分。读到primary就继续读下一个词确认是key,读到not就确认是null,读到unique直接落库,其他情况一律当错误处理:

if (strcmp(condition, "primary") == 0) { for (; SQL[i] == ' '; i++); for (j = 0; SQL[i] != ' ' && SQL[i] != ','; i++, j++) temp[j] = tolower(SQL[i]); temp[j] = '\0'; if (strcmp(temp, "key") == 0) fieldCondition[number][num] = 'primary key'; // 原文此处为字符赋值,实际应存字符串 else printf("你输入的'KEY'有误!\n"); } else if (strcmp(condition, "unique") == 0) { fieldCondition[number][num] = 'unique'; } else if (strcmp(condition, "not") == 0) { // 继续读 null 并确认 }

原文这里用单字符赋值给fieldCondition的写法是有问题的,因为fieldCondition是三维字符数组,正确做法是strcpy(fieldCondition[number][num], "primary key")。二次开发时这是第一个要修的点,否则约束信息存进去就是乱码。约束解析的游标推进逻辑和前面一致,靠空格、逗号、分号三个分隔符判断 token 边界。

4. ALTER TABLE 的实现:改表比建表多了一步“找表”

4.1 表存在性检查:线性扫描 tableName 数组

SQL_ALTER()的前半段和建表几乎一样,先校验alter、table两个关键字,再提取表名。区别在于提取完表名后多了一步查找:

for (al_num = 0; al_num < number; al_num++) { if (strcmp(tableName[al_num], alterName) == 0) break; // 找到就停,al_num 即表索引 } if (al_num == number) { printf("你输入的表%s不存在,请重新输入!\n", t_name); return 0; }

al_num是查找游标,number是当前表总数。如果循环走完还没break,说明al_num == number,表不存在。这个线性扫描在表数量少时没问题,但表一多就是 O(n),二次开发可以换成哈希或排序二分。找到表之后,后续所有修改都作用在al_num这个索引上,而不是新表索引,这是 ALTER 和 CREATE 最本质的区别。

4.2 ADD 分支:追加字段到 Pro_Num 末尾

ADD 分支的处理是“在现有字段末尾追加一个”。先提取新字段名、类型、长度、约束,然后写入fieldName[al_num][Pro_Num[al_num]]这一组位置,最后Pro_Num[al_num]++:

// ADD 分支核心逻辑(依据原文流程还原) int idx = Pro_Num[al_num]; // 新字段插入位置 strcpy(fieldName[al_num][idx], field_name); strcpy(fieldType[al_num][idx], f_type); strcpy(f_t_len[al_num][idx], len_mark); strcpy(fieldCondition[al_num][idx], condition); Pro_Num[al_num]++; // 字段计数加一

这里idx就是当前表的字段数,追加后计数自增。参数上要注意field_name、f_type这些临时缓冲必须在写入前完成解析和校验,否则会把半成品写进数组。ADD 分支不需要移动已有字段,所以实现最简单,也是二次开发时最安全的改造入口。

4.3 DROP 与 MODIFY:删除靠移位,修改靠覆盖

原文对 DROP 和 MODIFY 的描述比较简略,但从设计思想可以还原出常见做法。DROP 是找到目标字段索引后,把后面的字段整体前移一位,再Pro_Num--:

// DROP 分支:删除第 del_idx 个字段 for (int k = del_idx; k < Pro_Num[al_num] - 1; k++) { strcpy(fieldName[al_num][k], fieldName[al_num][k + 1]); strcpy(fieldType[al_num][k], fieldType[al_num][k + 1]); strcpy(f_t_len[al_num][k], f_t_len[al_num][k + 1]); strcpy(fieldCondition[al_num][k], fieldCondition[al_num][k + 1]); } Pro_Num[al_num]--;

MODIFY 则是找到字段后直接用新类型覆盖旧类型,长度标记同步更新。这两个分支的坑在于:DROP 移位时如果漏拷某一列(比如忘了f_t_len),就会出现类型和长度错位;MODIFY 如果不校验新类型是否在白名单内,就会把非法类型写进元数据。我一般会在覆盖前先跑一遍和 CREATE 相同的类型校验函数,复用逻辑比复制粘贴靠谱。

4.4 命令行与图形化双入口:菜单驱动加 SQL 输入

设计要求里明确提到“命令行和图形化界面两种形式”。原文的实现是用控制台菜单做图形化的简化替代:一级菜单选功能,二级界面输入 SQL 语句。这种设计的好处是交互路径清晰,坏处是图形化名不副实。二次开发时如果真要上图形界面,可以把SQL_CREATE()和SQL_ALTER()的解析逻辑抽成独立函数,界面层只负责收集字符串和展示结果,解析层完全不感知界面,这样命令行和 GUI 就能共用同一套核心代码。

5. 避坑与排查:这份代码跑起来最容易翻车的五个地方

5.1 现象:输入正确 SQL 却提示 create 有误

原因通常是 SQL 语句开头有不可见字符,或者create和table之间用了多个空格加制表符。原文的切分逻辑只认空格和分号,遇到\t会把它当成 token 的一部分,导致strcmp失败。解决办法是在扫描前先做一次字符清洗,把\t、\r、\n统一替换成空格,或者在比较前用isspace()判断分隔符。我一般会在输入函数里加一行for (i = 0; SQL[i]; i++) if (isspace(SQL[i])) SQL[i] = ' ';,把脏字符挡在解析层外面。

5.2 现象:保存后重新打开,约束条件变成乱码

这是原文代码里fieldCondition[number][num] = 'primary key'这类单字符赋值的直接后果。fieldCondition是char[LEN][LEN][MAX],用单引号赋值等于把多字符常量的低字节塞进一个char,其余内容全丢。解决方法是全部改成strcpy(fieldCondition[number][num], "primary key"),并且检查所有约束分支是否都走了字符串拷贝。排查时可以打开table.txt看约束列是不是只剩一个字母,如果是,基本就是这个问题。

5.3 现象:字段长度超过 9 就存错

原文长度解析用f_t_len[number][num][1] = f_length + 48,只往数组第二个位置放一个字符。char(20)会存成(2)加一个被截断的0,char(100)直接丢高位。原因是长度拼接没有走字符串格式化。解决办法是把f_t_len的写入改成sprintf(f_t_len[number][num], "(%d)", f_length),同时把f_t_len的第三维调大到至少 8 字节。这个坑很隐蔽,因为小长度测试全过,一上真实字段就翻车。

5.4 现象:表建多了程序崩溃或数据串表

LEN和MAX是写死的宏,表数量超过LEN或单表字段超过LEN时,数组越界会覆盖相邻内存,表现为数据串表或直接崩溃。排查方法是打印number和Pro_Num的实时值,看是否逼近上限。解决分两步:短期把宏调大并加边界检查if (number >= LEN) { printf("表数量已达上限"); return; },长期把定长数组换成动态分配或链表。课程设计阶段加检查就够,二次开发建议直接上动态结构。

5.5 现象:ALTER 改完表,显示的还是旧结构

常见原因是修改操作作用在了临时变量上,没有写回fieldName[al_num][...]这组全局数组,或者al_num查找失败后仍然继续执行了修改分支。排查时在 ALTER 每个分支末尾打印tableName[al_num]和Pro_Num[al_num],确认索引正确、计数已更新。另一个可能是保存时Save()遍历的是number而不是修改后的表,导致落盘内容没变。解决方法是确保所有修改都直接操作全局数组,并且Save()在退出前只调用一次。

6. 二次开发怎么改:从定长数组到可扩展元数据管理

如果要把这份课程设计往“能真正用”的方向推一步,我建议按三个层次改。第一层是修正确性:把 5.2 的约束字符串赋值、5.3 的长度拼接全部修掉,给所有数组写入加边界检查,这是保底。第二层是换存储:把四组三维数组换成一个struct Table { char name[MAX]; int field_count; struct Field fields[LEN]; }的数组,字段结构体里放名称、类型、长度、约束四个字段,这样增删改字段时只需要操作结构体,不用同步维护四个平行数组,出错概率直接降一个量级。

typedef struct { char name[MAX]; char type[LEN]; int length; // 0 表示无长度 char constraint[LEN]; } Field; typedef struct { char name[MAX]; int field_count; Field fields[LEN]; } Table; Table tables[LEN]; int table_count = 0;

结构体化之后,DROP 字段就是memmove一段连续内存,ADD 就是写fields[field_count++],MODIFY 就是改fields[idx].type,代码量能砍掉一半。第三层是解析器解耦:把SQL_CREATE()和SQL_ALTER()里的扫描逻辑抽成next_token()函数,返回 token 类型和内容,主解析函数只做状态机判断。这样加新语法(比如DROP TABLE)时不用再复制一遍游标推进代码。

验证方法很直接:准备一组覆盖正常和异常的 SQL 用例,包括大小写混写、多空格、长度两位数、约束组合、表不存在、字段不存在,每改一版就跑一遍,对比table.txt的落盘内容和内存打印是否一致。我习惯在Save()之后立刻调一次Read()再打印,模拟“重启后能否还原”,这一步能抓出大部分写读不对称的问题。

从那以后我每次改这种元数据管理代码,都强制先写一组“建表—改表—保存—重载—比对”的回归脚本再动手,因为这类 bug 往往不在解析逻辑本身,而在写和读的格式约定悄悄错位。希望这份拆解能帮你少走几个弯路,把课程设计跑通之后,再往结构体和解析器解耦的方向推一步,这个迷你 DBMS 就能变成你理解数据库内核的长期脚手架。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 15:06:10

电子签名攻击:SVG恶意代码植入与防御实战

1. 电子签名攻击概述&#xff1a;当信任成为攻击载体在2023年Q3的全球企业安全事件统计中&#xff0c;基于电子邮件的攻击占比首次突破67%&#xff0c;其中利用签名图片嵌入恶意代码的新型攻击手法同比增长240%。这种攻击之所以危险&#xff0c;在于它完美利用了人类认知的两个…

作者头像 李华
网站建设 2026/10/31 15:06:02

STEP-GUI开源GUI智能体:跨设备自动化操作实践

1. STEP-GUI技术解析&#xff1a;开源GUI智能体的工程实践在移动互联网生态高度碎片化的今天&#xff0c;如何让AI智能体真正理解并操作不同品牌、不同型号设备上的图形界面&#xff0c;成为人机交互领域的关键挑战。STEP-GUI作为首个完整开源且包含模型与基础设施的GUI智能体解…

作者头像 李华
网站建设 2026/10/31 15:05:52

AI艺术策展系统:多模态感知与个性化推荐实践

1. 项目概述&#xff1a;当智能体遇上闲暇艺术去年在东京森美术馆看到的那场AI生成艺术展&#xff0c;彻底改变了我对技术介入创意领域的认知。那些由算法实时生成的互动装置&#xff0c;会根据观众心率变化调整光影节奏&#xff0c;这种"数字呼吸感"让我意识到&…

作者头像 李华
网站建设 2026/10/31 15:04:11

SpringBoot+MyBatis社区图书管理系统开发实战

1. 项目背景与核心价值 社区图书管理系统作为典型的Java Web应用&#xff0c;完美契合计算机专业毕业设计的选题要求。这个选题的价值在于&#xff1a; 技术栈全面 &#xff1a;覆盖SpringBootMyBatisThymeleaf主流技术组合 业务场景完整 &#xff1a;包含用户管理、图书管…

作者头像 李华
网站建设 2026/10/31 15:03:16

大模型训练框架与数据处理关键技术解析

1. 大模型训练的核心框架解析大模型训练本质上是一个分阶段的知识获取与能力塑造过程&#xff0c;其核心框架包含三个关键阶段&#xff1a;数据预处理、监督学习微调&#xff08;Supervised Fine-Tuning, SFT&#xff09;和强化学习对齐&#xff08;RLHF&#xff09;。每个阶段…

作者头像 李华
网站建设 2026/10/31 15:02:38

2026届学术写作必备AI工具全景指南

1. 学术写作的AI革命&#xff1a;2026届研究者必备工具全景 作为一名经历过论文煎熬的博士生&#xff0c;我深刻理解学术写作中的痛点&#xff1a;文献管理混乱、数据可视化耗时、语法错误频出、格式调整抓狂。2023年Nature调查显示&#xff0c;研究者平均花费37%的工作时间在写…

作者头像 李华