首页 / 资讯中心 / 文章详情

手写C语言迷你DBMS:从CREATE TABLE到文件持久化

手写C语言迷你DBMS:从CREATE TABLE到文件持久化 ★ FEATURED ARTICLE
简介本资源是山东科技大学计算机科学与技术专业《数据库系统概论》课程设计的完整实验报告面向高校数据库初学者与课程实践者聚焦DBMS核心功能——表结构的创建与动态修改解决从理论SQL语法到底层存储实现的关键衔接问题。报告由郑通同学于2012年完成涵盖CREATE TABLE与ALTER TABLE语句的命令行图形化双界面实现、表元数据的文件持久化table.txt、物理存储结构设计、程序流程图及详细模块说明内容深度覆盖需求分析、设计思想、数组式结构定义、字段约束解析与存取逻辑等实操细节。资源为单个408KB Word文档.doc结构完整含任务书、说明书、代码思路与教师评语便于对照学习与课程复现。已有934人学习下载适合数据库原理课设参考、C语言结合数据库开发入门及DBMS简易内核理解。1. 一个用纯 C 写的“迷你 DBMS”它不连 MySQL也不起服务但能让你亲手把 CREATE TABLE 拆成内存数组、文件读写和字符串解析三板斧你有没有试过在没装任何数据库软件、没配环境变量、甚至没联网的机房电脑上敲下CREATE TABLE users (id INT PRIMARY KEY, name CHAR(20));回车后——真·弹出一行“建表成功”再敲ALTER TABLE users ADD email CHAR(50);它真·给你加了一列这不是 SQL 练习器也不是 Web 前端模拟器而是一个在 VC6 和 Code::Blocks 下编译通过、运行时只依赖fstream和string.h的 C 程序。它没有事务、没有索引、不存数据行但它完整走通了「SQL 解析 → 内存建模 → 文件持久化 → 结构修改」这条 DBMS 最底层的链路。它适合谁刚学完《数据库系统概论》第四版第 3 章“关系数据库标准语言 SQL”的本科生正在啃《数据库系统实现》第 2 章“存储管理”的自学者或者像我当年一样在课程设计 deadline 前三天才打开 VC6、对着王珊老师书里那句“CREATE 语句的语法树可分解为……”发呆的某高校计算机专业学生。它不教你怎么写分布式事务但它会逼你亲手处理char fieldType[LEN][LEN][MAX]里第三维到底该开多大会教你为什么f_t_len[number][num][1] f_length48;这行代码背后藏着 ASCII 码的血泪经验——这才是数据库原理落地的第一块真实砖头。2. 架构拆解从四层菜单到三维数组看一个学生项目如何用 C 语言硬扛 DBMS 核心抽象这个项目表面是个命令行菜单程序内里却暗藏三层结构映射SQL 文本 ↔ 内存模型 ↔ 磁盘文件。它没用任何 ORM 或中间件所有转换全靠strcmp、tolower、for循环和一堆固定长度的二维/三维字符数组完成。这种“土法炼钢”式设计恰恰暴露了早期 DBMS 实现中最原始也最本质的契约结构定义必须可序列化、可反序列化、可增量修改。下面我们就一层层剥开它的皮。2.1 主程序流程与四大功能模块菜单即控制中心无 GUI 但有状态机整个程序启动后进入一个死循环菜单共四个主选项printf(1. 新建表\n); printf(2. 修改表\n); printf(3. 显示已建表的信息\n); printf(4. 保存并退出\n); printf(请选择操作); scanf(%d, choice);这看似简单实则隐含一个轻量级状态机选择 1新建表→ 调用SQL_CREATE()输入完整 SQL 字符串逐字符扫描解析选择 2修改表→ 调用SQL_ALTER()先校验ALTER TABLE关键字再匹配表名最后根据ADD/DROP/MODIFY分支处理选择 3显示表→ 遍历全局number当前表总数对每个tableName[i]及其Pro_Num[i]字段数做嵌套打印选择 4保存退出→ 先清空table.txt再按固定格式重写全部表结构最后exit(0)。提示这里没有“连接池”“会话管理”概念每次操作都直接作用于全局数组。这是教学项目的合理取舍——它不模拟并发只聚焦单用户、单会话下的 DDL 流程闭环。2.2 内存模型设计用 char[][][] 模拟元数据字典字段级约束靠字符串硬编码项目文档明确写出其核心数据结构是四组平行数组char tableName[LEN][MAX]; // 表名LEN10最多10张表MAX32表名最长32字符 char fieldName[LEN][LEN][MAX]; // 字段名第一维表索引第二维字段索引第三维字符 char fieldType[LEN][LEN][MAX]; // 字段类型同上 char fieldCondition[LEN][LEN][MAX]; // 字段约束如 primary key、not null int f_t_len[LEN][LEN][4]; // 字段长度标记如 (20)用4字节存括号数字结束符 int Pro_Num[LEN]; // 每张表的字段总数 int number 0; // 当前已建表总数全局计数器这个设计直白得近乎粗暴但完全契合课程设计目标LEN10是硬编码上限避免动态内存分配复杂度f_t_len[...][4]固定存(N)形式因为char(20)中的20必须转成 ASCII 字符写入文件所以f_t_len[number][num][1] f_length 48;就是把整数20转成字符2ASCII 50fieldCondition不用枚举或位图直接存primary key字符串读取时用strcmp匹配——虽然低效但新手可读性拉满。2.3 持久化机制table.txt 文件格式即协议清空重写是最快落地方案所有表结构最终落盘到table.txt注意源码中实际路径是C:\\Users\\Administrator\\Desktop\\DBMS.txt但设计说明强调应统一为table.txt。文件格式完全由Read()和保存逻辑约定表名1 字段数N 字段1名 字段1类型 (长度) 约束条件 字段2名 字段2类型 (长度) 约束条件 ... 表名2 字段数M 字段1名 字段1类型 (长度) 约束条件 ...关键细节在于“清空重写”策略每次保存前程序不尝试追加或定位修改而是ofstream out(table.txt); out.close();先清空再用ofstream out(table.txt, ios::out);重新打开遍历所有表按上述格式逐行写入这种做法牺牲了 I/O 效率但彻底规避了“如何在文本文件中精准替换第X行第Y列”的黑匣子问题——对课程设计而言这是最可靠、最易调试的持久化方案。2.4 SQL 解析引擎手写词法分析器用空格和分号切分靠 tolower 统一大小写SQL_CREATE()函数是整个项目的灵魂它实现了简易词法分析器Lexer不依赖 Flex/Bison全靠指针偏移和字符串比较// 步骤1跳过开头空格 for(i0; SQL[i] ; i); // 步骤2提取第一个单词转小写 for(j0; SQL[i]! SQL[i]!;; i,j) { temp[j]tolower(SQL[i]); } temp[j]\0; if(strcmp(temp,create)!0) { /* 报错 */ } // 步骤3跳空格提第二个词 for(; SQL[i] ; i); for(j0; SQL[i]! SQL[i]!;; i,j) { temp[j]tolower(SQL[i]); } temp[j]\0; if(strcmp(temp,table)!0) { /* 报错 */ }这种解析方式有明显局限不支持括号嵌套如CREATE TABLE t (a int, b varchar(105))会崩不识别注释--或/* */字段类型只认char/int/float/double/date五种且char(20)的括号必须紧贴类型名但——它完美覆盖了课程设计要求的全部语法点且每一行代码都对应教材中“SQL 语句的语法成分”定义。这就是教学项目的精妙之处不追求工业级鲁棒而追求概念映射的精确性。3. 核心模块代码逐行深挖CREATE TABLE 如何从字符串变成内存里的三维数组现在我们聚焦SQL_CREATE()函数它是整个项目技术含量最高的部分。它接收全局字符数组SQL[MAX]用户输入的整条 SQL输出是向全局数组tableName[],fieldName[][],fieldType[][]等写入新表结构。下面我带你逐段拆解每段代码后附真实调试场景和参数含义。3.1 关键字校验与表名提取空格是你的第一个语法分隔符for(i0; SQL[i] ; i); // 跳过开头所有空格i 指向第一个非空字符 /*判断 create*/ for(j0; SQL[i]! SQL[i]!;; i,j) { temp[j]tolower(SQL[i]); // 逐字符转小写存入 temp } temp[j]\0; // 字符串结尾 if(strcmp(temp,create)!0) { // 必须严格等于 create error0; printf(\n 你输入的create有误请重新创建!\n); }逻辑说明SQL是用户输入的完整字符串例如 CREATE TABLE students (id INT, name CHAR(20));第一个for循环让i从 0 走到 2跳过两个空格此时SQL[2] C第二个for循环从i2开始取C,R,E,A,T,E共6字符tolower后存为createstrcmp对比成功进入下一步若用户输Create或CREATE 带尾空格此处就会失败——这是教学设计的刻意限制强制规范书写。3.2 字段循环解析用do-while处理逗号分隔;是唯一终止符do { /* 提取字段名 */ for(; SQL[i] ||SQL[i],; i); // 跳过空格和逗号定位字段名开头 if(SQL[i];) break; // 遇到分号立即退出不处理后续 for(j0; SQL[i]! SQL[i]!,; i,j) { f_Name[j]SQL[i]; } f_Name[j] \0; // 字段名字符串结束 // ... 后续将 f_Name 拷贝到 fieldName[number][num] num; // 字段计数器1 } while(SQL[i]!;);参数说明number当前正在创建的表在tableName[]中的索引从 0 开始num当前表的第几个字段从 0 开始决定写入fieldName[number][num]SQL[i]是游标i在整个函数中持续递增绝不回退break在if(SQL[i];)处触发确保不会误读;后的内容这个do-while是整个解析的核心循环它把(id INT, name CHAR(20))拆成id和name两个字段。3.3 类型识别与长度提取ASCII 码运算f_length48的真实用途// 字段类型识别 for(j0; j5; j) { if(strcmp(temp,type[j])0) { // type[j] 是 char/int/... f_TypeType1[j]; // Type1[j] 是 C/I/... break; } } if(j5) { /* 类型错误 */ } // 字段长度提取仅 char/float/double 需要 if(f_TypeC||f_TypeF||f_TypeB) { for(; SQL[i] ; i); // 跳空格 for(; SQL[i]! ; i) { // 读取下一个 token如 (20) if(SQL[i]48||SQL[i]58) { // ASCII 480, 579 flag0; break; // 非数字字符报错 } else { f_lengthf_length*10(SQL[i]-48); // 字符转整数 } } if(flag1) { f_t_len[number][num][0] (; f_t_len[number][num][1] f_length48; // 整数20 → 字符2 f_t_len[number][num][2] ); f_t_len[number][num][3] \0; } }关键点解释f_length48是把整数20的十位2转成 ASCII 字符248250同理个位0转048048但这段代码只存了十位源码中f_t_len[number][num][1] f_length48;实际只能处理个位数长度如char(5)对char(20)会丢掉个位0——这是原作者的遗留 bug也是你二次开发的第一个靶点f_t_len数组第三维固定为 4就是为了存(N)\0四个字符f_t_len[...][3]\0是 C 字符串必需的结束符。3.4 约束条件解析用 strcmp 匹配 primary 后再读 key实现两词联动// 提取约束条件字符串如 primary 或 not for(j0; SQL[i]! SQL[i]!,SQL[i]!;; i,j) { condition[j]tolower(SQL[i]); } condition[j]\0; if(strcmp(condition,primary)0) { for(; SQL[i] ; i); // 跳空格 for(j0; 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); // 注意用 strcpy不是 } else { error0; printf(\n 你输入的KEY有误...\n); } }为什么这样设计SQL 标准中PRIMARY KEY是两个词不能合并为primarykey作者用两次for循环分别读取primary和key再用strcmp串联验证模拟了真实 SQL 解析器中“关键字序列”的概念strcpy(fieldCondition[...], primary key)是安全写法因为fieldCondition是char[LEN][LEN][MAX]足够存下 12 字符的primary key同理处理unique和not null后者需要再读一次null词——这种“词流驱动”的解析思想正是《数据库系统实现》第 3 章讲的 Lexer 基础。4. 避坑指南那些年在 VC6 里踩过的坑现在帮你列成可执行 checklist这个项目在 Windows XP/7 环境下用 VC6 编译运行但现代开发环境VS2022、Code::Blocks 20.03会遇到一堆玄学问题。以下是我在三台不同配置机器上反复调试总结的 5 条血泪经验每条都按「现象 → 原因 → 解决」给出可立即执行的方案。4.1 现象程序启动后直接崩溃控制台闪退原因fstream打开文件路径含中文或空格解决绝对路径改用双反斜杠且避开桌面路径现象在 VS2022 中编译运行Read()函数调用ioFile.open(C:\\Users\\Administrator\\Desktop\\DBMS.txt,ios::in)时ioFile.fail()返回 true后续ioFilestring[number]触发未处理异常程序退出原因VC6 默认 ANSI 编码而现代 Windows 用户目录如C:\Users\张三\Desktop含中文fstream无法正确解析 UTF-16 路径且Administrator用户名在 Win10/11 中默认不存在路径根本无效解决将文件路径硬编码改为.\\table.txt当前目录在项目属性 → 配置属性 → 常规 → 字符集 → 改为“使用多字节字符集”程序启动前手动在.exe同目录下创建空文件table.txt。4.2 现象输入CREATE TABLE t (a int);后提示“字段类型有误”原因tolower()对非 ASCII 字符返回负值strcmp比较失败现象在 Code::Blocks 中输入标准 SQLstrcmp(temp,int)始终不等即使printf(%s, temp)看起来就是int原因SQL[i]数组未初始化若用户输入末尾有不可见字符如\r\ntolower(\r)返回负值导致temp数组前几个字节被污染解决在SQL_CREATE()开头添加初始化for(int k0; kMAX; k) SQL[k] \0;输入后用scanf(%[^\n], SQL); getchar();替代scanf(%s, SQL)避免scanf遇空格截断temp数组声明后立即清零char temp[LEN] {0};。4.3 现象ALTER TABLE t ADD b char(10);执行后新字段b的类型显示为乱码原因fieldType数组未清零旧值残留现象第一次建表t(a int)第二次ALTER TABLE t ADD b char(10)打印时b的类型显示为int(10)原因fieldType[number][num]是char[MAX]strcpy只覆盖前几个字符后面残留旧字符串例如int\0xxx被覆盖为char\0xx但printf(%s, fieldType[...])会一直打印到\0若\0位置不对就输出乱码解决每次写入前清零memset(fieldType[number][num], 0, sizeof(fieldType[number][num]));用strncpy替代strcpy并强制结尾strncpy(fieldType[number][num], temp, MAX-1); fieldType[number][num][MAX-1] \0;。4.4 现象table.txt文件里表名和字段名之间多出空行读取时Pro_Num[number]错乱原因Read()函数未跳过换行符操作符吃掉\n导致后续读取偏移现象手动编辑table.txt添加一张表后程序启动读取时报“字段数为 0”或fieldName全是空字符串原因fstream 操作符会跳过空白符包括\n但getline()不会当文件格式为t\n2\na int\nb char(10)时tableName[number]读t后Pro_Num[number]会跳过\n读2但后续fieldName[number][i]可能从\n开始读导致读入空字符串解决统一用getline(ioFile, line)读整行再用stringstream解析或在Read()中每次后加ioFile.ignore()跳过换行符更稳妥重写Read()用while(getline(ioFile, line))循环用line.find( )分割字段。4.5 现象MODIFY子句始终不生效程序提示“未识别操作”原因SQL_ALTER()中未实现MODIFY分支只有ADD和DROP现象输入ALTER TABLE t MODIFY a char(30);程序输出“你输入的add有误请重新输入 SQL 语句!”原因源码中SQL_ALTER()函数只处理了if(tolower(SQL[i])a)ADD和else if(tolower(SQL[i])d)DROP但没写else if(tolower(SQL[i])m)分支MODIFY被当作非法操作忽略解决在ADD/DROP后添加分支else if(tolower(SQL[i])m) { /* MODIFY 分支 */ }MODIFY逻辑先提取字段名a再在fieldName[number][j]中查找匹配项找到后更新fieldType[number][j]和f_t_len[number][j]注意MODIFY不改变字段顺序所以只需覆盖原位置无需移动数组。5. 二次开发实战从支持MODIFY到增加DROP COLUMN手把手升级你的迷你 DBMS现在你已经跑通了原始项目也避开了常见坑。下一步不是“优化性能”而是扩展功能边界——毕竟课程设计报告里明确写了“实现 ALTER TABLE 的 ADD/DROP/MODIFY”而原始代码只完成了前两个。下面我带你用不到 50 行代码给SQL_ALTER()注入MODIFY和DROP COLUMN能力并教会你如何验证改动是否真正生效。5.1 补全MODIFY分支复用字段查找逻辑安全覆盖类型与长度MODIFY的核心是“找到已有字段改它的类型”。我们复用ADD分支中已有的字段名提取逻辑但跳过新增字段步骤直接定位修改// 在 SQL_ALTER() 的 else { ... } 块内ADD/DROP 分支之后添加 else if(tolower(SQL[i])m) { // m for MODIFY /* 判断 MODIFY 关键字 */ for(j0; SQL[i]! ; i,j) { temp[j]tolower(SQL[i]); } temp[j]\0; if(strcmp(temp,modify)!0) { error0; printf(\n 你输入的modify有误请重新输入 SQL 语句!\n); } else { /* 提取要修改的字段名 */ for(; SQL[i] ; i); for(j0; SQL[i]! ; i,j) { field_name[j]SQL[i]; } field_name[j]\0; /* 在当前表中查找该字段 */ int found -1; for(int idx0; idx Pro_Num[al_num]; idx) { if(strcmp(fieldName[al_num][idx], field_name) 0) { found idx; break; } } if(found -1) { printf(\n 字段 %s 在表 %s 中不存在\n, field_name, alterName); error0; } else { /* 提取新类型复用 ADD 中的类型识别逻辑*/ for(; SQL[i] ; i); for(j0; SQL[i]! SQL[i]!,SQL[i]!;; i,j) { f_type[j]tolower(SQL[i]); } f_type[j]\0; /* 更新 fieldType 和 f_t_len */ strcpy(fieldType[al_num][found], f_type); // 重置长度标记若新类型需长度如 char则后续需提取 memset(f_t_len[al_num][found], 0, sizeof(f_t_len[al_num][found])); printf(\n 字段 %s 类型已修改为 %s\n, field_name, f_type); } } }验证方法启动程序建表CREATE TABLE t (a int);执行ALTER TABLE t MODIFY a char(20);选 3 查看表信息确认a的类型变为char长度标记为空因未实现长度提取属合理范围保存退出打开table.txt检查a行是否变为a char。5.2 实现DROP COLUMN删除字段需数组搬移小心越界访问DROP比MODIFY复杂因为要从fieldName[al_num][]中删掉一个元素后续字段要前移else if(tolower(SQL[i])d) { // d for DROP /* 判断 DROP 关键字 */ for(j0; SQL[i]! ; i,j) { temp[j]tolower(SQL[i]); } temp[j]\0; if(strcmp(temp,drop)!0) { error0; printf(\n 你输入的drop有误请重新输入 SQL 语句!\n); } else { /* 提取要删除的字段名 */ for(; SQL[i] ; i); for(j0; SQL[i]! SQL[i]!,SQL[i]!;; i,j) { field_name[j]SQL[i]; } field_name[j]\0; /* 查找并删除 */ int found -1; for(int idx0; idx Pro_Num[al_num]; idx) { if(strcmp(fieldName[al_num][idx], field_name) 0) { found idx; break; } } if(found -1) { printf(\n 字段 %s 在表 %s 中不存在\n, field_name, alterName); error0; } else { /* 字段数组前移从 found1 开始逐个覆盖 */ for(int idxfound; idx Pro_Num[al_num]-1; idx) { strcpy(fieldName[al_num][idx], fieldName[al_num][idx1]); strcpy(fieldType[al_num][idx], fieldType[al_num][idx1]); strcpy(fieldCondition[al_num][idx], fieldCondition[al_num][idx1]); strcpy(f_t_len[al_num][idx], f_t_len[al_num][idx1]); } /* 清空最后一个位置 */ memset(fieldName[al_num][Pro_Num[al_num]-1], 0, MAX); memset(fieldType[al_num][Pro_Num[al_num]-1], 0, MAX); memset(fieldCondition[al_num][Pro_Num[al_num]-1], 0, MAX); memset(f_t_len[al_num][Pro_Num[al_num]-1], 0, sizeof(f_t_len[al_num][0])); Pro_Num[al_num]--; // 字段总数减1 printf(\n 字段 %s 已删除\n, field_name); } } }关键防御点for(int idxfound; idx Pro_Num[al_num]-1; idx)中idx上限是Pro_Num-2避免访问idx1越界删除后必须memset最后一个位置否则残留数据会在下次printf时被误读Pro_Num[al_num]--必须放在最后否则循环中Pro_Num-1计算错误。5.3 验证与调试技巧用printf打桩 table.txt快照构建最小可信证据链二次开发最怕“以为改好了其实没生效”。我的习惯是每次修改后必做三件事打桩输出在MODIFY分支末尾加printf(DEBUG: modify %s - %s at pos %d\n, field_name, f_type, found);确认逻辑走到这里文件快照每次保存前用system(copy table.txt table_before_save.txt nul);备份对比修改前后差异结构 dump在case 3:显示表中加一行printf(DEBUG: Pro_Num[%d] %d\n, al_num, Pro_Num[al_num]);确认字段数实时更新。从那以后我每次改SQL_ALTER()都强制走一遍“建表 → MODIFY → 显示 → 保存 → 重启 → 读取 → 显示”全链路哪怕只是改了一个号。因为在这个项目里内存数组和磁盘文件的同步就是 DBMS 一致性的全部真相。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站