C语言手搓迷你DBMS:建表改表与SQL解析实战
简介这份资源是山东科技大学信息科学与工程学院《数据库系统概论》课程设计的实验报告面向计算机科学与技术专业学生及需要完成类似数据库课设的学习者。报告围绕实现一个简单DBMS展开核心功能包括用CREATE TABLE语句定义表结构、列名、数据类型与完整性约束以及用ALTER TABLE语句完成添加列、删除列、修改列类型等操作并涉及表信息的持久化存储与读取。压缩包内为1个doc文档约408KB完整记录了设计要求、需求分析、设计思想、程序流程图与模块划分涵盖新建表、修改表、显示已建表信息、保存并退出四个功能模块还列出了参考书目与任务书内容。目前已有933人学习下载适合需要参考课设报告结构、理解表存储与命令行解析思路的读者借鉴。1. 从一份课程设计说起用 C 语言手搓一个能建表改表的迷你 DBMS很多人第一次接触数据库都是从写 SQL 开始的但真让你自己实现一个能解析CREATE TABLE、ALTER TABLE的迷你 DBMS多数人还是会卡在“怎么把表结构存下来”这一步。这份课程设计做的就是这件事用 C 语言实现一个简单的 DBMS支持创建表、修改表、读取已建表、保存表结构交互上同时提供命令行和图形化界面两种形式。它适合正在学《数据库系统概论》的学生也适合想通过一个完整小项目理解 DBMS 内部表结构管理逻辑的开发者。整个项目不依赖任何数据库引擎表结构信息全部由程序自己解析、存储和加载麻雀虽小但把“SQL 解析 → 内存结构 → 文件持久化”这条链路完整跑通了。2. 架构与存储设计表结构怎么在内存和文件之间流转2.1 四个核心模块与主流程程序围绕四个功能展开新建表、修改表、显示已建表信息、保存并退出。主流程是一个循环菜单用户选择操作后进入对应的 SQL 解析函数解析成功后更新内存中的表结构数组退出时统一写回文件。整体数据流可以这样理解程序启动时先调用Read()从table.txt中加载已有表结构到全局数组。用户输入 SQL 语句SQL_CREATE()或SQL_ALTER()负责解析。解析结果写入tableName、fieldName、fieldType、fieldCondition等数组。用户选择退出时程序清空文件并重新写入全部表结构。这种“全量重写”的策略虽然简单但避免了在文件中定位修改位置的麻烦对于课程设计规模的项目来说是完全够用的。2.2 内存中的表结构表示项目用多维字符数组来存储表信息这是最直观的做法#define LEN 50 #define MAX 100 char tableName[LEN][MAX]; // 表名 char fieldName[LEN][LEN][MAX]; // 属性名 char fieldType[LEN][LEN][MAX]; // 属性类型 char fieldCondition[LEN][LEN][MAX]; // 列级约束条件 int Pro_Num[LEN]; // 每张表的属性个数 int number 0; // 当前已创建的表数量tableName[i]存第 i 张表的表名。fieldName[i][j]存第 i 张表第 j 个字段的名字。fieldType[i][j]存对应字段的类型如int、char、float、double、date。fieldCondition[i][j]存约束条件如primary key、unique、not null、NULL。Pro_Num[i]记录第 i 张表有多少个字段读取时靠它控制循环次数。这种设计的好处是索引直接、访问快缺点是字段长度和表数量都有上限。如果要做二次开发可以把这些数组换成动态链表或结构体数组后面会细说。2.3 文件持久化格式表结构保存在table.txt中格式是纯文本按固定顺序排列表名 字段个数 字段名 字段类型 字段长度 约束条件 字段名 字段类型 字段长度 约束条件 ...保存时先写表名再写字段数然后逐个字段写入。读取时按同样顺序反向解析。这种格式的好处是可读性强调试时直接打开文件就能看懂缺点是字段名和类型之间用空格分隔如果字段名本身含空格就会解析错位。课程设计场景下字段名一般不会含空格所以问题不大。2.4 保存策略为什么选择全量重写修改表结构后如果直接在原文件里定位并替换某一行需要处理变长记录、偏移量计算等问题很容易出错。项目采用的做法是退出时先清空table.txt再把内存中所有表结构重新写入。void Save() { ofstream outFile(table.txt, ios::trunc); // 清空文件 for (int i 0; i number; i) { outFile tableName[i] endl; outFile Pro_Num[i] endl; for (int j 0; j Pro_Num[i]; j) { outFile fieldName[i][j] fieldType[i][j] fieldCondition[i][j] endl; } } outFile.close(); }ios::trunc表示打开时清空原有内容。外层循环遍历所有表内层循环遍历每张表的所有字段。写入顺序与Read()的读取顺序严格对应保证下次启动能正确加载。注意如果程序异常退出而没有走到保存逻辑本次修改就会丢失。常见做法是在每次修改操作后立即调用一次保存而不是等到退出时统一写。3. SQL 解析实战CREATE TABLE 和 ALTER TABLE 怎么拆3.1 CREATE TABLE 的解析链路SQL_CREATE()是整个项目里最核心也最长的函数。它的任务是把用户输入的一整条 SQL 语句拆成表名、字段名、字段类型、字段长度、约束条件五类信息。解析按顺序进行跳过开头空格读取第一个单词判断是否为create。继续读取下一个单词判断是否为table。提取表名存入tableName[number]。进入循环逐个提取字段定义直到遇到分号。判断关键字时统一转小写避免用户输入CREATE或Create时解析失败for (j 0; SQL[i] ! SQL[i] ! ;; i, j) { temp[j] tolower(SQL[i]); } temp[j] \0; if (strcmp(temp, create) ! 0) { printf(\n你输入的create有误请重新创建!\n); return 0; }tolower()把字符转成小写保证大小写不敏感。temp暂存当前单词用于和关键字比较。如果比较失败直接打印错误并返回不继续解析。字段类型只支持五种char、int、float、double、date。解析时用数组做映射char type[5][LEN] {char, int, float, double, date}; char Type1[5] {C, I, F, B, D};type存类型名称用于和输入比较。Type1存类型代号后续判断是否需要读取长度时用代号比较。char、float、double需要读取字段长度int和date不需要。字段长度解析时有一个细节长度必须是纯数字如果遇到非数字字符就报错。for (; SQL[i] ! ; i) { if (SQL[i] 48 || SQL[i] 58) { // 不是数字 flag 0; break; } else { f_length f_length * 10 (SQL[i] - 48); } }48到58是字符0到9的 ASCII 码范围。f_length按十进制累加支持多位长度如(100)。如果flag被置为 0说明长度输入有误直接报错。约束条件支持primary key、unique、not null三种解析时先读第一个单词再根据单词决定是否需要继续读第二个单词if (strcmp(condition, primary) 0) { // 继续读 key 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) { strcpy(fieldCondition[number][num], primary key); } }primary和key之间可能有多个空格所以先用循环跳过空格。not null同理先读not再读null。如果约束条件不是这三种直接报错并终止解析。3.2 ALTER TABLE 的三种操作SQL_ALTER()支持三种子操作ADD、DROP、MODIFY。解析流程和CREATE类似先判断alter和table关键字再提取表名然后在已有表中查找该表是否存在。for (al_num 0; al_num number; al_num) { if (strcmp(tableName[al_num], alterName) 0) break; } if (al_num number) { printf(\n你输入的表%s不存在请重新输入!\n, t_name); return 0; }al_num是循环变量也用来记录找到的表索引。如果循环走完还没找到说明表不存在直接报错。找到后al_num就是目标表在数组中的下标后续修改都基于这个下标。ADD操作在表末尾追加一个字段需要解析字段名、类型、长度和约束条件逻辑和CREATE中解析单个字段完全一致。DROP操作根据列名或约束名删除字段需要遍历该表的所有字段找到匹配项后把后面的字段整体前移。MODIFY操作修改字段类型直接定位到目标字段后覆盖fieldType和fieldCondition即可。3.3 读取与显示Read() 和 Show()Read()在程序启动时调用从table.txt中恢复上次保存的表结构。读取时先读表名再读字段个数然后按个数循环读取每个字段的信息。ioFile tableName[number]; ioFile Pro_Num[number]; for (i 0; i Pro_Num[number]; i) { ioFile fieldName[number][i]; ioFile fieldType[number][i]; // 根据类型决定是否读取长度 // 读取约束条件 } number;number是全局表计数每读一张表加一。Pro_Num[number]控制内层循环次数。读取类型后需要判断是否为char、float、double如果是则继续读取长度信息。Show()负责把内存中所有表的信息打印到屏幕如果没有表则提示“无表可显示”。显示格式按表逐个输出每张表先打印表名再逐行打印字段名、类型和约束条件。4. 避坑与排查那些年解析 SQL 时踩过的坑4.1 关键字大小写不一致导致解析失败现象用户输入CREATE TABLE或Create Table程序提示create有误。原因解析时用strcmp直接比较原始输入和全小写关键字没有做大小写归一化。解决在比较前统一用tolower()转小写或者用strcasecmp非标准但常见替代strcmp。项目里已经在读取每个单词时做了tolower但如果某个分支漏了就会出现这个问题。4.2 字段长度解析成多位数时出错现象输入char(100)程序只识别到1或直接报错。原因长度累加逻辑写成了f_length f_length * 10 (SQL[i] - 48)但如果循环条件写错比如遇到右括号没停就会把)也当成数字处理。解决在读取长度时明确终止条件遇到非数字字符立即跳出循环并检查是否读到了合法的数字序列。项目里用flag标记是否遇到非法字符遇到就报错。4.3 表名或字段名含空格导致解析错位现象输入CREATE TABLE my table (...)程序把my当表名table当字段名。原因解析器用空格作为分隔符没有处理带空格的标识符。解决标准 SQL 允许用双引号包裹含空格的标识符但课程设计场景下一般不会这么用。如果确实需要支持可以在解析时检测双引号把引号内的内容整体作为一个标识符读取。4.4 修改表后忘记保存导致数据丢失现象修改完表结构程序正常退出但下次启动发现修改没生效。原因保存逻辑只在用户选择“退出并保存”时触发如果用户直接关窗口或程序崩溃内存中的修改就不会写回文件。解决在每次CREATE或ALTER成功后立即调用一次Save()把内存中的全部表结构写回文件。虽然会增加一点 IO 开销但能保证数据不丢。4.5 文件路径硬编码导致换机器后读不到文件现象在开发机上能正常运行换一台机器后程序启动时读不到table.txt。原因Read()中使用了绝对路径C:\\Users\\Administrator\\Desktop\\DBMS.txt换机器后路径不存在。解决改用相对路径table.txt让程序在当前工作目录下读写文件。如果必须用绝对路径至少把路径做成可配置的宏或命令行参数。5. 二次开发指南从课程设计到可扩展的迷你 DBMS这份课程设计的代码结构清晰但受限于数组容量和固定字段类型扩展性有限。如果你打算在此基础上做二次开发可以从以下几个方向入手。5.1 把固定数组换成动态结构体当前用多维字符数组存储表信息表数量和字段数量都有硬上限。更合理的做法是定义结构体用链表或动态数组管理typedef struct Field { char name[MAX]; char type[20]; int length; char condition[MAX]; struct Field *next; } Field; typedef struct Table { char name[MAX]; int fieldCount; Field *fields; struct Table *next; } Table;Field用链表串联同一张表的所有字段插入和删除都方便。Table用链表串联所有表新增表时动态分配节点。字段长度用int存储不再依赖字符数组拼接。改造后CREATE TABLE解析到新字段时直接malloc一个Field节点挂到链表末尾ALTER TABLE ADD同理DROP则从链表中摘除对应节点并free。5.2 支持更多数据类型和约束当前只支持char、int、float、double、date五种类型约束只支持primary key、unique、not null。可以扩展的方向包括扩展项当前支持可扩展为数据类型char, int, float, double, datevarchar, text, decimal, boolean列约束primary key, unique, not nullcheck, default, foreign key表约束无primary key(多列), foreign key(多列)扩展时需要在解析函数中增加对应的关键字判断分支并在存储结构中增加字段来保存新信息。比如default约束需要额外存一个默认值字符串check约束需要存一个表达式。5.3 增加简单的数据插入和查询当前项目只管理表结构不管理表数据。如果想让这个迷你 DBMS 更完整可以增加INSERT INTO和SELECT的基本实现。INSERT需要按表结构解析值列表把数据追加到数据文件中SELECT需要读取数据文件并按条件过滤。一个简单的做法是每张表对应两个文件一个存结构表名.schema一个存数据表名.data。数据文件按行存储每行是一条记录字段之间用固定分隔符隔开。查询时逐行读取并匹配条件。5.4 用单元测试验证解析逻辑SQL 解析是纯逻辑非常适合写单元测试。可以准备一组输入输出用例覆盖正常和异常情况void test_create_table() { char sql[] CREATE TABLE student (id int primary key, name char(20) not null);; int ret SQL_CREATE(sql); assert(ret 1); assert(strcmp(tableName[0], student) 0); assert(Pro_Num[0] 2); assert(strcmp(fieldName[0][0], id) 0); assert(strcmp(fieldType[0][0], int) 0); assert(strcmp(fieldCondition[0][0], primary key) 0); }每个测试用例构造一条 SQL 语句调用解析函数。用assert检查解析结果是否符合预期。异常用例可以检查返回值是否为 0以及错误提示是否打印。我一般会在修改解析逻辑后先把这组测试跑一遍确认没有回归再继续改其他部分。从那以后我每次动 SQL 解析相关的代码都强制走一遍单元测试避免改一个关键字判断把另一个分支搞挂。希望这份拆解能帮到你拿到源码后先跑通建表和改表流程再按自己的需求逐步扩展。本文还有配套的精品资源点击获取