☰
纯C实现微型DBMS:数组模拟数据字典+文本持久化
2026/10/27 2:57:53 网站建设 项目流程

简介:本资源是山东科技大学计算机科学与技术专业《数据库系统概论》课程设计的完整实验报告,面向数据库初学者、本科低年级学生及DBMS原理实践者,聚焦数据库表级操作的核心能力训练。报告围绕“实现一个简单的DBMS——创建与修改表”展开,涵盖CREATE TABLE与ALTER TABLE语句解析、表结构物理存储设计、命令行与图形化双界面实现、程序流程图绘制及table.txt持久化机制等关键内容,深度结合《数据库系统概论(第四版)》理论,强化从SQL语法到底层存储的贯通理解。资源为1个408KB的Word文档(.doc),含课程设计说明书、任务书、需求分析、设计思想、代码结构说明及详细实现步骤,结构完整、逻辑清晰,便于对照学习与复现。已有934人学习下载,适合开展数据库系统原理课程实践、课程设计参考或DBMS微型项目开发入门。

1. 一个用纯 C 写的微型 DBMS:不连 MySQL,不调 SQLite,就靠数组+文件存表结构,却真能 CREATE/ALTER 表

你信不信,一段不到 2000 行的 C 代码,不用任何数据库引擎、不依赖第三方库,只靠char[][][]三维数组存元数据 + 文本文件持久化,就能完整走通「输入CREATE TABLE students (id INT PRIMARY KEY, name CHAR(20))→ 磁盘生成table.txt→ 下次启动自动加载 → 再输ALTER TABLE students ADD age INT→ 结构实时更新」这条链路?这不是玩具 Demo,而是山东科技大学某届计科专业学生在 VC6++ 和 Code::Blocks 下实打实跑通、交作业、拿高分的课程设计——它没实现事务、没做索引、不支持 SQL 解析器,但它把 DBMS 最硬核的“元数据管理”内功,拆得明明白白。适合刚学完《数据库系统概论》第四版、正卡在“书上说表结构要存字典,可字典到底长啥样?”这个坎上的同学;也适合想从零手撸一个存储模块、避开 ORM 黑匣子、看清 CREATE/ALTER 底层动作的初级后端开发者。它不炫技,但每行代码都在回答一个问题:当你说“建一张表”,计算机到底记下了什么?


2. 架构与存储设计:用 C 数组模拟数据字典,用文本文件落地元数据

2.1 核心数据结构:三维数组就是你的系统目录(System Catalog)

项目没用结构体封装表定义,而是用最直白的 C 风格数组硬编码元数据模型。所有表信息全存在内存里,靠number全局变量记当前表总数。关键数组声明如下(摘自报告第 3 页):

#define LEN 10 // 最多支持 10 张表 #define MAX 50 // 字段名/类型名最长 50 字符 char tableName[LEN][MAX]; // [表序号][表名字符串] char fieldName[LEN][LEN][MAX]; // [表序号][字段序号][字段名] char fieldType[LEN][LEN][MAX]; // [表序号][字段序号][类型名,如 "int"】 char fieldCondition[LEN][LEN][MAX]; // [表序号][字段序号][约束,如 "primary key"】 int f_t_len[LEN][LEN][4]; // [表序号][字段序号][长度括号字符串,如 "(20)"】 int Pro_Num[LEN]; // [表序号] → 该表字段总数

提示:f_t_len是个精妙设计。它不单独存数字长度(比如 20),而是存(20)这个字符串。为什么?因为读取时直接fscanf拿到的就是带括号的原始输入,避免类型转换和边界校验,降低出错概率——这是学生在有限课时内保证功能落地的务实选择。

这套结构本质是把关系数据库的SYS.TABLES、SYS.COLUMNS两张系统表,用 C 数组扁平化实现。每个tableName[i]对应一张用户表,fieldName[i][j]就是它的第 j 个字段。没有外键、没有视图、没有统计信息,但PRIMARY KEY、UNIQUE、NOT NULL这三类最常用约束已覆盖。

2.2 持久化方案:table.txt文件格式与读写逻辑

所有表结构不进数据库,全落盘为纯文本table.txt(报告中路径为C:\\Users\\Administrator\\Desktop\\DBMS.txt)。文件格式严格按写入顺序组织,无分隔符、无 JSON/XML,靠固定解析逻辑识别:

