简介这份资源是山东科技大学信息科学与工程学院《数据库系统概论》课程设计的实验报告面向计算机科学与技术专业学生及需要完成类似数据库课程设计的学习者。报告围绕实现一个简易DBMS展开核心功能包括CREATE TABLE建表与ALTER TABLE修改表涉及表结构定义、列完整性约束、物理存储结构设计、命令行与图形化界面交互以及程序流程图绘制并参考了王珊、萨师煊《数据库系统概论》等教材。压缩包内共1个doc文档约408KB完整记录了需求分析、设计思想、模块划分与实现过程包含新建表、修改表、显示已建表信息、保存并退出四个基本模块的说明。目前已有933人学习下载适合作为数据库课程设计参考模板帮助读者理解表结构持久化存储、SQL语句解析与动态修改表结构等关键实现思路。1. 从一份课程设计说起用 C 语言手搓一个能建表改表的迷你 DBMS很多人第一次接触数据库都是从写 SQL 开始的但真让你自己动手实现一个能解析CREATE TABLE、ALTER TABLE的小型数据库管理系统多数人还是会卡在“怎么把表结构存下来、怎么再读回来”这一步。这份课程设计做的就是这件事用 C 语言实现一个简单的 DBMS支持创建表、修改表、读取已建表、保存并退出四个核心功能命令行和图形界面两种交互方式都要有。它适合正在学数据库系统概论、需要交课程设计的学生也适合想搞明白“SQL 语句到底是怎么被程序吃进去、拆开、再存成文件”的开发者。整个项目不依赖任何数据库引擎纯靠数组和文件流把表结构持久化到本地文本文件里麻雀虽小但建表、改表、存表、读表这条链路是完整的。2. 整体架构与存储设计四个模块怎么串起来2.1 功能模块划分与主流程程序围绕四个功能展开新建表、修改表、显示已建表信息、保存并退出。主流程是一个循环菜单用户选择操作后进入对应的 SQL 解析函数解析成功后更新内存中的表结构数组退出时统一写回文件。// 主菜单循环伪代码结构 int main() { int choice; while (1) { printf(1.新建表 2.修改表 3.显示表信息 4.保存并退出\n); scanf(%d, choice); switch (choice) { case 1: printf(请输入 CREATE TABLE 语句\n); gets(SQL); // 读取整条 SQL SQL_CREATE(); // 解析并存入数组 break; case 2: printf(请输入 ALTER TABLE 语句\n); gets(SQL); SQL_ALTER(); // 解析并修改数组 break; case 3: ShowTables(); // 遍历数组打印 break; case 4: Save(); // 写回 table.txt return 0; } } }这里的关键点是所有表结构先存在内存数组里只有退出时才落盘。这样做的好处是修改表结构时不用反复读写文件坏处是程序崩溃会丢数据。课程设计场景下够用但如果你要改成实用工具建议每次操作后都同步写文件。2.2 表结构的物理存储数组怎么定义表名、字段名、字段类型、约束条件全部用字符数组存储维度设计直接决定了能建多少表、每张表能有多少字段。#define LEN 20 #define MAX 50 char tableName[LEN][MAX]; // 表名最多 LEN 张表 char fieldName[LEN][LEN][MAX]; // 字段名每表最多 LEN 个字段 char fieldType[LEN][LEN][MAX]; // 字段类型 char fieldCondition[LEN][LEN][MAX]; // 列级约束条件 char f_t_len[LEN][LEN][MAX]; // 字段长度如 (20) int Pro_Num[LEN]; // 每张表的字段个数 int number 0; // 当前已建表数量tableName[LEN][MAX]表示最多 LEN 张表每张表名最长 MAX 个字符。fieldName[LEN][LEN][MAX]是三维数组第一维是表索引第二维是字段索引第三维是字段名本身。这种设计在 C 语言课程设计里很常见优点是直观、不用动态内存缺点是容量固定、浪费空间。如果你要扩展可以把LEN和MAX调大或者改成链表加动态分配。2.3 持久化策略为什么每次保存都全量重写保存表信息时程序先把table.txt清空再把内存中所有表结构重新写进去。这个策略看起来笨但避免了“在文件中间修改某张表”的复杂定位逻辑。void Save() { ofstream outFile(table.txt, ios::trunc); // 清空重写 outFile number endl; // 先写表数量 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] f_t_len[i][j] fieldCondition[i][j] endl; } } outFile.close(); }写入顺序是表数量 → 每张表的表名 → 字段个数 → 每个字段的四个属性。读取时按同样顺序反向解析即可。这种“顺序写、顺序读”的格式比在文件里做随机访问简单得多也是课程设计里最稳妥的做法。3. 核心模块拆解CREATE TABLE 的解析链路3.1 词法扫描怎么从字符串里抠出关键字SQL_CREATE()的第一步是跳过前导空格然后逐字符读取用tolower()统一转小写后比对关键字。int SQL_CREATE() { int i 0, j; char temp[LEN]; // 跳过开头空格 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... }这里用tolower()是为了让CREATE、Create、create都能通过。SQL是全局字符数组存的是用户输入的整条语句。i是当前扫描位置j是临时缓冲区索引。判断完create后同样的逻辑再判断table然后开始提取表名。3.2 提取表名与字段定义表名提取完后进入do-while循环逐个解析字段名、类型、长度和约束。// 提取表名 for (; SQL[i] ; i); for (j 0; SQL[i] ! SQL[i] ! ;; i, j) { t_Name[j] SQL[i]; } t_Name[j] \0; strcpy(tableName[number], t_Name); // 循环提取字段 do { // 跳过空格和逗号 for (; SQL[i] || SQL[i] ,; i); if (SQL[i] ;) break; // 字段名 for (j 0; SQL[i] ! ; i, j) { f_Name[j] SQL[i]; } f_Name[j] \0; strcpy(fieldName[number][num], f_Name); // 字段类型 for (; SQL[i] ; i); for (j 0; SQL[i] ! SQL[i] ! ,; i, j) { temp[j] tolower(SQL[i]); } temp[j] \0; // 匹配 char/int/float/double/date // ... num; } while (SQL[i] ! ;);字段类型只支持五种char、int、float、double、date。匹配时用strcmp逐个比对匹配不上就报错。字段长度只对char、float、double提取且必须是纯数字否则报错。约束条件支持primary key、unique、not null其他情况存为NULL。3.3 约束条件的识别与存储约束条件的解析是嵌套在字段循环里的先读一个单词再判断是不是primary、unique、not。// 读取约束条件第一个词 for (; SQL[i] ; i); for (j 0; SQL[i] ! SQL[i] ! , SQL[i] ! ;; i, j) { condition[j] tolower(SQL[i]); } condition[j] \0; 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); } } else if (strcmp(condition, unique) 0) { strcpy(fieldCondition[number][num], unique); } else if (strcmp(condition, not) 0) { // 继续读 null // ... strcpy(fieldCondition[number][num], not null); } else { strcpy(fieldCondition[number][num], NULL); }primary key和not null都是两个单词所以需要二次读取。unique是单个单词直接存。如果用户没写约束就存NULL。这里有个细节fieldCondition是二维数组[LEN][MAX]但代码里有时写成fieldCondition[number][num] primary key这是原代码的笔误正确写法应该是strcpy。4. ALTER TABLE 的实现改表比建表更容易翻车4.1 表存在性检查与分支判断ALTER TABLE的第一步是提取表名然后遍历tableName数组确认表是否存在。int SQL_ALTER() { char alterName[MAX]; int al_num; // ... 解析 alter 和 table 关键字 ... // 提取表名 for (; SQL[i] ; i); for (j 0; SQL[i] ! ; i, j) { t_name[j] SQL[i]; } t_name[j] \0; strcpy(alterName, t_name); // 查找表 for (al_num 0; al_num number; al_num) { if (strcmp(tableName[al_num], alterName) 0) break; } if (al_num number) { printf(你输入的表 %s 不存在\n, t_name); return 0; } // 继续判断 ADD / DROP / MODIFY }al_num记录找到的表索引后续所有修改都作用在这个索引上。如果遍历完都没找到说明表不存在直接报错返回。4.2 ADD 子句追加新列ADD 分支负责在现有表末尾追加一个字段需要解析字段名、类型、长度和约束然后写入数组的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; int pos Pro_Num[al_num]; // 新字段插入位置 strcpy(fieldName[al_num][pos], field_name); // 提取类型、长度、约束与 CREATE 类似 // ... Pro_Num[al_num]; // 字段数加一 }关键点是pos Pro_Num[al_num]新字段直接放在已有字段后面不需要移动其他数据。这也是用数组存储的好处追加操作是 O(1)。4.3 DROP 与 MODIFY 子句DROP 按约束名删除MODIFY 修改字段类型。这两个分支的解析逻辑与 ADD 类似但操作的是已有字段。// DROP 分支删除指定约束 if (tolower(SQL[i]) d) { // 解析 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); } } } // MODIFY 分支修改字段类型 if (tolower(SQL[i]) m) { // 解析 modify、字段名、新类型 // 找到字段后更新 fieldType 和 f_t_len for (int k 0; k Pro_Num[al_num]; k) { if (strcmp(fieldName[al_num][k], modifyName) 0) { strcpy(fieldType[al_num][k], newType); // 更新长度... } } }DROP 在课程设计里实现的是“删除约束”而不是“删除列”这和标准 SQL 的DROP COLUMN有区别。如果你要改成删列需要把后续字段整体前移并减少Pro_Num。5. 避坑与排查课程设计里最容易翻车的几个点5.1 文件路径写死导致换机就崩现象在自己电脑上跑得好好的换一台机器或者换个目录程序一启动就报错或者读不到表。原因原代码里文件路径写成了C:\\Users\\Administrator\\Desktop\\DBMS.txt这是绝对路径换环境必然失效。解决改成相对路径table.txt或者用argv[0]动态获取程序所在目录。最稳妥的做法是把文件放在程序同级目录用fopen(table.txt, r)直接访问。5.2 字段长度只存了一位数字现象定义char(20)的字段保存后再读取长度变成了(2)或者乱码。原因原代码里f_t_len[number][num][1] f_length 48;只把长度转成一个字符如果长度是 20204868对应字符D完全错了。解决长度应该用sprintf或itoa转成字符串再存。比如sprintf(f_t_len[number][num], (%d), f_length);。5.3 约束条件用字符赋值而不是字符串拷贝现象设置了primary key保存后读出来变成乱码或者只剩一个字符。原因代码里写的是fieldCondition[number][num] primary key;这是把字符串赋给单个字符变量编译能过但运行结果完全错误。解决统一用strcpy(fieldCondition[number][num], primary key);。所有字符串赋值都要用strcpy或strncpy不能用。5.4 读取时没有处理文件结束标志现象程序启动后显示表信息最后多出一张空表或者乱码表。原因Read()函数里用while(!ioFile.eof())判断文件结束但eof()只有在读操作失败后才返回 true导致最后一次循环多读一次。解决改成while(ioFile tableName[number])用流读取的返回值判断是否成功。或者读完一条记录后立即检查ioFile.eof()并break。5.5 数组越界没有检查现象建了太多表或者字段太多程序崩溃或者数据覆盖。原因tableName[LEN][MAX]的LEN是 20建第 21 张表时越界写入覆盖了相邻内存。解决每次number之前检查if (number LEN) { printf(表数量已达上限\n); return 0; }。字段数同理在num前检查num LEN。6. 二次开发与扩展从课程设计到能用的工具如果你想让这个项目从“交作业”变成“真能用”有几个方向可以改。第一把固定数组换成动态链表或std::vector这样表数量和字段数量不再受限。第二把文件存储从纯文本改成二进制或 JSON 格式读取时不用再逐字符解析直接反序列化就行。第三加一个简单的 SQL 解析器支持INSERT、SELECT、DELETE让它真正能存数据而不只是存表结构。// 用链表替代固定数组的示例结构 typedef struct Field { char name[MAX]; char type[MAX]; char length[MAX]; char condition[MAX]; struct Field *next; } Field; typedef struct Table { char name[MAX]; int fieldCount; Field *fields; struct Table *next; } Table; Table *tableList NULL; // 表链表头指针改成链表后建表就是往链表尾部追加节点改表就是遍历链表找到目标节点再修改。删除表就是链表节点摘除。这样容量问题彻底解决代价是代码复杂度上升需要自己管理内存释放。另一个实用的改造是加一个简单的图形界面。课程设计要求里提到了“命令行和图形化界面两种形式”但原代码只实现了命令行。你可以用 Windows API 或者 GTK 做一个窗口把输入框、按钮、列表控件绑到现有的解析函数上。解析逻辑不用动只是把scanf换成从输入框读字符串把printf换成往列表控件写数据。验证改造是否成功我一般会走一遍完整流程建三张表每张表带不同约束改其中一张表的字段类型删一个约束保存退出再重新打开看数据是否完整。这个流程能覆盖 90% 的 bug。从那以后我每次改完存储相关的代码都强制走一遍“建表→改表→保存→重启→读表”的闭环不然很容易出现“写进去读不出来”的玄学问题。希望帮到你。本文还有配套的精品资源点击获取