资讯详情

C语言文件操作与数据处理实战:从结构体落盘到排序回写

📅 2026/10/21 17:49:05 | 华诺云谱 👁 阅读
C语言文件操作与数据处理实战:从结构体落盘到排序回写
上周末整理旧硬盘翻出一本2015年培训班发的C语言补充讲义扉页上写着“第十一章 文件操作与数据处理”。那会儿课上用圆珠笔抄代码画流程图页边角还留着一个当时的疑问“fopen和fclose到底谁在前谁在后”。如今重看这个问题当然是幼稚了但这一章从结构体到文本加密、再从排序到文件回写的内容依然值得拿出来重新走一遍。它不是讲什么高深技巧而是把一个数据处理程序最基本的闭环讲透了内存里的结构体数据怎么落到磁盘怎么加密怎么读回来排序再写回去。这篇文章我就顺着当年的笔迹把这章实战内容重新读一遍也把这几年踩过的坑一并补上。1. 翻出2015年的第十一章笔记我先看到了什么1.1 那个年代学文件操作硬件和课堂的氛围都不一样2015年培训班还是典型的“上午讲课、下午上机”节奏。前面十章的课程安排大致是变量、分支、循环、数组、指针、字符串到第十一章突然上文件操作大家其实都是懵的。当时上机用的机器性能很一般老师反复强调文件不要乱存磁盘空间紧张项目路径写错可能导致整个练习区变慢。就是在这种环境下第十一章被安排成课程尾声的重点实战章节。这一章的内容放到现在来看其实不多打开文件、读写文件、关闭文件利用结构体数组处理一批学生成绩数据加密后写入文本再读回来排序。全部代码加起来大概一百来行可那是我第一次意识到程序不只是在屏幕上打几个数字而是可以把运行结果永久留下来。培训班里有很多同学卡在这一章因为前面学数组、学指针还能靠逻辑推文件操作涉及外部环境、文件状态、字节流这些概念出错时连调试都不知道从哪下手。所以这次重读笔记时我没有把旧代码当一个“古董”看而是把它当成一次现场复习。当年的示例代码简朴很多边界条件没写清楚正好借这个机会把文件操作模块化、补全错误处理变成一个能直接拿到真实数据上跑的版本。1.2 笔记首页那句“文件是程序的记忆”我隔了九年才看懂笔记本第一页上有句被红笔框起来的话应该是当时老师写在黑板上的“文件是程序的记忆”。那时我把它当考试金句背下来但并没有真正理解。九年后回头看这句话其实点中了文件操作的本质。一个程序在内存里运行变量、结构体、数组随时存在可一旦退出内存被释放一切归零。要让数据跨时间、跨运行存在就必须把数据写到文件里。小到一个学生成绩单大到业务系统的用户档案底层都是同一个逻辑把内存中的结构化数据序列化成文件字节下次运行再反序列化回来。数据库之所以复杂是因为加了索引、事务、并发控制而它最底层的存储能力和C语言文件的落盘思路同源。想通了这一点再看第十一章的标题“文件操作与数据处理”就会明白它为什么把结构体、加密、排序三者放在一起。结构体定义了数据在内存中的排列方式文件决定了数据如何持久化加密是数据在磁盘上的一种变换排序则是对读回数据后的处理。整个链路缺一不可。2. 结构体落盘把一个学生信息库完整写进文件2.1 为什么非要用结构体来组织数据第十一章的实例是经典的“学生成绩管理”每个学生有姓名、学号、三门课成绩。如果用三个数组分别存姓名、学号、成绩代码也能写但会出现一个问题数组下标把三个维度的数据强行关联一旦要删除某个学生、按成绩调整顺序所有数组都要同步移动非常容易出bug。结构体的意义在于把一组相关数据打包成一个逻辑单位。一个struct Student就是一个学生的完整档案studentArray[i]里的姓名、学号、成绩天然属于同一个人。不仅如此结构体在内存中是连续分布的一条记录对应一段连续内存块这给文件写入提供了极大便利。当年笔记里定义的结构体大概长这样#define NAME_LEN 20 struct Student { char name[NAME_LEN]; char id[10]; int score1; int score2; int score3; };注意这里用的是定长字符数组而不是char *指针。原因很现实文件写入需要知道每条记录的固定大小如果用指针写进文件的是地址值而不是字符串内容下次程序运行读回来那个地址早已失效。只要你想把结构体直接写进文件字段就必须是定长数组或固定大小的类型这是当年笔记里用红笔标注过的一条教训。2.2 fwrite一次写入与逐字段写入的区别笔记里给出的核心写入代码是fwrite版本FILE *fp fopen(students.dat, wb); if (fp NULL) { perror(无法打开文件); return 1; } fwrite(students, sizeof(struct Student), n, fp); fclose(fp);fwrite四个参数分别是内存缓冲区地址、单个数据块大小、数据块数量、文件指针。整句话的意思是把内存中从students开始的n个sizeof(struct Student)大小的连续数据原样复制到文件里。这种写法的优点是高效、简单、类型信息天然保留读取时也用同样的结构体定义就是一次内存拷贝。另一种做法是逐字段用fprintf写入文本fprintf(fp, %s %s %d %d %d\n, students[i].name, students[i].id, students[i].score1, students[i].score2, students[i].score3);两者差别很大。fwrite写的是二进制原样字节文件打开后是一堆不可读字符但读写速度快、代码简洁。fprintf写的是文本人眼可读、可以用文本编辑器查看、方便导入导出但每次读写都要做一次格式化解析字段之间要有分隔符读取时还要按相同格式逆向解析。第十一章笔记里两种方法都讲了当时不太理解为什么同一个问题要写两遍后来才意识到这就是“二进制文件”和“文本文件”两条路线的分叉口。2.3 结构体对齐当年没写进笔记本的隐藏知识点这次重读我特地问了自己一个问题sizeof(struct Student)真的是字段字节数之和吗答案是未必。编译器为了CPU访问效率会在结构体成员之间插入填充字节这叫结构体对齐。比如上面的结构体name占20字节id占10字节三个int各占4字节看起来总字节数应该是201044442。但按常见32位编译器id后面可能需要补充2个填充字节使下一个int按4字节边界对齐于是sizeof可能变成44甚至在64位平台上都不一样。当年培训班笔记没有提对齐这个概念因为它不教底层编译原理。但这是文件操作的真坑用fwrite写入时所有填充字节也会被写进文件。如果你在同一台机器、同一个编译器下读回问题不大可如果你把文件发给别人用另一个编译器处理或者程序升级后改变结构体定义读回来的数据就会错位。稳妥做法是数据落地时采用显式固定格式比如逐字段存取或者在结构体上使用打包属性但这会牺牲一些性能。说到底fwrite的便捷是建立在“结构体布局一致”这个前提上的做文件数据交换时必须时刻记得这一点。3. 文本加密实战异或加密和它的两种改进方向3.1 一个字节密钥的异或加密为什么能写进教程第十一章的后半部分安排了文本加密当时的示例是异或加密。原理简单到一句话就能讲完一个字节与密钥做异或得到密文密文再与同一个密钥异或即可还原原文因为对任意字节a和密钥k恒有(a ^ k) ^ k a。为什么培训班要选它当教材因为异或加密能同时复习三样东西位运算、文件逐字节读写、对称加密的基本思想。代码量小逻辑一目了然跑起来还能看到文件内容变成乱码数学上又自洽很适合作为教学案例。当年抄下来的加密函数核心循环大致是void encryptFile(FILE *fin, FILE *fout, unsigned char key) { int ch; while ((ch fgetc(fin)) ! EOF) { fputc((unsigned char)ch ^ key, fout); } }解密时调用同一个函数即可因为加密再解密等于异或两次。这个函数定义很干净但站在今天的角度看有两个问题需要补写。3.2 重读代码后发现的两个隐患第一个隐患是把fgetc返回值和EOF混用。fgetc返回的是int类型正常情况下返回读到的字节值范围是0到255读到文件末尾或出错时返回EOFEOF的值通常是-1。因为返回值是int所以能区分255和EOF用char变量接收就会出问题。上面代码里用int接收ch再通过判断ch EOF退出循环逻辑没毛病。但很多人会因为图省事写成while((ch fgetc(fin)) ! EOF)这里ch必须声明成int而不是char否则对于包含0xFF字节的二进制文件会出现“还没到结尾就被误判成EOF”的问题。笔记里没写这个细节我当年第一次做加密练习就栽在这。第二个隐患是单字节密钥的强度几乎为零。密钥空间只有256种可能把所有密钥值穷举一遍哪怕全手工测试也能轻易找到有意义的明文。更关键的问题是单字节密钥对所有字符做同样的变换文件的统计特征几乎原样保留频率分析一下就能猜出密钥的大致范围。所以这种加密只适合教学、适合当玩具绝不能用来保护真实数据。如果谁在公司项目里用这个思路做加密等于把数据脱光了放硬盘上。3.3 把单密钥升级成多密钥和位置混淆重读时我给这个练手项目做了两个简单升级。第一是多字节密钥循环参与异或例如const unsigned char key[] {0x1F, 0x2A, 0x3C, 0x4D}; size_t klen sizeof(key) / sizeof(key[0]); size_t idx 0; int ch; while ((ch fgetc(fin)) ! EOF) { fputc((unsigned char)ch ^ key[idx % klen], fout); idx; }这样相同明文字节在不同位置会得到不同密文统计特征被弱化。第二是加入位置混淆让第i个字节额外异或一个与i相关的常数比如i 0x0F。这样密钥不再是单一重复模式破解难度上升一个数量级。当然这类代码写起来很有意思但我在笔记的空白处补了一句新感想正统加密需要成熟的密码库自己写的流密码仅供学习和理解。想在这种主题下走得更深可以去研究密钥流生成算法但从工程角度讲专业的事交给专业库。4. 从文件读回来再排序数据处理最完整的闭环4.1 读文件之前必须先确认文件是啥格式排序的前提是把数据从文件读回内存。读文件最忌上来就写fscanf或fread你得先确认文件当初是怎么写出来的。如果之前用的是fwrite二进制写入那么读回时要用fread并且使用同一个struct Student类型FILE *fp fopen(students.dat, rb); if (fp NULL) { perror(无法打开文件); return 1; } struct Student readBack[100]; int n 0; size_t count fread(readBack, sizeof(struct Student), 100, fp); n (int)count; fclose(fp);注意fread的返回值是成功读取的元素个数不是字节数所以要直接拿它作为元素数量。如果文件里记录数少于100fread会在文件结尾处停下count就是实际数量这个用法比feof判断更可靠。如果之前用的是fprintf文本写入那么读回时应该一段一段解析比如while (fscanf(fp, %s %s %d %d %d, readBack[n].name, readBack[n].id, readBack[n].score1, readBack[n].score2, readBack[n].score3) 5) { n; }两种方式按文件格式匹配即可。千万不要用fread去读fprintf生成的文本文件也不要指望fscanf能解析fwrite写出的二进制数据格式错位会让数据面目全非。4.2 用qsort排序把冒泡留给作业本2015年培训班讲排序时正处在“数组排序”章节那会儿大家还在手写冒泡。所以笔记里第十一章的排序部分保留了冒泡排序的流程图。但这次重读我决定换成qsort。原因是C标准库已经提供了稳定、通用的排序函数工程实践中没有理由自己造一个O(n²)的轮子。qsort的用法固定难点只在比较函数int cmpByScore(const void *a, const void *b) { const struct Student *pa (const struct Student *)a; const struct Student *pb (const struct Student *)b; return (pa-score1 pb-score1) - (pa-score1 pb-score1); }比较函数返回负值、零、正值分别表示a小于、等于、大于b。写成(pa-score1 pb-score1) - (pa-score1 pb-score1)是为了避免直接用差值当score是浮点数时差值会有精度问题用大小比较更安全。调用只需要一行qsort(readBack, n, sizeof(struct Student), cmpByScore);这次重读我特意比较了冒泡和qsort的差距。数据量只有几十条时冒泡的劣势看不出来一旦升到几万条冒泡要跑几秒qsort几乎是瞬间完成。C语言的确是基础但基础不等于要重复造轮子标准库放在那里不用的理由只有一个你还没学到它。4.3 排序后写回文件覆盖、备份和模式选择排序完成的下一步是写回文件。这里有个很容易踩的坑用“wb”还是“ab”。把“a”当成“追加”还是“覆盖”决定了文件是否保留原有内容。当年笔记里的需求是生成一份排序结果所以我推荐先备份原始文件再写新文件。做法是排序后打开一个新文件比如“students_sorted.dat”以“wb”写入排序后的数组。这样原始数据保留新文件是排序结果对比检查时谁都不会改坏原文件。如果想直接覆盖原文件必须先确认你在内存里已经保留了所有数据否则一旦打开原文件写入失败数据就彻底丢了。另外写回文件同样要区分二进制和文本需求。排序结果需要给人看时我用fprintf格式化成对齐的文本FILE *fp fopen(students_sorted.txt, w); if (fp NULL) { perror(无法打开文件); return 1; } for (int i 0; i n; i) { fprintf(fp, %s %s %d %d %d\n, readBack[i].name, readBack[i].id, readBack[i].score1, readBack[i].score2, readBack[i].score3); } fclose(fp);这个版本比二进制更适合演示因为你能直接打开看结果。而如果排序后还要再被程序读取那继续用二进制fwrite反而更方便。所以写代码前一定要先问一句这个文件是给人看还是给程序看的两种答案对应两种模式。5. 重读笔记之后才想明白的坑全在这三条里5.1 换行符一个字节引发的“看起来正常”的错乱笔记里讲过文本文件按行读写但没有往深一层讲换行符在不同平台上不一致的问题。在文本模式下程序里写一个\nWindows系统会转换为\r\n两个字符存盘读取时反向把\r\n还原成\n。在二进制模式下\n就是一个字节0x0A不做任何转换。这个差异放在当年纯中文Windows环境不明显因为大家基本只在文本模式写文件。现在做跨平台开发就必须重视一个在Windows下生成的文本文件直接放到类Unix环境读取每一行末尾会多出一个\r打印时经常看到奇怪的换行效果反过来Unix换行文件拿到Windows的记事本里会显示成一行。解决办法是统一用二进制模式“rb”、“wb”打开文件自己处理换行或者用文本模式时明确知道平台转换规则。写文件落盘前先想清楚目标平台比事后清理\r高效太多。5.2 feof不是你想的那样用第十一章笔记里当年很多同学读文件时都用过while (!feof(fp)) { fscanf(fp, %s %s %d %d %d, ...); // 处理读取的数据 }这看似聪明实则会在文件末尾多处理一次。feof只有在读取函数试图越过文件末尾之后才会被置位。也就是说读完最后一条有效数据时feof还是假程序会再进入循环一次这次fscanf读取失败但代码里如果没有检查返回值就会把上一次循环残留的数据又处理一遍造成重复记录或越界访问。正确的写法是把读取函数的结果作为循环条件while (fscanf(fp, %s %s %d %d %d, readBack[n].name, readBack[n].id, readBack[n].score1, readBack[n].score2, readBack[n].score3) 5) { n; }fscanf返回成功转换的元素个数等于5才说明读到了一条完整记录。读到末尾时会返回EOF循环自然退出。这样写不仅代码短还天然处理了“最后一行的空白导致异常读取”的问题。5.3 中文内容读写时的字节梦魇排序、加密都和数据内容打交道一旦数据里有中文2015年的笔记本就变得有点旧了。那会儿很多中文系统默认使用GBK编码一个汉字占2字节现在主流环境已经广泛转向UTF-8一个汉字占3字节。同样一块char name[20]字段能存下的汉字数量完全不同。如果用fprintf写字符串程序内部以多少字节一个汉字当作字符数组来看待文件里就会以相应编码写出对应的字节。写回后用fscanf按%s读入遇到空格、换行符会截断中文通常不含空格所以整段能被读回来。但这不代表没有坑如果你的char数组长度是20而一个UTF-8中文名含有7个汉字共21字节就会溢出写坏内存GBK年代同样容量的数组能多存两个汉字。这个细节在2015年不太起眼如今却直接决定一个文件处理程序是否支持中文数据。涉及加密时要更小心。异或加密会把中文字节的高位字节改得面目全非文件变成纯二进制流这是预期行为。解密时只要按原顺序异或回来就能还原成原始字节进而恢复中文。但如果加密时分了块或用了缓冲区就必须保证密文写入顺序和读取解密顺序一致否则文件直接变成一摊乱码。6. 重读之后我做成了一件小事把四段代码练成一体这次重读第十一章笔记我给自己定了一个小目标把结构体定义、文件写入、加密、读取排序、回写这几个片段串成一个完整程序。程序流程是录入一批学生信息放进结构体数组以二进制fwrite方式写入students.dat对students.dat做异或加密生成students_enc.dat再把students.dat读回内存按总成绩用qsort排序排序结果用fprintf写入students_sorted.txt并打印到屏幕。串起来之后你会发现几件事。第一文件操作本身代码量不大真正的复杂度全部集中在格式约定和状态管理上——你用什么格式写出去就要用对应格式读回来加密和解密的规则要完全对称。第二错误处理远比当年笔记里写的多打开文件要检查NULL写入后要检查fflush与fclose的返回值不然数据可能还在缓冲区里没落盘。第三一个功能完整的C语言数据处理程序其实不需要多少高深语法文件操作就是它走向真实世界的那道门槛。最后分享一个整理旧笔记时很实用的小技巧不要把旧代码直接拿来跑而是先抄一遍边抄边问为什么这么写。当年觉得理所当然的写法现在看可能是没有处理边界当年一笔带过的概念现在看可能是整个环节的命门。我就是这样把第十一章重新读了一遍读完又顺手在自己的练习目录里多建了一个mini项目。对你来说如果手边也翻出了类似的旧代码不妨试试这个笨办法。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