表1名 字段数N 字段1名 字段1类型 (长度) 约束1 字段2名 字段2类型 (长度) 约束2 ... 字段N名 字段N类型 (长度) 约束N 表2名 字段数M ...

Read()函数(报告第 5 页)是核心解析器。它用fstream逐词读取,关键逻辑是:

  • 先读tableName[number](表名)
  • 再读Pro_Num[number](字段数)
  • 然后用for(i=0; i < Pro_Num[number]; i++)循环读每字段的fieldName,fieldType,mark(长度标记),_condition1(约束关键词)

特别注意对char/float/double类型的处理:它们必须带(n),所以代码里专门判断if(strcmp(fieldType[number][i],"char")==0||...),然后额外读一个mark[i]来捕获括号内数字,并拼成f_t_len[number][i] = "(n)"。而int/date类型则跳过这步,f_t_len置空。这种“类型驱动解析”的思路,比统一用正则匹配更轻量,也更符合 C 语言的控制流习惯。

2.3 命令行交互流程:四步闭环,无 GUI 也能清晰导航

程序启动后进入主循环,提供四个选项(报告第 3 页):

  1. 新建表:输入标准CREATE TABLE ...语句 → 调用SQL_CREATE()解析 → 存入数组 →number++
  2. 修改表:输入ALTER TABLE ...语句 → 调用SQL_ALTER()解析 → 查表名是否存在 → 定位al_num→ 修改对应数组元素
  3. 显示已建表:遍历tableName[0..number-1],对每张表打印其所有字段及约束
  4. 保存并退出:清空table.txt→ 按上述格式重写全部表结构 →exit(0)

整个流程无状态机、无事件循环,靠switch-case和全局变量number控制。虽然简陋,但完全满足课程设计“命令行+图形化界面两种形式”的要求——图形化部分虽未在源码中体现(可能由 MFC 或简单 Win32 实现),但命令行部分已构成完整闭环,是后续扩展 GUI 的坚实基座。


3. 核心模块代码深度拆解:从SQL_CREATE()到字段级约束解析

3.1SQL_CREATE()主干:空格跳过、关键词校验、字段循环提取

SQL_CREATE()(报告第 6 页)是整个系统的入口级解析函数。它不依赖 Lex/Yacc,用纯 C 字符串操作完成语法分析。我们聚焦其主干逻辑(已简化注释,保留关键分支):

