简介这份资源是山东科技大学信息科学与工程学院《数据库系统概论》课程设计的实验报告面向计算机科学与技术专业学生及需要完成类似DBMS实践任务的学习者。报告围绕“实现一个简单的DBMS——创建表和修改表”展开完整记录了从需求分析、设计思想到程序实现的全过程涵盖CREATE TABLE与ALTER TABLE语句解析、表结构物理存储设计、命令行与图形化界面交互、程序流程图绘制等核心内容。压缩包内为1个doc文档约408KB结构完整可直接作为课程设计报告的参考模板。报告详细阐述了用数组存储表名、属性名、属性类型及约束条件的方法以及通过table.txt持久化保存表信息、启动时重新加载的设计思路并包含新建表、修改表、显示已建表信息、保存退出四个功能模块的实现说明。目前已有933人学习下载适合数据库初学者理解DBMS底层工作原理也可为课程设计选题、报告撰写与代码实现提供具体参考。1. 从一份课程设计拆起用 C 语言手搓一个能建表改表的迷你 DBMS很多人第一次接触数据库是从CREATE TABLE和ALTER TABLE这两条语句开始的。敲下去表就建好了字段就加上了至于背后发生了什么基本靠脑补。这份课程设计资源干的事情就是把脑补变成代码——用 C 语言实现一个简单的 DBMS核心功能只有两个创建表和修改表。它不依赖 MySQL、SQLite 这类现成引擎而是自己解析 SQL 语句、自己设计表结构的存储格式、自己把表信息持久化到文本文件里。适合正在学《数据库系统概论》的学生、想搞明白 DBMS 底层怎么运转的开发者以及需要一份能跑通的课程设计参考的人。下面我按“它怎么解析语句、怎么存表、怎么改表、哪里容易翻车”的顺序把这份资源拆开讲。2. 语句解析与表结构存储从字符串到内存数组的映射2.1 为什么用数组而不是链表或哈希表这份设计里表信息的存储用的是多维字符数组。表名存在char tableName[LEN][MAX]字段名存在char fieldName[LEN][LEN][MAX]字段类型和约束条件也各有对应的数组。第一维是表的编号第二维是字段的编号第三维是字符串本身。选数组而不是链表原因很直接课程设计的规模有限表的数量、每张表的字段数量都不会太大数组的随机访问比链表遍历方便得多。解析 SQL 语句时需要频繁地按索引回填字段信息数组的fieldName[tableIndex][fieldIndex]这种写法比在链表里找节点要直观。哈希表虽然查找快但实现复杂度上来了对一份教学性质的课程设计来说不划算。常见做法是给每个维度定一个上限比如#define LEN 20、#define MAX 50表示最多 20 张表、每张表最多 20 个字段、每个标识符最长 50 个字符。这个上限在真实 DBMS 里当然不够用但作为课程设计够跑通演示就行。2.2 解析 CREATE TABLE 的完整流程解析CREATE TABLE语句的核心思路是跳过空格、提取关键词、校验关键词、提取表名、循环提取字段定义。下面这段代码是从资源里摘出来的关键逻辑我加了注释说明每一步在干什么。// 判断 create 关键词 for(i 0; 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); // 跳过 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; } // 提取表名 for(; SQL[i] ; i); for(j 0; SQL[i] ! SQL[i] ! ;; i, j) { t_Name[j] SQL[i]; } // 将表名写入全局数组 for(int _i 0; _i j; _i) tableName[number][_i] t_Name[_i]; tableName[number][_i] \0;这段代码的逻辑是逐词推进的先确认第一个词是create再确认第二个词是table然后提取表名。每一步都做了tolower转换这样用户输入CREATE、Create、create都能识别。跳过空格用的是for(; SQL[i] ; i);这种空循环虽然写法不够优雅但在课程设计里很常见。字段提取部分是一个do-while循环每次迭代处理一个字段定义。先提取字段名再提取字段类型然后根据类型判断是否需要读取长度比如char(20)里的20最后提取约束条件primary key、unique、not null。字段类型只支持五种char、int、float、double、date分别映射到内部编码C、I、F、B、D。注意字段长度的提取只处理了个位数的情况。代码里f_t_len[number][num][1] f_length 48;把整数长度转成字符时如果长度是 20f_length 48得到的是d而不是2和0。这是一个隐藏的坑后面避坑章节会展开。2.3 表信息的持久化为什么每次全量重写保存表信息的逻辑在资源里说得很清楚打开table.txt先把文件内容清空再把内存里所有表的信息重新写进去。不是追加不是定位修改是全量覆盖。这个选择乍看很粗暴但仔细想有它的道理。如果用户修改了某张表的某个字段类型在文件里定位到那一行去改需要知道每张表在文件里的偏移量还要处理字段数量变化导致的行数变化。全量重写就简单了内存里的数组就是唯一真相文件只是数组的序列化结果。每次保存时先写表名再写字段数量然后逐个写字段名、字段类型、字段长度、约束条件。// 保存表信息的核心逻辑伪代码示意 void Save() { ofstream outFile(table.txt); for(int t 0; t number; t) { outFile tableName[t] ; // 表名 outFile Pro_Num[t] ; // 字段数量 for(int f 0; f Pro_Num[t]; f) { outFile fieldName[t][f] ; // 字段名 outFile fieldType[t][f] ; // 字段类型 outFile f_t_len[t][f] ; // 字段长度 outFile fieldCondition[t][f] ; // 约束条件 } outFile endl; } outFile.close(); }读取时的逻辑完全对称先读表名再读字段数量然后按数量循环读取每个字段的四个属性。这种“写入时按固定顺序、读取时按同样顺序”的做法是文本文件持久化最朴素也最不容易出错的方式。3. ALTER TABLE 的实现在已有表上做增删改3.1 先查表是否存在再判断操作类型ALTER TABLE的解析比CREATE TABLE多了一步先提取表名然后在已有的表数组里查找这张表。如果找不到直接报错返回。查找用的是strcmp逐个比较表名找到后记录下标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 不存在请重新输入!\n, t_name); return 0; }找到表之后根据ALTER后面的关键词分三种情况处理ADD加新列、DROP删约束、MODIFY改类型。资源里主要实现了ADD的逻辑DROP和MODIFY的框架也在但细节处理不如ADD完整。3.2 ADD 新列的字段解析与数组扩容ADD的逻辑是提取新列名、提取数据类型、判断是否需要长度、提取约束条件然后把这四个信息追加到该表的字段数组末尾同时把字段数量Pro_Num[al_num]加一。// 提取要添加的字段名 for(; SQL[i] ; i); for(j 0; SQL[i] ! ; i, j) { field_name[j] SQL[i]; } field_name[j] \0; // 提取字段类型与 CREATE 中逻辑相同 // ... // 追加到数组末尾 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], length_str); strcpy(fieldCondition[al_num][idx], condition); Pro_Num[al_num]; // 字段数量加一这里的关键是Pro_Num[al_num]既表示当前字段数量又正好是新字段要插入的下标位置。因为数组是从 0 开始编号的如果当前有 3 个字段下标 0、1、2新字段就应该放在下标 3而Pro_Num的值正好是 3。加完之后Pro_Num变成 4下次再加就放在下标 4。提示这种“数量即下标”的写法在数组追加场景里很常见但前提是字段数量不能超过数组第二维的上限。如果Pro_Num[al_num]已经等于LEN再追加就会越界。资源里没有做这个边界检查实际使用时需要自己补上。3.3 MODIFY 和 DROP 的简化处理MODIFY的逻辑是找到指定字段把它的类型和长度替换成新的值。资源里的做法是遍历该表的所有字段用strcmp找到名字匹配的那个然后直接覆盖fieldType和f_t_len数组对应位置的内容。DROP在标准 SQL 里可以删列也可以删约束资源里主要处理的是删约束。实现方式是找到对应字段后把fieldCondition数组里那个位置的内容改成NULL表示没有约束。如果要删的是列本身需要把该列之后的所有列向前移动一位同时Pro_Num减一。资源里对删列的实现不够完整这是课程设计常见的取舍——先把主要功能跑通边缘功能留个框架。4. 避坑与排查这份代码跑起来容易翻车的五个地方4.1 字段长度超过 9 时显示成乱码现象建表时写char(20)保存后重新读取字段长度显示成d或者别的奇怪字符。原因代码里把整数长度转成字符时用了f_length 48这只对 0 到 9 有效。20 48 68对应 ASCII 字符D不是2和0。解决把长度存成字符串而不是单个字符。用sprintf(f_t_len[number][num], (%d), f_length);替代原来的逐字符赋值。读取时也按字符串整体读取不要逐字符解析。4.2 表名或字段名超过数组上限导致越界现象程序运行中突然崩溃或者表信息保存后读取出来是乱码。原因tableName[LEN][MAX]里LEN是表数量上限MAX是名字长度上限。如果输入的表名超过MAXstrcpy会写越界。如果表数量超过LENnumber变量会超出数组第一维的范围。解决在提取表名和字段名时加长度检查超过上限就截断或报错。在number之前判断number LEN满了就提示“表数量已达上限”。4.3 文件路径硬编码导致换台机器就读不到现象在自己的电脑上跑得好好的换一台电脑或者换个目录程序启动后显示“无表可显示”。原因读取文件的路径写死成了C:\\Users\\Administrator\\Desktop\\DBMS.txt这个路径只在特定机器上存在。解决把文件路径改成相对路径比如table.txt让文件生成在程序运行目录下。或者用命令行参数传入文件路径提高可移植性。4.4 解析语句时对空格和逗号的处理不统一现象输入CREATE TABLE student(id int, name char(10));能成功但输入CREATE TABLE student ( id int , name char(10) ) ;就报错。原因解析代码里跳过空格和逗号的逻辑分散在各处有的地方只跳空格不跳逗号有的地方只跳一个空格不跳多个。字段名提取时用SQL[i] ! 作为结束条件如果字段名后面直接跟逗号而没有空格就会把逗号也读进字段名里。解决写一个统一的skipDelimiters()函数把空格、逗号、制表符都当作分隔符跳过。提取标识符时结束条件改成“遇到空格、逗号、分号或左括号就停”。4.5 保存时全量重写但没处理文件打开失败现象程序提示保存成功但重新启动后之前建的表全没了。原因ofstream打开文件时如果路径不存在或没有写权限会静默失败。代码里没有检查outFile.is_open()直接往里写写了个寂寞。解决打开文件后立即判断is_open()失败就打印错误信息并返回。保存完成后可以再读一次文件做校验确认写入的内容和内存里的数组一致。5. 进阶用法把文本存储换成二进制再加一个简单的查询这份课程设计的默认存储格式是纯文本好处是能用记事本直接打开看坏处是读取时要逐字符解析字段多了之后加载速度会明显变慢。一个自然的进阶方向是把表结构存成二进制文件用fwrite把整个数组结构一次性写进去读取时用fread一次性读回来。结构体定义大概是这样typedef struct { char name[MAX]; char type[10]; char length[10]; char condition[20]; } FieldDef; typedef struct { char tableName[MAX]; int fieldCount; FieldDef fields[LEN]; } TableDef; TableDef tables[LEN]; // 全局表数组 int tableCount 0;保存时fwrite(tables, sizeof(TableDef), tableCount, fp);读取时fread(tables, sizeof(TableDef), LEN, fp);然后根据实际读到的数量恢复tableCount。二进制存储不需要解析分隔符字段名里有空格也不怕读写速度比文本快一个数量级。另一个值得加的进阶功能是SELECT查询。不需要实现完整的 SQL 查询引擎只要支持SELECT * FROM 表名;这种最简单的形式就行。解析时提取表名在数组里找到对应的表然后把字段名、类型、约束条件逐行打印出来。这个功能加上之后整个程序就从“只能建表和改表”变成了“能建、能改、能看”演示效果会好很多。我自己的习惯是每次改完存储格式或者加完新功能都会用同一组测试用例跑一遍建三张表每张表三到五个字段字段类型覆盖int、char、float约束条件覆盖primary key、not null、unique。然后保存、退出、重新启动、显示所有表信息逐字段核对。这个流程走下来大部分数组越界和解析遗漏的问题都能提前暴露。从那以后我每次动存储相关的代码都强制走一遍这个回归测试省了不少事后排查的时间。希望帮到你。本文还有配套的精品资源点击获取