在学习文件操作之前,我们的程序的运行过程(比如scanf和printf)主要是键盘和内存的交互,最后由屏幕显示结果。如果你试着将一段程序调试多次,你可能会发现每一次调试时,存放变量的地址都是不一样的,也就是说,这时的数据是存放于内存之中的,并没有持久化的存放进硬盘
通过使用文件,我们就可以直接把数据存放进电脑硬盘,从而实现数据的持久化
``
这里的“文件”指的是磁盘上的文件,可以类比记事本.txt和应用程序.exe等等
程序文件包括程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)
文件的输出内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件
比如我下了一个gal,自己又懒得把所有支线剧情推一遍,这时我在网上找的通关存档就是数据文件
一个文件必须有一个唯一的文件标识,以便于用户识别和引用
文件名包括3部分:文件路径+文件名主干+文件后缀,一般我们所说的文件标识指的其实是文件后缀,
为了看见/修改文件标识,在Windows11系统下你需要做如下图所示的设置
文件指针的概念非常重要
每一个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息
这些信息被存放在一个被系统取名为FILE的结构体变量里面
在vs2013提供的头文件stdio.h里面可以看到这个结构体大概长下面这样:
struct _iobuf
{
char* _ptr;
int _cnt;
char* base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
};
不过我们除了文件指针的声明方法以外并不需要过分了解细节
根据规定,文件在读写以前需要先打开,后关闭,为了实现这两个操作我们需要掌握两个函数的使用方法:
fopen函数用于打开函数,当打开函数的时候返回指向这个文件起始位置的地址,打开失败时返回空指针NULL;
再根据fopen函数的返回类型和函数的参数类型,我们就可以写出像下面这样的简单语句:
FILE* ptr = fopen("data.txt", "w");
if (ptr == NULL)
{
printf("文件打开失败\n");
return 0;
}
//文件打开成功,执行后续操作
在上图代码中,fopen的第二个参数“w”表示“操作”文件的方式,常见的文件使用方式放在下面的表格里面了:
文件使用方式 | 含义 | 如果指定的文件不存在 |
---|---|---|
“r” | 为了输入数据,打开一个已经存在的文本文件 | error |
“w” | 为了输出数据,打开一个文本文件 | 建立一个新文件 |
“a” | 向文本文件添加数据 | 建立一个新文件 |
“rb” | 为了输入数据,打开一个二进制文件 | error |
“wb” | 为了输出文件,打开一个二进制文件 | 建立一个新文件 |
“ab” | 向一个二进制文件尾添加数据 | error |
“r+” | 为了读和写,打开一个文本文件 | error |
“w+” | 为了读和写,建立一个新的文件 | 建立一个新文件 |
“a+” | 打开一个文件,在文件尾进行读写 | 建立一个新文件 |
“rb+” | 为了读和写打开一个二进制文件 | error |
“wb+” | 为了读和写,建立一个新的二进制文件 | 建立一个新文件 |
“ab+” | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新文件 |
关于函数fclose无需深究,在打开文件以后不要忘记关闭就好:
FILE* ptr = fopen("data.txt", "w");
if (ptr == NULL)
{
printf("文件打开失败\n");
return 0;
}
//文件打开成功,执行后续操作
fclose(ptr);//关闭文件
另外,大家可以在上面的代码里面看到,fopen的第一个参数为data.txt,这其实是一个相对路径,data.txt就和vs创建的解决方案存储在一起,如下图所示:
用绝对路径写,也就是磁盘:\文件夹\文件夹\记事本.txt这样的格式
就相对比较麻烦:
FILE* ptw = fopen("d:\\code\\xbs\\2022.10.16\\data2.txt", "w");
if (ptw == NULL)
{
printf("文件2打开失败\n");
return 0;
}
为避免产生困惑,可以先看一下下面这图来认识一下什么是“读”和“写”
在文件操作的相关函数中,“读”代表“内存读取文件中的内容”,“写”代表“内存向文件写入数据”,因此,我们通过这些函数用键盘向文件中写入数据的时候,也必须经过内存这个媒介,你应该可以在通讯录的部分体会到这一点
“顺序读写”里的“顺序”指的是一个元素一个元素地读写,或者一行一行地读写
按照功能分类,上图中的函数是“成对”的
与读写相关的函数除了顺序读写还有随机读写,不过随机读写的相关常用函数比较少,暂时不做详细讲解
这个函数可以把单个字符c写入到FILE*类型指针stream所指向的文件里面,如果你事先声明了一个字符串,fputc在读完一个字符以后还会自动把指针向后移动一个字节的大小,以便于一个接一个地写入数据;
如果写入成功,函数返回这个字符的ASCII码值,否则返回EOF;
如果你注意到了它的第一个参数类型为int,你应该可以猜到实际传输的就是字符的ASCII值,也就是说我们可以像下面这样向文件中写入26个字符:
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
int i, ret = 0;
FILE* ptr = fopen("data.txt", "w");
if (ptr == NULL)
{
printf("文件打开失败\n");
return 0;
}
for (i = 0; i < 26; i++)
{
fputc('a' + i, ptr);
}
fclose(ptr);
ptr = NULL;//关闭文件后不要忘记将指针置空
return 0;
}
与fputc相对的函数fgetc函数除了少了一个参数以外,使用方法和fputc大致相同,毕竟读完一个字符以后也会向后移动一位,返回值也是相同的规则:
下面我们把写文件和读文件的代码合并起来:
int main()
{
int i, ret = 0;
FILE* ptr = fopen("data.txt", "w");
if (ptr == NULL)
{
printf("文件打开失败\n");
return 0;
}
for (i = 0; i < 26; i++)
{
fputc('a' + i, ptr);
}
fclose(ptr);
ptr = NULL;
FILE* ptw = fopen("data.txt", "r");
if (ptw == NULL)
{
printf("文件打开失败\n");
return 0;
}
while ((ret = fgetc(ptw)) != EOF)
{
printf("%c ", ret);
}
fclose(ptw);
ptw = NULL;
return 0;
}
最后得出这个输出结果:
虽然fgetc和fputc在运行过程中是一个字符一个字符向后读取的,但是如果你尝试打印ptr和ptw的地址,你会发现地址没有发生变化,一直指向文件的开头,希望不要产生误会,误认为ptr和ptw发生改变
与fgetc和fputc一个字符一个字符地读取方式相比,fputs和fgets更加快捷,可以一行一行地读取和写入:
如果成功输入,fputs返回0,否则返回EOF,如果想实现“一行一行”地输入,不要忘记手动输入\n
fgets的功能是从stream这个地址开始向后读取n个字符大小的字符串,然后把首元素地址存储在string里面,但是这个函数最多只能读一行
如果这一行只有10个字符,却把n设置成了20,这个函数最多最多也只会把\n读走
但如果没有完全读完这一行的时候,函数实际上读走了n-1个字符,这是因为fgets读走的字符串末尾是带有\0的
如果成功输入,fgets返回读取到的字符串的首地址,否则返回EOF;
示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
int i = 0;
FILE* p = fopen("test.txt", "w");
char arr1[] = "test string\nwhat\n";
char* ptr = arr1;
fputs(arr1, p);
fclose(p);
p = NULL;
FILE* p2 = fopen("test.txt", "r");
printf("%s", fgets(ptr, 50, p2));
fclose(p2);
p2 = NULL;
return 0;
}
虽然我在test.txt这个文件里写了两行字符,但是程序最后只能读取并打出"test string",想使用循环一直打完整个文件的话需要知道fgets在发生错误或读完文件时返回NULL,而是否真的发生错误需要使用函数feof来判断
这两个函数的功能是从文件中“格式化地”读取数据,举个例子,以“%d %s”的形式打印参数,就可以称为“格式化”打印
这一对函数和printf和fscanf相比,就多了文件指针stream一个参数;
fscanf的功能是从文件中读取数据,以你选择的格式存储到你选择的变量中,fprintf的功能是从内存中选择变量,以特定的形式写入文件
现在我在程序目录中创建这样一个文件:
然后运行下面的代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include
struct S
{
char name[20];
int age;
int score;
};
int main()
{
struct S s = {0};
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
fscanf(pf, "%s %d %d",s.name, &(s.age), &(s.score));
printf("%s %d %d", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
FILE* pt = fopen("test.txt", "w");
char arr2[] = "lisi";
int b = 30, c = 100;
fprintf(pt, "%s %d %d", arr2, b, c);
fclose(pt);
return 0;
}
你会惊奇的发现虽然屏幕打印出了我们之前写入的数据,但test.txt却已经变成lisi的形状了:
这其实是因为我们两次打开文件时用的都是指向文件开头的地址,但是也存在其他方法,可以让我们可以在已知指针与文件起始位置的偏移量的时候从这个位置写入数据
这3个函数分别为fseek,ftell和rewind,感兴趣的同学可以到这个网站学习一下它们的具体功能传送门:cplusplus
还是先看一下函数的返回类型和参数:
fread这个函数的功能是从*stream这个位置读取count个size大小的元素,写入到buffer指向的文件里面,最终返回成功读取的元素个数,如果数据全部读完或者程序发生错误,返回值会小于count,但我们可以用ferror或feof来断定具体情况
如果count为0,函数的返回值为0,buffer指向的空间也不会发生改变
fwrite这个函数的功能是从*buffer这个位置读取count个size大小的元素,写入到stream指向的文件里面,最终返回成功写入的元素个数,如果这个数据与count不同的话,程序会自动停止,但我们可以用ferror来断定具体情况
这两个函数的返回值部分的讲解有点钻牛角尖了,在我们实际应用时,目前也只有通讯录这个“大工程”,而且我自己在写的时候甚至未曾产生对于函数返回值判定的担忧,根本原因还是因为见识浅薄所以无法举出恰当例子,不过如果我在日后遇到了相关的问题,我一定会回来写博客补票
直接说结论:
在文件读取过程中,不能用feof函数的返回值直接判断文件是否结束,而是应当用于文件读取结束的时候,判断文件是读写失败结束,还是遇到文件末尾结束
这就需要通过牢记函数的返回值来判断了,比如:
fgetc读取结束时返回EOF,fgets读取结束时返回NULL;fread则需要判断返回值是否小于实际要读的个元素个数
ANSIC标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区的时候才会送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区。缓冲区的大小由C编译系统决定
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件,如果不这样做的话可能导致读写文件的问题
笔者认为这部分的难点还是容易混淆“读”和“写”的定义,从而导致使用函数的时候产生困惑,多练一下,写个通讯录或者系统地梳理一下相关函数的细节就会好很多