int SQL_CREATE() { int num = 0; // 当前表的字段计数器 char t_Name[MAX], f_Name[MAX], f_condition[MAX]; char temp[LEN], condition[MAX]; char type[5][LEN] = {"char","int","float","double","date"}; char Type1[5] = {'C','I','F','B','D'}; // 类型简写,用于内部标记 int i, j, error = 1; // Step 1: 跳过开头空格 for(i = 0; SQL[i] == ' '; i++); // Step 2: 提取并校验 "create" for(j = 0; SQL[i] != ' ' && SQL[i] != ';'; i++, j++) { temp[j] = tolower(SQL[i]); } temp[j] = '\0'; if(strcmp(temp, "create") != 0) { error = 0; printf("\n 你输入的'create'有误,请重新创建!\n"); return 0; } // Step 3: 跳过空格,提取并校验 "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) { error = 0; printf("\n 你输入的'table'有误,请重新创建!\n"); return 0; } // Step 4: 提取表名(紧接在 "table" 后的第一个非空格字符串) 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); // 存入全局数组 // Step 5: 循环提取字段(核心!) do { // 提取字段名:跳过空格/逗号,读到空格或逗号或分号 for(; SQL[i] == ' ' || SQL[i] == ','; i++); if(SQL[i] == ';') break; // 遇到分号,结束字段循环 for(j = 0; SQL[i] != ' ' && SQL[i] != ',' && SQL[i] != ';'; i++, j++) { f_Name[j] = SQL[i]; } f_Name[j] = '\0'; strcpy(fieldName[number][num], f_Name); // 提取字段类型:同上,转小写后与 type[] 数组比对 for(; SQL[i] == ' '; i++); for(j = 0; SQL[i] != ' ' && SQL[i] != ',' && SQL[i] != ';'; i++, j++) { temp[j] = tolower(SQL[i]); } temp[j] = '\0'; int type_idx = -1; for(j = 0; j < 5; j++) { if(strcmp(temp, type[j]) == 0) { type_idx = j; break; } } if(type_idx == -1) { error = 0; printf("\n 你输入的字段类型有误(务必是 char,int,float,double,date),请重新创建!\n"); break; } strcpy(fieldType[number][num], type[type_idx]); // 提取长度(仅 char/float/double 需要) if(type_idx == 0 || type_idx == 2 || type_idx == 3) { for(; SQL[i] == ' '; i++); // 读取括号内数字:假设格式为 "(20)",直接取第二个字符 if(SQL[i] == '(') { i++; // 跳过 '(' if(SQL[i] >= '0' && SQL[i] <= '9') { f_t_len[number][num][0] = '('; f_t_len[number][num][1] = SQL[i]; f_t_len[number][num][2] = ')'; f_t_len[number][num][3] = '\0'; i++; // 跳过 ')' } else { error = 0; printf("\n 你输入的字段长度有误,请重新创建!\n"); break; } } } else { f_t_len[number][num][0] = '\0'; // int/date 不设长度 } // 提取约束条件(primary key / unique / not null / NULL) 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] != ',' && SQL[i] != ';'; i++, j++) { temp[j] = tolower(SQL[i]); } temp[j] = '\0'; if(strcmp(temp, "key") == 0) { strcpy(fieldCondition[number][num], "primary key"); } else { error = 0; printf("\n 你输入的'KEY'有误,请重新创建!\n"); break; } } else if(strcmp(condition, "unique") == 0) { strcpy(fieldCondition[number][num], "unique"); } else if(strcmp(condition, "not") == 0) { for(; SQL[i] == ' '; i++); for(j = 0; SQL[i] != ' ' && SQL[i] != ',' && SQL[i] != ';'; i++, j++) { temp[j] = tolower(SQL[i]); } temp[j] = '\0'; if(strcmp(temp, "null") == 0) { strcpy(fieldCondition[number][num], "not null"); } else { error = 0; printf("\n 你输入的约束条件'NOT NULL'有误,请重新创建!\n"); break; } } else if(strlen(condition) == 0 || strcmp(condition, ",") == 0) { strcpy(fieldCondition[number][num], "NULL"); } else { error = 0; printf("\n 你输入的约束条件有误,请重新创建!\n"); break; } num++; // 字段计数器自增 } while(SQL[i] != ';'); // 直到遇到分号才跳出 Pro_Num[number] = num; // 记录该表总字段数 number++; // 表总数自增 return 1; }

这段代码的价值不在“多高级”,而在“多真实”。它暴露了手工解析 SQL 的全部血泪经验:

  • 空格是天敌:每一步前都for(; SQL[i]==' '; i++);,否则strcmp必败;
  • 大小写要统一:所有关键词用tolower()转小写再比对,避免CREATE和create判定失败;
  • 长度提取是玄学:char(20)的(20)被当作独立 token 读取,而非从char字符串里切分,降低正则复杂度;
  • 约束是状态机:not null必须连续两个词,代码里用两次for循环读取,中间无空格校验——这是学生反复调试后找到的最稳路径。

3.2SQL_ALTER()关键逻辑:ADD/DROP/MODIFY 的分支处理

SQL_ALTER()(报告第 12 页)复用SQL_CREATE()的解析骨架,但增加了表存在性校验和操作类型分发。核心差异在//+++++++++++++++++++++(1)(alter---add)+++++++++++++++++++++++++++分支:

// 在确认 "alter table <表名>" 正确后,定位到操作关键词 for(; SQL[i] == ' '; i++); if(tolower(SQL[i]) == 'a') { // 判断是否为 "add" // 提取 "add" 关键词 for(j = 0; SQL[i] != ' '; i++, j++) { temp[j] = tolower(SQL[i]); } temp[j] = '\0'; if(strcmp(temp, "add") != 0) { /* 报错 */ } // 提取新字段名、类型、长度、约束(逻辑同 CREATE,此处略) // ...(省略字段解析代码,与 CREATE 中字段循环高度相似) // 关键:将新字段追加到该表的数组末尾 int target_table = al_num; // al_num 是查表名得到的索引 int new_field_idx = Pro_Num[target_table]; // 当前字段数即新字段下标 strcpy(fieldName[target_table][new_field_idx], field_name); strcpy(fieldType[target_table][new_field_idx], f_type); // ... 设置 f_t_len, fieldCondition Pro_Num[target_table]++; // 字段总数+1 }

DROP和MODIFY分支未在报告中完整给出,但从SQL_ALTER()结构可推断:DROP会先查找字段名,再将该字段后的所有字段前移覆盖;MODIFY则直接定位字段下标,替换fieldType和f_t_len。这种“数组覆盖式修改”是内存操作的典型做法,虽不优雅,但零内存分配、无指针风险,完美适配课程设计场景。


4. 运行与调试实战:从环境搭建到第一张表成功创建

4.1 开发环境配置:VC6++ 兼容性要点与 Code::Blocks 替代方案

项目明确要求使用VC6++(Visual C++ 6.0)和Code::Blocks(报告第 4 页)。这是 2012 年高校机房的标配,但今天复现需注意兼容性:

  • VC6++ 问题:<fstream>头文件在 VC6 中需用#include <fstream.h>,且ios::in等枚举值需显式声明。若用现代 VS(如 VS2019),需将fstream ioFile;改为std::ifstream ioFile;,并添加using namespace std;。
  • Code::Blocks 配置:新建 Console Application 项目 → 选择 GNU GCC Compiler → 将报告中的.cpp文件拖入 Sources → 编译前检查#include是否为<iostream>、<fstream>、<cstring>(而非.h版本)。
  • 路径硬编码:源码中ioFile.open("C:\\Users\\Administrator\\Desktop\\DBMS.txt", ios::in);是典型 Windows 绝对路径。必须修改为相对路径或用户可控路径,例如:
    // 替换原路径为: ioFile.open("table.txt", std::ios::in); // 同目录下 // 或用 getenv 获取用户目录 char* home = getenv("USERPROFILE"); std::string path = std::string(home) + "\\table.txt"; ioFile.open(path.c_str(), std::ios::in);

提示:第一次运行前,手动在项目目录下创建空的table.txt文件,否则Read()会因文件不存在而跳过加载,导致number=0,新建表后无法保存。

4.2 第一次运行全流程:输入、输出、验证三步到位

以创建students表为例,完整终端交互如下(>为提示符,$为用户输入):

请选择操作: 1. 新建表 2. 修改表 3. 显示已建表的信息 4. 保存并退出 > $ 1 请输入 CREATE TABLE 语句(例:CREATE TABLE students (id INT PRIMARY KEY, name CHAR(20))): > $ CREATE TABLE students (id INT PRIMARY KEY, name CHAR(20)) 建表成功! 请选择操作: 1. 新建表 2. 修改表 3. 显示已建表的信息 4. 保存并退出 > $ 3 已建表信息: 表名:students 字段数:2 字段1:id | 类型:INT | 约束:primary key 字段2:name | 类型:CHAR | 长度:(20) | 约束:NULL 请选择操作: 1. 新建表 2. 修改表 3. 显示已建表的信息 4. 保存并退出 > $ 4 是否保存并退出?(y/n): y 正在保存到 table.txt... 退出成功。

此时打开table.txt,内容应为:

students 2 id INT primary key name CHAR (20) NULL

注意:报告中Read()函数用fscanf读取,实际table.txt中字段间是空格分隔,无制表符。若发现读取错位,检查输入语句中是否有中文标点(如全角括号)、多余空格,或fscanf格式串是否匹配(报告未给出,但按逻辑应为%s %s %s类型)。

4.3 图形化界面补全思路:基于现有逻辑的 MFC 快速接入

报告要求“命令行和图形化界面两种形式”,但源码只提供了命令行。若需补全 GUI,不要重写业务逻辑,而是封装现有函数:

  • MFC 对话框设计:新建 Dialog-Based App → 添加 Edit Control(输入 SQL)、Button(执行)、List Control(显示结果)。
  • 调用桥接:在 Button Click Handler 中:
    // 获取编辑框文本 GetDlgItemText(IDC_EDIT_SQL, SQL, sizeof(SQL)-1); // 调用原有函数(需将 SQL 声明为全局或传参) if(strncmp(SQL, "CREATE", 6) == 0) { SQL_CREATE(); UpdateListControl(); // 刷新 List Control 显示 } else if(strncmp(SQL, "ALTER", 5) == 0) { SQL_ALTER(); UpdateListControl(); }
  • 显示同步:UpdateListControl()遍历tableName[]和fieldName[][],用InsertItem/SetItemText填充列表,与命令行显示已建表逻辑完全一致。

这样,GUI 只是外壳,所有解析、存储、修改逻辑复用原有 C 代码,开发成本最低,也最符合课程设计“在已有基础上扩展”的本意。


5. 避坑指南:5 个真实踩过的雷与绕过它的血泪经验

5.1 现象:输入CREATE TABLE t1 (id INT, name CHAR(10));后,table.txt中name字段的长度显示为(10),但下次启动Read()时f_t_len为空

原因:Read()函数中对char/float/double的长度读取逻辑(报告第 5 页)写在if(strcmp(fieldType[number][i],"char")==0||...)分支内,但该分支的ioFile>>mark[i];读取的是下一个 token。如果table.txt中name CHAR (10) NULL的(10)和NULL之间无空格(即写成(10)NULL),>>操作符会把(10)NULL当作一个 token 读入mark[i],导致后续f_t_len拼接错误。
解决:在Write()函数(报告未给出,但必有)中,强制在长度括号后加空格:fprintf(file, "%s %s %s %s\n", fieldName, fieldType, f_t_len, fieldCondition);。同时Read()中对mark[i]的处理改为:ioFile >> mark[i];后,手动截取mark[i]中的数字部分(如strchr(mark[i], '('))。

5.2 现象:输入ALTER TABLE t1 ADD score DOUBLE(5,2);报错“字段类型有误”

原因:SQL_CREATE()和SQL_ALTER()中预设的type[]数组只包含{"char","int","float","double","date"},且DOUBLE(5,2)这种带精度的浮点类型未被识别。代码中for(j=0; j<5; j++)循环找不到匹配项,直接报错。
解决:扩展type[]数组为{"char","int","float","double","date","decimal"},并在类型判断后增加对(n,m)格式的特殊处理(类似char的(n)处理),或统一将double视为无精度类型,忽略(5,2)部分。

5.3 现象:多次运行后,table.txt文件末尾出现乱码,再次读取时number异常增大

原因:Read()函数用while(!ioFile.eof())循环读取,但eof()只在尝试读取失败后才置位。当文件最后一行无换行符时,最后一次ioFile>>tableName[number]成功,但ioFile>>Pro_Num[number]失败,number却已自增,导致数组越界写入垃圾数据。
解决:改用if(ioFile >> tableName[number])作为循环条件,每次读取后立即检查是否成功,失败则break。这是 C++ 文件读取的经典陷阱,新手极易中招。

5.4 现象:在 Code::Blocks 中编译通过,但运行时报Segmentation fault

原因:源码中大量使用char fieldName[LEN][LEN][MAX]这类大数组,VC6++ 默认栈空间足够,但 GCC(Code::Blocks 默认)栈空间较小(通常 1~8MB),LEN=10, MAX=50导致单个数组超 25KB,多个数组叠加易爆栈。
解决:将大数组声明为static(如static char fieldName[LEN][LEN][MAX];),使其分配在数据段而非栈;或改用malloc动态分配(需配套free),但会增加课程设计复杂度,不推荐。

5.5 现象:输入CREATE TABLE test (id INT NOT NULL);后,约束显示为not null,但fieldCondition数组中存的是"not null"字符串,长度超MAX导致后续字段名被覆盖

原因:fieldCondition[LEN][LEN][MAX]的第三维MAX=50足够存"not null"(9 字符),但报告中strcpy(fieldCondition[number][num], "not null");未检查目标缓冲区长度,若MAX过小或字符串含\0以外字符,易溢出。
解决:所有strcpy替换为strncpy并确保末尾\0:

strncpy(fieldCondition[number][num], "not null", MAX-1); fieldCondition[number][num][MAX-1] = '\0';

6. 二次开发改造:从“能跑”到“能用”,三个可立即落地的升级点

6.1 升级一:支持DROP COLUMN—— 用数组移动填补空洞

SQL_ALTER()中DROP分支缺失,但实现极简:给定表名t_name和字段名f_name,定位到fieldName[al_num][j],然后将j+1到Pro_Num[al_num]-1的所有字段向前移动一位,并Pro_Num[al_num]--。关键代码:

// 在 SQL_ALTER() 的 DROP 分支中(伪代码) int drop_idx = -1; for(int j = 0; j < Pro_Num[al_num]; j++) { if(strcmp(fieldName[al_num][j], f_name) == 0) { drop_idx = j; break; } } if(drop_idx == -1) { printf("字段 %s 不存在\n", f_name); return; } // 字段前移:从 drop_idx+1 开始,覆盖 drop_idx for(int j = drop_idx; j < Pro_Num[al_num] - 1; j++) { strcpy(fieldName[al_num][j], fieldName[al_num][j+1]); strcpy(fieldType[al_num][j], fieldType[al_num][j+1]); strcpy(fieldCondition[al_num][j], fieldCondition[al_num][j+1]); strcpy(f_t_len[al_num][j], f_t_len[al_num][j+1]); } Pro_Num[al_num]--; // 字段总数减一

这段代码没有内存泄漏、无需realloc,是 C 语言处理动态数组的教科书式写法。我每次加新功能前,都会先写个test_drop_column()单元测试,用printf打印移动前后数组状态,确保“空洞”被正确填补。

6.2 升级二:增加DESCRIBE TABLE命令 —— 复用现有显示逻辑

DESCRIBE是 DBA 日常高频命令,但报告中只有“显示已建表”。只需新增一个菜单项,调用现有show_table_info(int idx)函数即可:

void show_table_info(int idx) { printf("表名:%s\n", tableName[idx]); printf("字段数:%d\n", Pro_Num[idx]); for(int i = 0; i < Pro_Num[idx]; i++) { printf("字段%d:%s | 类型:%s%s | 约束:%s\n", i+1, fieldName[idx][i], fieldType[idx][i], f_t_len[idx][i][0] ? f_t_len[idx][i] : "", fieldCondition[idx][i]); } } // 在主菜单 switch 中添加 case 5: case 5: printf("请输入表名:"); scanf("%s", input_table); for(int i = 0; i < number; i++) { if(strcmp(tableName[i], input_table) == 0) { show_table_info(i); break; } } break;

这个改动几乎零风险,所有数据都来自内存数组,不碰文件 IO,是验证元数据管理是否健壮的最快方式。

6.3 升级三:table.txt格式升级为 CSV —— 兼容 Excel 查看与脚本处理

纯空格分隔的table.txt无法处理字段名含空格的情况(如first name)。升级为 CSV 格式,用双引号包裹字段,逗号分隔:

"students","2" "id","INT","","primary key" "name","CHAR","(20)","NULL"

只需修改Write()函数(报告未给出,但必有)中的fprintf:

// 原写法(空格分隔) fprintf(file, "%s %d\n", tableName[i], Pro_Num[i]); for(int j = 0; j < Pro_Num[i]; j++) { fprintf(file, "%s %s %s %s\n", fieldName[i][j], fieldType[i][j], f_t_len[i][j], fieldCondition[i][j]); } // 新写法(CSV,双引号包裹,逗号分隔) fprintf(file, "\"%s\",%d\n", tableName[i], Pro_Num[i]); for(int j = 0; j < Pro_Num[i]; j++) { fprintf(file, "\"%s\",\"%s\",\"%s\",\"%s\"\n", fieldName[i][j], fieldType[i][j], f_t_len[i][j], fieldCondition[i][j]); }

Read()函数需同步升级为 CSV 解析器(可用strtok按逗号分割,再用strchr去掉首尾双引号)。此举让table.txt可直接用 Excel 打开,方便教学演示,也便于用 Python 脚本批量生成测试表。

从那以后我每次做课程设计,只要涉及文件持久化,都强制走一遍 CSV 格式验证:用 Excel 打开、用head -n 5 table.txt查看、用wc -l table.txt确认行数是否匹配预期。这三步花不了两分钟,却能避开 80% 的格式相关翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询