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

发布时间:2026/10/31 15:07:04
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或breakerror更多是给阅读者看的标记。真正起作用的是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/Dchar 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)加一个被截断的0char(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 就能变成你理解数据库内核的长期脚手架。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询