C语言手写迷你DBMS:建表改表与表结构持久化实战
发布时间:2026/10/13 12:55:06 锦皓数字建站

简介这份资源是山东科技大学信息科学与工程学院《数据库系统概论》课程设计的实验报告面向计算机科学与技术专业学生及需要完成类似DBMS实践任务的学习者。报告围绕“实现一个简单的DBMS——创建表和修改表”展开完整记录了从需求分析、设计思想到程序实现的全过程涵盖CREATE TABLE与ALTER TABLE语句的解析、表物理存储结构设计、命令行与图形化界面两种交互方式以及“新建表”“修改表”“显示已建表信息”“保存并退出”四个核心模块的实现思路。资源包为1个doc文档约408KB内容包含课程设计任务书、设计要求、需求分析、设计思想、程序流程图及关键代码说明并列出王珊《数据库系统概论》、谭浩强《C程序设计》等参考资料。目前已有931人学习下载适合作为数据库课程设计参考、实验报告撰写范例及DBMS入门实践的学习材料。1. 从一份课程设计拆起用 C 语言手搓一个能建表改表的迷你 DBMS很多人第一次接触数据库都是从写CREATE TABLE和ALTER TABLE开始的但真正让你理解“表在磁盘上到底长什么样”的往往不是背语法而是自己动手写一个能解析这两条语句的小系统。这份课程设计资源核心就是用 C 语言实现一个极简 DBMS支持创建表、修改表、读取已建表、保存表结构四个功能交互上要求命令行和图形化界面两种形式。它适合正在做数据库课程设计的学生也适合想搞明白 SQL 解析和表结构持久化到底怎么落地的开发者。我拆完这套源码后最大的感受是麻雀虽小但词法扫描、数组存储、文件读写、约束校验这几个环节一个都没少踩坑点也一个都没少。2. 表结构怎么存数组建模与 table.txt 持久化方案2.1 为什么用四维字符数组而不是结构体这份设计最反直觉的地方是它没有用struct来定义表、字段、约束而是用了一组多维字符数组来承载所有元数据。核心定义大致是这样#define LEN 20 #define MAX 50 char tableName[LEN][MAX]; // 表名第一维是表序号 char fieldName[LEN][LEN][MAX]; // 字段名第二维是字段序号 char fieldType[LEN][LEN][MAX]; // 字段类型存 int char 等 char fieldCondition[LEN][LEN][MAX]; // 列级约束存 primary key 等 char f_t_len[LEN][LEN][MAX]; // 字段长度如 (20) int Pro_Num[LEN]; // 每张表的字段个数 int number 0; // 当前已建表数量逻辑说明tableName[0]是第一张表的名字fieldName[0][2]是第一张表第三个字段的名字fieldType[0][2]是它的类型。这种“平铺”写法在课程设计里很常见好处是不用动态内存分配坏处是维度一多就容易写错下标。参数上LEN控制最多建多少张表、每张表最多多少字段MAX控制单个名字或类型字符串的最大长度。我一般会把LEN设成 20、MAX设成 50太小了演示时容易溢出太大了栈上数组会吃紧。2.2 保存表信息先清空再重写的“后悔药”策略持久化这块设计者用了一个很务实的做法每次保存时先把table.txt整体清空再把内存里所有表的信息按固定顺序重新写一遍。这样就不需要处理“修改某张表后原位置对不上”的问题。void Save() { ofstream outFile(table.txt, ios::trunc); // trunc 表示清空重写 outFile number endl; // 先写表的总数 for (int i 0; i number; i) { outFile tableName[i] Pro_Num[i] endl; for (int j 0; j Pro_Num[i]; j) { outFile fieldName[i][j] fieldType[i][j] f_t_len[i][j] fieldCondition[i][j] endl; } } outFile.close(); }逻辑说明第一行写表数量后续每张表先写表名和字段数再逐字段写“字段名 类型 长度 约束”。参数上ios::trunc是关键少了它就会变成追加读回来时表数量对不上直接翻车。读取时按同样顺序反向解析即可用while(!ioFile.eof())配合number逐表恢复。这里有个血泪经验字段名和约束里不能有空格否则按空格分隔的读取逻辑会直接错位所以输入校验阶段必须把空格挡掉。3. CREATE TABLE 解析从字符串到字段数组的完整链路3.1 词法扫描的三个阶段解析CREATE TABLE语句本质是把一整行字符串切成有意义的 token。这份代码的做法是分三段推进先跳过前导空格读第一个词判断是不是create再跳空格读第二个词判断是不是table然后进入字段循环逐个提取字段名、类型、长度、约束。int SQL_CREATE() { int i 0, j 0, num 0; char temp[MAX]; // 阶段一跳过空格读 create for (; SQL[i] ; i); for (j 0; SQL[i] ! SQL[i] ! ;; i, j) temp[j] tolower(SQL[i]); temp[j] \0; if (strcmp(temp, create) ! 0) { printf(你输入的 create 有误请重新创建!\n); return 0; } // 阶段二跳空格读 table for (; SQL[i] ; i); 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; } // 阶段三提取表名进入字段循环 // ... }逻辑说明tolower保证大小写不敏感SQL[i] ! SQL[i] ! ;是 token 的边界条件。参数上i是全局扫描指针j是当前 token 的写入位置。常见做法是每读完一个 token 就补\0否则strcmp会读到上一次的残留内容这种玄学 bug 在 VC6 里尤其难查。3.2 字段类型与长度的提取规则字段类型只认五种char、int、float、double、date。代码里用两个平行数组做映射一个存类型字符串一个存单字符编码char type[5][LEN] {char, int, float, double, date}; char Type1[5] {C, I, F, B, D};逻辑说明匹配到类型后只有char、float、double需要读长度int和date直接跳过。长度提取时逐字符判断是不是数字遇到非数字就报错if (f_Type C || f_Type F || f_Type B) { for (; SQL[i] ; i); for (; SQL[i] ! ; i) { if (SQL[i] 0 || SQL[i] 9) { flag 0; break; } f_length f_length * 10 (SQL[i] - 0); } if (flag 0) { printf(你输入的字段长度有误请重新创建!\n); return 0; } // 拼成 (20) 形式存入 f_t_len }参数说明f_length是累乘出来的整型长度值最后要转回字符串存进f_t_len。这里有个隐藏坑——原代码用f_length 48把长度转成字符只能处理 0 到 9 的长度一旦写char(20)就会存成乱码。我一般会改成sprintf或itoa来拼字符串这是复现时第一个要修的地方。3.3 约束条件的识别与存储约束支持三种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) strcpy(fieldCondition[number][num], primary key); else { printf(你输入的 KEY 有误请重新创建!\n); return 0; } } else if (strcmp(condition, unique) 0) { strcpy(fieldCondition[number][num], unique); } else if (strcmp(condition, not) 0) { // 继续读 null }逻辑说明约束是可选字段如果当前 token 是逗号或分号说明这个字段没有约束直接存NULL。参数上condition存当前读到的约束词temp存第二个词。注意primary key中间的空格必须处理掉否则存进文件后读取时会多出一个 token导致后续字段全部错位。4. ALTER TABLE 解析ADD、DROP、MODIFY 三条分支怎么走4.1 先查表是否存在再分派子命令ALTER TABLE的第一步不是解析动作而是确认表名存在。代码用一个循环在tableName数组里查找找到就记下下标al_num找不到直接报错返回。int al_num; for (al_num 0; al_num number; al_num) { if (strcmp(tableName[al_num], alterName) 0) break; } if (al_num number) { printf(你输入的表 %s 不存在请重新输入 SQL 语句!\n, alterName); return 0; }逻辑说明number是当前表总数循环走完还没break就说明没找到。参数上alterName是从语句里提取的表名al_num是后续修改的目标表下标。这一步是很多同学容易漏的——不查存在性就直接改数组越界后程序行为完全不可预测。4.2 ADD 分支追加字段并更新字段计数ADD 分支的逻辑和 CREATE 里的字段提取几乎一样区别是它把新字段追加到Pro_Num[al_num]的位置然后把字段计数加一。if (tolower(SQL[i]) a) { // 确认是 add // 提取字段名 for (; SQL[i] ; i); for (j 0; SQL[i] ! ; i, j) field_name[j] SQL[i]; field_name[j] \0; // 提取类型、长度、约束存入 fieldName[al_num][Pro_Num[al_num]] 等 // ... Pro_Num[al_num]; // 字段数加一 }逻辑说明新字段的下标就是当前Pro_Num[al_num]存完再自增。参数上field_name是临时缓冲最终要拷进三维数组的对应位置。常见做法是复用 CREATE 里的类型解析函数避免两处逻辑不一致。4.3 DROP 与 MODIFY 分支删除和改类型的边界DROP 分支按约束名删除列级约束MODIFY 分支修改字段类型。这两个分支的代码在原设计里相对简略复现时需要补全。DROP 的核心是找到约束名对应的字段把fieldCondition置空MODIFY 的核心是找到字段名覆盖fieldType和f_t_len。// DROP 分支示意 for (int k 0; k Pro_Num[al_num]; k) { if (strcmp(fieldCondition[al_num][k], dropName) 0) { strcpy(fieldCondition[al_num][k], NULL); break; } } // MODIFY 分支示意 for (int k 0; k Pro_Num[al_num]; k) { if (strcmp(fieldName[al_num][k], modifyName) 0) { strcpy(fieldType[al_num][k], newType); // 同步更新 f_t_len break; } }逻辑说明DROP 删的是约束不是字段这点和标准 SQL 有出入复现时要看清题目要求。MODIFY 改类型后必须同步改长度否则保存到文件再读回来时类型和长度对不上。参数上dropName和modifyName都是从语句里提取的目标名。5. 避坑与排查这套课程设计里最容易翻车的五个点5.1 现象保存后重启程序表数量变成 0原因Save()里用了ios::trunc清空文件但Read()里第一行读表数量的逻辑和写入顺序不一致或者文件路径写死成了某个绝对路径换机器后读不到。解决统一用相对路径table.txt读写顺序严格对齐——先读表数量再循环读每张表的表名、字段数、字段详情。每次改完保存逻辑手动打开table.txt核对一遍格式。5.2 现象输入char(20)后长度显示成乱码原因原代码用f_length 48把整型转字符只能处理 0 到 9。长度超过 9 时高位丢失。解决改用sprintf(f_t_len[number][num], (%d), f_length)或者手写一个整数转字符串的小函数。这是复现时必改的一处。5.3 现象primary key存进去后读回来变成两个字段原因保存时把primary key当成一个字符串写进文件中间有空格读取时按空格分隔primary和key被拆成两个 token后续字段全部错位。解决要么保存时把空格换成下划线如primary_key读取时再换回来要么读取时用getline按行读自己解析。我一般选前者改动最小。5.4 现象ALTER 修改后显示的还是旧结构原因修改的是内存数组但没有触发保存或者保存时number没更新导致新字段没写进文件。解决每次 ALTER 成功后立即调用一次Save()并在Save()开头重新计算number。调试时可以在保存后立刻调用Read()回读一遍验证一致性。5.5 现象VC6 编译通过CodeBlocks 里报错原因VC6 对 C 标准宽松fstream的open模式、strcpy的安全性检查在不同编译器下行为不同。原代码里ioFile.open(C:\\Users\\Administrator\\Desktop\\DBMS.txt, ios::in)这种绝对路径换机器直接失效。解决统一改成ioFile.open(table.txt, ios::in)头文件补上fstream和cstring避免用gets等已废弃函数。如果必须在两个 IDE 间切换把文件路径做成宏或命令行参数。6. 进阶技巧把解析器改成表驱动再加一个回读校验这套课程设计跑通之后最值得做的一件事是把散落在SQL_CREATE和SQL_ALTER里的解析逻辑抽成表驱动。具体做法是定义一个关键字表把create、table、add、drop、modify、primary、unique、not、null这些词和对应的处理函数指针放进去主循环只负责读 token、查表、调用。typedef struct { const char *keyword; int (*handler)(int *pos); } KeywordEntry; KeywordEntry keywords[] { {create, handle_create}, {table, handle_table}, {add, handle_add}, {drop, handle_drop}, {modify, handle_modify}, {NULL, NULL} };逻辑说明每个handler接收当前扫描位置返回下一个位置或错误码。这样加新关键字只需要加一行表项不用改主流程。参数上pos是指向SQL下标的指针方便 handler 内部推进。另一个实用技巧是加一个回读校验函数每次Save()之后立刻Read()到另一组临时数组逐字段比对内存和文件是否一致。不一致就打印差异位置直接定位是哪个字段写错了。int VerifyAfterSave() { // 备份当前内存状态 // 清空内存调用 Read() // 逐表逐字段比对 tableName、fieldName、fieldType、fieldCondition // 返回不一致的字段数量 }参数说明这个函数只在调试阶段开启正式演示时关掉避免每次保存都多一次磁盘 IO。我一般会在main里加一个#ifdef DEBUG开关来控制。从那以后我每次做完这种带持久化的课程设计都强制走一遍“保存—回读—比对”的流程宁可多花十分钟也不想到答辩时被问“你改完的表存哪了”答不上来。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。