人 类 高 质 量 文 件 操 作 教 学(上)【C语言】
🔥 CSDN 累计订阅量破千的火爆 C/C++ 教程的 2023 重制版,C 语言入门到实践的精品级趣味教程。 了解更多: 👉 "不太正经" 的专栏介绍 ← 试读第一章 订阅链接: 🔗《C语言趣味教程》 ← 猛戳订阅! |
前言:
本章为文件操作教学上篇,由浅入深的引入问题,然后逐一介绍知识。将详细讲解文件的打开和关闭、文件的顺序读写并精讲函数部分,初步学习“流”的概念!
🚪 传送门:文件操作(下)
一、问题引入
0x00 什么是文件?
【百度百科】电脑文件,也可以称之为计算机文件,是存储在某种长期储存设备或临时存储设备中的一段数据流,并且归属于计算机文件系统管理之下。所谓“长期储存设备”一般指磁盘、光盘、磁带等。而“短期存储设备”一般指计算机内存。需要注意的是,存储于长期存储设备的文件不一定是长期存储的,有些也可能是程序或系统运行中产生的临时数据,并于程序或系统退出后删除。
📚 简单来讲,就是磁盘上的文件。
0x01 为什么使用文件?
📚 举个例子,我们想实现一个 “通讯录” 程序时,在通讯录中新建联系人、删除联系人等一系列操作,此时的数据存储于内存中,程序退出后所有数据都会随之消失,为了让通讯录中的信息得以保存,也就是想让数据持久化,我们就需要采用让数据持久化的方法。我们一般数据持久化的方法有:把数据存放在磁盘文件中,或存放到数据库等方式。
0x02 什么是程序文件和数据文件?
📚 但在程序设计中,我们一般所说的文件为 程序文件 和 数据文件 。
📂 程序文件:程序文件包括源程序文件(后缀为.c),目标文件(Windows环境下后缀为.obj),可执行程序(Windows环境下后缀为.exe)。
📂 数据文件:数据文件的内容不一定是程序,而是程序在运行时读写的数据,比如程序运行需要从中读取数据的文件,或输出内容的文件。
💬 我们随便写一段代码,用于演示:
#include <stdio.h>
int main(void) {
printf("Hello,World!\n");
return 0;
}
🚩 随后运行代码(便于生成文件):
❓ 那么,什么是程序文件呢?
找到代码路径,打开文件夹查看 “可执行程序” :
退回到上层目录,找 “目标文件”:
❓ 那数据文件又是什么呢?
在代码路径下创建一个文件:
0x03 什么是文件名?
【百度百科】文件名是文件存在的标识,操作系统根据文件名来对其进行控制和管理。不同的操作系统对文件命名的规则略有不同,即文件名的格式和长度因系统而异。为了方便人们区分计算机中的不同文件,而给每个文件设定一个指定的名称。由文件主名和扩展名组成。
📚 存在的意义:一个文件要有一个惟一的文件标识,方便用户识别和引用。
❗ 文件名包含三个部分:文件路径 + 文件名主干 + 文件后缀
( 例如:C:\code2021\TestDemo.txt )
🔑 为了方便起见,文件标识通常被称为 文件名 。
🔺 本章我们将对 数据文件 进行探讨!
二、文件的打开和关闭
文件读写之前应该先打开文件,在使用结束后应该关闭文件。
在编写程序的时候,再打开文件的同时,都会返回一个 FILE* 指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用 fopen 函数来打开文件, fclose 函数来关闭文件。
0x00 文件指针
❓ 什么是文件指针:
【百度百科】在C语言中用一个指针变量指向一个文件,这个指针称为文件指针。通过文件指针就可对它所指的文件进行各种操作。
🔑 在缓冲文件系统中,有一个关键的概念是 "文件类型指针" ,简称 "文件指针" 。
📚 每个被使用的文件,都会在内存中开辟出一个相应的文件信息区。该信息区用来存放文件相关信息(如文件名、文件状态以及文件当前位置等)。这些信息是保存在一个结构体变量中的,该结构体类型是由系统申明的,名为 FILE (注意是类型)。
💬 例如由 VS2013 编译环境提供的 stdio.h 头文件中有以下的文件类型声明:
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
📌 注意事项:
① FILE 的结构在不同的C编辑器中包含的内容并不是不完全相同的,但还是颇为相似的。
② 每当打开一个文件时,系统会根据文件的状况自动创建一个 FILE 结构的变量,并填充其中的信 息,只要文件被读写发生变化,文件信息区也会跟着发生变化。至于文件变化时文件信息区是怎么变化和修改的,我们其实并不需要关心这些细节,因为C语言已经帮你弄好了。
③ 我们一般会通过一个 FILE 的指针来维护这个 FILE 结构的变量。并不会直接使用,而是拿一个结构体指针指向这个结构,通过这个指针来访问和维护相关的数据,这样使用起来会更加方便。
💬 下面我们来创建一个 FILE* 的指针变量:
定义 pf 是一个指向 FILE 类型的指针变量。可以使 pf 指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区的信息就能够访问该文件。
FILE* pf; // 文件指针变量
💡 也就是说,通过文件指针变量能够找到与他关联的文件。
0x01 fopen 函数与 fclose 函数
📜 头文件:stdlib.h
📚 ANSIC 规定使用 fopen 函数来打开文件, fclose 函数来关闭文件。
✅ filename 参数指的是文件名,mode 参数为打开方式,打开方式如下:
文件使用方式 | 含义 | 如指定文件不存在 |
---|---|---|
" r "(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
" w "(只写) | 为了输出数据,打开一个文本文件 | 建立一个新文件 |
" a "(追加) | 像文本文件尾添加数据 | 建立一个新文件 |
" rb "(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
" wb "(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新文件 |
" ab "(追加) | 象一个二进制文件尾添加数据 | 出错 |
" r+ "(读写) | 为了读和写,打开一个文本文件 | 出错 |
" w+ "(读写) | 为了读和写,建立一个新的文件 | 建立一个新的文件 |
" a+ "(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
" rb+ "(读写) | 为了读和写,打开一个二进制文件 | 出错 |
" wb+ "(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
" ab+ "(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
💬 代码演示:打开我们刚刚手动创建的 test.dat 文件
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test.dat", "w");
// 检查是否为空指针
if (pf == NULL) {
perror("fopen");
return 1;
}
/* 写文件 */
// 关闭文件
fclose(pf);
pf = NULL; // 记得将pf置为空指针
return 0;
}
🚩 (代码正常运行)
❓ 之前我们创建的 test.dat 的路径是在 路径下的,如果放在其他路径下可以读吗?
🔑 可以,但文件必须在该工程的路径下才行。
💬 我们把 test.dat 文件删除,然后打开方式改成 r 试试看:
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test.dat", "r");
// 检查是否为空指针
if (pf == NULL) {
perror("fopen");
return 1;
}
/* 写文件 */
// 关闭文件
fclose(pf);
pf = NULL; // 记得将pf置为空指针
return 0;
}
🚩 运行结果如下: (通过刚才的表格可知,如果 r 找不到指定的文件,会导致报错)
💬 如果不适用相对路径,使用绝对路径读文件:
可以使用绝对路径,但是要注意转义绝对路径中的斜杠!
#include <stdio.h>
int main(void) {
// FILE* pf = fopen("D:\code2021\0817\0817\test2.dat", "w"); // error
FILE* pf = fopen("D:\\code2021\\0817\\0817\\test2.dat", "w"); // 转移字符\
// 检查是否为空指针
if (pf == NULL) {
perror("fopen");
return 1;
}
/* 写文件 */
// 关闭文件
fclose(pf);
pf = NULL; // 记得将pf置为空指针
return 0;
}
📌 注意事项: 不关闭文件的后果:一个程序能够打开的文件是有限的,文件属于一种资源。如果只打开不释放,文件就会被占用。可能会导致一些操作被缓冲在内存中,如果不能正常关闭,缓冲在内存中的数据就不能正常写入到文件中从而导致数据的丢失。
三、文件的顺序读写
0x00 什么是顺序读写
首先要了解什么是读写:我们写的程序是在内存中,而数据是要放到文件中的,文件又是在硬盘上的。当我们把文件里的数据读到内存中去时,这个动作我们称之为输入/读取。反过来,如果把程序中的东西放到硬盘上,这个动作我们称之为输出/写入。
📚 顺序读写,顾名思义就是按照顺序在文件中读和写。
0x01 顺序读写函数一览表
0x02 字符输出函数 fputc
📚 介绍:将参数 char 指定的字符写入到指定的流 stream 中,并把位置标识符向前移动 (字符必须为一个无符号字符)。适用于所有输出流。
💬 代码演示:创建一个 test.txt,随后使用 fputc 函数分别写入 "abc" 到文件中
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test.txt", "w");
if (pf == NULL) {
perror("fopen");
return 1;
}
// 写文件
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
🚩(代码正常运行)
📂 此时打开工程文件夹可以成功看到 test.txt 被创建了(大小为1kb可以看出写入成功了):
💬 我们正好测试下 w 的覆盖效果,我们把写的内容注释掉:
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test.txt", "w");
if (pf == NULL) {
perror("fopen");
return 1;
}
// 写文件
//fputc('a', pf);
//fputc('b', pf);
//fputc('c', pf);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
🚩 此时再次运行,我们发现那个文件里的内容不见了(大小也变为0kb):
❗ 值得注意的是,文件的写入是有顺序的。abc,先是a,然后是b,最后是c:
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
0x03 字符输入函数 fgetc
📚 介绍:从指定的流 stream 获取下一个字符,并把位置标识符向前移动(字符必须为一个无符号字符)。如果读取成功会返回相应的ASCII码值,如果读取失败它会返回一个EOF。适用于所有输入流。
💬 代码演示:在工程文件夹里打开 test.txt ,随便写入一些数据,随后使用 fgetc 函数读取:
#include <stdio.h>
// 使用fgetc从文件里读
int main(void) {
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
// 读文件
int ret = fgetc(pf);
printf("%c\n", ret);
ret = fgetc(pf);
printf("%c\n", ret);
ret = fgetc(pf);
printf("%c\n", ret);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
🚩 运行结果如下:
❓ 如果读完了会发生什么?
🐞 我们来调试一下看看:
0x04 文本行输出函数 fputs
📚 介绍:将字符串写入到指定的流 stream 中(不包括空字符)。适用于所有输出流。
💬 代码演示:利用 fputs 在 test2.txt 中随便写入几行数据:
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test2.txt", "w");
if (pf == NULL) {
perror("fopen");
return 1;
}
// 写文件 - 按照行来写
fputs("abcdef", pf);
fputs("123456", pf);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
🚩 (代码成功运行)
❓ 如果想换行呢?
🔑 换行需要在代码里自行加 \n :
fputs("abcdef\n", pf);
fputs("123456", pf);
(这时候打开文件,就是换行的了)
0x05 文本行输入函数 fgets
📚 介绍:从指定的流 stream 读取一行,并把它存储在 string 所指向的字符串中,当读取(n-1)个字符时,或者读取到换行符、到达文件末尾时,它会停止,具体视情况而定。适用于所有输入流。
📌 注意事项:假如 n 是100,读取到的就是99个字符(n-1),因为要留一个字符给斜杠0。
💬 代码演示:利用 fgets 读取 test2.txt 中的内容:
#include <stdio.h>
int main(void) {
char arr[10] = "xxxxxx"; // 存放处
FILE* pf = fopen("test2.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
// 读文件 - 按照行来读
fgets(arr, 4, pf);
printf("%s\n", arr);
fgets(arr, 4, pf);
printf("%s\n", arr);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
🚩 代码运行结果如下:
🐞 调试一下看看:
0x06 格式化输出函数 fprintf
📚 介绍:fprintf 用于对格式化的数据进行写文件,发送格式化输出到流 stream 中。适用于所有输出流。
💬 代码演示:将结构体的三个数据利用 fprintf 写到 test3.txt 中:
#include <stdio.h>
struct Player {
char name[10];
int dpi;
float sens;
};
int main(void) {
struct Player p1 = { "carpe", 900, 3.12f };
// 对格式化的数据进行写文件
FILE* pf = fopen("test3.txt", "w");
if (pf == NULL) {
perror("fopen");
return 1;
}
// 写文件
fprintf(pf, "%s %d %f", p1.name, p1.dpi, p1.sens);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
🚩 (代码成功运行)
0x07 格式化输入函数 fscanf
📚 介绍:fscanf 用于对格式化的数据进行读取,从流 stream 读取格式化输入。适用于所有输入流。
💬 代码演示:利用 fscanf 读取 test4.txt 中的内容,并打印:
#include <stdio.h>
struct Player {
char name[10];
int dpi;
float sens;
};
int main(void) {
struct Player p1 = { 0 }; // 存放处
// 对格式化的数据进行写文件
FILE* pf = fopen("test3.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
// 读文件
fscanf(
pf, "%s %d %f",
p1.name, &(p1.dpi), &(p1.sens)
); // 👆 p1.name本身就是地址(不用&)
// 将读到的数据打印
printf("%s %d %f\n", p1.name, p1.dpi, p1.sens);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
🚩 运行结果如下:
0x08 二进制输出函数 fwrite
📚 介绍:写一个数据到流中去,把 buffer 所指向的数组中的数据写入到给定流 stream 中。
💬 创建一个 test5.txt,用 fwrite 写入一个数据到 text5.txt 中去:
#include <stdio.h>
// 二进制的形式写
struct S {
char arr[10];
int num;
float score;
};
int main(void) {
struct S s = { "abcde", 10, 5.5f };
FILE* pf = fopen("test5.txt", "w");
if (pf == NULL) {
perror("fopen");
return 1;
}
// 写文件
fwrite(&s, sizeof(struct S), 1, pf);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
🚩 (代码成功运行)
我们发现他烫起来了2333(划掉)
💡 打开文件后我们发现只有abcde看得懂,后面是什么我们看不懂。
我们试着用 nodepad++ 打开:
❓ 为什么还是乱码?为什么 abcde 不是乱码?
🔑 解答:
① 我们刚才用的都是文本编译器,文本编译器打开二进制形式的文件完全是两种状态。
② 因为字符串以文本形式写进去和以二进制形式写进去是一样的,但是对于整数、浮点数等来说就不一样了,文本形式写入和二进制形式写入完全是两个概念。
(那么该怎么读呢,我们来看下面的 fread 函数)
0x08 二进制输入函数 fread
📚 介绍:从流中读取,从给定流 stream 读取数据到 buffer 所指向的数组中。
💬 用 fread 读取 text5.txt 中的二进制数据:
#include <stdio.h>
// 二进制的形式读
struct S {
char arr[10];
int num;
float score;
};
int main(void) {
struct S s = { 0 }; // 存放处
FILE* pf = fopen("test5.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
// 读文件
fread(&s, sizeof(struct S), 1, pf);
// 将读到的数据打印
printf("%s %d %f", s.arr, s.num, s.score);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
🚩 (代码正常运行)
🔺 总结: fwrite 和 fread 是一对,fwrire 写进去用 fread 读。
0x09 流的概念(stream)
在这里,我们补充一下流的概念。
📚 观察刚才的表格我们可以发现有的函数是适用于所有xx流的(比如 fputc 函数)。fputc 就适用于所有输出流,也就是说它不仅仅可以给文件里写。我们来读一下MSDN的介绍:
我们发现它还可以写到 stdout 上。
❓ 那么 stdout 是什么呢?
💡 stdout 就是标准输出流,在这里,我们要来讲一下流的概念。
📚 C语言默认打开的3个流:
① stdin - 标准输入流 - 键盘
② stdout - 标准输出流 - 屏幕
③ stderr - 标准输出流 - 屏幕
💬 我们用流向屏幕上输出信息 - stdout:
#include <stdio.h>
int main(void) {
fputc('a', stdout);
fputc('b', stdout);
fputc('c', stdout);
return 0;
}
🚩 a b c
💬 fgetc 从标准输入流读取 - stdin
#include <stdio.h>
// 使用fgetc从标准输入流中读
int main(void) {
int ret = fgetc(stdin);
printf("%c\n", ret);
ret = fgetc(stdin);
printf("%c\n", ret);
ret = fgetc(stdin);
printf("%c\n", ret);
return 0;
}
🚩 运行:
参考资料:
Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .
比特科技. C语言进阶[EB/OL]. 2021[2021.8.31]. .
📌 本文作者: 王亦优
📃 更新记录: 2021.8.19
❌ 勘误记录: 孙老师:有两张图违规了【现已修正】
💬 参考资料: 百度百科、比特科技、www.cplusplus.com、MSDN、RUNOOB.com
📜 本文声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!
未完待续...
🚪 传送门:文件操作(下)