【C】文件操作
这篇文章我们将会讲解C语言中跟文件相关的操作,包括为什么使用文件,文件是什么,打开与关闭文件,文件的顺序读写,文件的随机读写,文件读取结束的判定以及文件缓冲区。
目录
1 为什么使用文件
2 文件是什么
1) 程序文件
2) 数据文件
编辑3) 文件名
4) 二进制文件与文本文件
3 文件的打开与关闭
1) 流与标准流
(1) 流
(2) 标准流
2) 文件指针
3) 打开
4) 关闭
4 文件的顺序读写
1) fgetc函数
2) fputc函数
3) fgets函数
4) fputs函数
5) fscanf函数
6) fprintf函数
7) fwrite函数
8) fread函数
5 文件的随机读写
1) fseek函数
2) ftell函数
3) rewind函数
6 文件读取结束判定
1) feof与ferror函数
2) 正确判断文件读取结束
7) 文件缓冲区
1 为什么使用文件
在之前写过的代码中,运行程序的结果都会显示在控制台,也就是运行程序时那个黑框框上,但是运行结束后,程序运行的结果并不会保存下来,因为程序运行的数据都在内存中,程序退出,内存回收,数据就丢失了,无法保存。所以使用文件就可以用来将数据长期保存起来。
2 文件是什么
在程序设计中,我们所指的文件主要包括两种,一种是程序文件,另一种是数据文件。
1) 程序文件
所谓程序文件,就是程序经过一系列的预处理、编译、汇编、链接一系列过程之后所生成的文件,主要包括三种,一种是源程序文件(后缀为.c或者.h),一种是目标文件(后缀是.obj),还有一种是可执行程序(后缀是.exe)。
2) 数据文件
所谓数据文件,就是存储数据的文件,里面的内容不一定是程序,而是程序在运行时读写的数据。
里面的数据:
3) 文件名
每个文件有一个唯一的标识,就是文件名,用于方便用户识别和引用。
每个文件名都有三个组成部分:文件路径 + 文件名主干 + 文件后缀。比如:C:\Clanguage\test-1-6.txt,其中C:\Clanguage\就是文件路径,代表文件处在C盘Clanguage文件夹下,test-1-6是文件名的主干,.txt就是文件名的后缀,表示该文件是什么文件,.txt表明该文件是一个记事本文件。
在通常情况下,我们看到的都是文件名主干,后缀如果不隐藏的话也是可以看到的。
4) 二进制文件与文本文件
根据数据的组织形式,数据文件被称为文本文件和二进制文件。
数据如果在内存中以二进制的形式存储,不转换为文本直接输出到外存上的文件中,就是二进制文件。二进制文件如果直接打开,我们看到的会是一堆乱码,根本看不懂。
如果要求在将数据以ASCII码值的形式存储在外存上,那么存储数据的文件就叫做文本文件。
比如,有一个数据5000,如果直接以二进制形式,也就是00000000 00000000 00010011 10001000 直接存储到外存上,那就是二进制文件,如果转换为ASCII码值,也就是00110101 00110000 00110000 00110000 00110000 (5的ASCII码值为53,0的ASCII码值为48)就是文本文件(直接显示为5000)。
3 文件的打开与关闭
1) 流与标准流
(1) 流
我们写程序就是将数据输入到各种各样的外部设备上,比如硬盘,U盘等,而对于不同的外部设备,其操作也是不同的,所以为了方便程序员操作,就抽象出了流的概念。
流就相当于是程序员和各种外部设备之间交流的一种工具,有了流之后,我们只需要关注如何将数据输入到流之中,至于流如何将数据输入到各种设备里面,我们是不需要关注的。
(2) 标准流
既然有了流的概念,那么为什么我们从键盘上输入数据或者从屏幕上输出数据的时候,为什么没有打开流呢?
因为C程序在启动的时候,就会打开3个标准流:
(1) stdin -- 标准输入流。在大多数环境下是从键盘输入数据,如scanf函数就是
从stdin中获取数据。
(2) stdout -- 标准输出流。大多数环境是将数据直接输出到屏幕上,如printf函数
就是将数据输入到stdout流中。
(3) stderr -- 标准错误流。大多数环境下会输出到显示器界面。
这三个标准流的类型都是FILE* 类型的,也就是文件指针。
在C语言里面,就是通过FILE* 类型的指针来维护各种关于流的操作的。
2) 文件指针
当一个文件被使用时,C程序就在内存中自动开辟了一个文件信息区用来存放文件的相关信息(如文件的名字、状态、所在位置等等信息)。当一个文件被打开的时候,这些信息统一填充在一个系统自动声明的一个结构体中,这个结构体的名字就是FILE,在VS2013中对FILE结构体的定义(不同编译器对FILE结构体的定义不完全相同)如下:
struct _iobuf
{
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
文件指针就是指的是 FILE* 类型的指针,该类型的指针指向了打开文件是自动开辟的对应文件信息区的起始地址,所以一般都是通过定义一个 FILE* 类型的文件指针来维护所打开的一个文件,也就是,通过文件指针可以间接找到与之相关联的文件。
3) 打开
用C语言打开一个文件需要用到一个函数 fopen ,该函数原型为:
fopen函数共有两个参数,第一个参数是一个字符指针,指向了想要打开的文件的名字字符串的地址。第二个参数也是一个字符指针,指向了想要打开文件模式的字符串的地址,打开的模式共有一下几种:
文件打开方式 | 含义 | 如果指定文件不存在时 |
---|---|---|
"r"(只读) | 为输入数据,打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为输出数据,打开一个文本文件 | 新建一个文件 |
"a"(追加) | 向文本文件末尾添加数据 | 新建一个文件 |
"rb"(二进制只读) | 为输入数据,打开一个已经存在的二进制文件 | 出错 |
"wb"(二进制只写) | 为输出数据,打开一个二进制文件 | 新建一个文件 |
"ab"(二进制追加) | 向二进制文件末尾添加数据 | 新建一个文件 |
"r+"(读写) | 为读和写,打开一个已经存在的文本文件 | 出错 |
"w+"(读写) | 为读和写,打开一个文本文件 | 新建一个文件 |
"a+"(读写) | 打开一个文件,在文件尾进行读写 | 新建一个文件 |
"rb+"(二进制读写) | 为读和写,打开一个已经存在的二进制文件 | 出错 |
"wb+"(二进制读写) | 为读和写,打开一个二进制文件 | 新建一个文件 |
"ab+"(二进制读写) | 打开一个二进制文件,在文件尾进行读和写 | 新建一个文件 |
表格中的输出和输入数据是对于程序来说的,而写文件和读文件是对于文件来说的,所以输出数据就相当于写文件,而输入数据就相当于读文件。
如果使用fopen函数正确打开一个文件的话,fopen函数会返回打开文件的文件指针,而如果打开失败,则返回NULL,所以我们需要判断fopen函数的返回值,看文件是否打开成功。
如以写的形式打开一个data,txt文件:
#include<stdio.h>
int main()
{
//创建一个文件指针变量,接受fopen的返回值
//后面的模式是字符串,一定要用双引号引起来
FILE* pf = fopen("data.txt", "w");
//判断是否打开成功
if (pf == NULL)
{
//打印错误信息
perror("fopen");
retrun 1;
}
//文件操作
return 0;
}
4) 关闭
文件的关闭也需要用到一个函数:fclose函数,函数原型如下:
fclose函数只有一个参数,就是要关闭文件的文件指针。但是,使用fclose函数有一个需要注意的点,就是对应的文件指针一定要置为NULL, 因为fclose函数并不会将文件指针置为NULL,而文件信息区又关闭了,所以经过fclose函数之后,那个文件指针就变为了野指针,需要置为NULL。
打开data.txt的完整代码为:
#include<stdio.h>
int main()
{
//创建一个文件指针变量,接受fopen的返回值
//后面的模式是字符串,一定要用双引号引起来
FILE* pf = fopen("data.txt", "w");
//判断是否打开成功
if (pf == NULL)
{
//打印错误信息
perror("fopen");
retrun 1;
}
//文件操作
//关闭文件
fclose(pf);
//pf变为了野指针
pf = NULL;
return 0;
}
另外,还有两个点需要注意一下:
(1) 关于文件打开的 "w" ,"w+","wb","wb+"模式,对于一个已经存在的文件,当再次使用这些模式打开文件时,这些文件中的内容会先清空,然后再写程序中写入文件的数据。
比如:有一个data.txt文件,在运行程序之前,里面存放有abcde五个字符,而使用上述模式打开文件,向文件中写入了fghij五个字符,文件中并不是有abcdefghij十个字符,而是只有fghij五个字符。
(2) 在fopen的第一个参数文件名的字符串里面,有相对路径和绝对路径两种路径。
绝对路径:是从根目录开始写,比如C:\Clanguage\code\test.txt就是一个绝对路径。
相对路径:相对路径有两种写法。第一种是直接写文件名主干及其后缀,比如data.txt,这种文件名默认是打开与源程序文件处于同一目录下的文件;另一种写法是用 . 来代替当前目录,.. 来代替前一个目录,如源文件test.c处在C:\Clanguage\code\test.c路径下,而data.txt处在C:\Clanguage\data.txt目录下,那么打开这一data.txt文件就可以写为 .\..\..\data.txt。
4 文件的顺序读写
文件中存在一个文件内容的光标,操控着文件中内容的读取。这个光标会在读取一个字符后,自动向后跳一个字符,比如:文件中有abcdef 六个字符,刚开始文件光标在a字符的前面,读取a字符之后,光标就会跳到b字符的前面。文件的顺序读写就是指按照文件光标向后走的熟悉,一个字符一个字符的依次读写。
文件的顺序读写通过许多函数来实现,包括fgetc,fputc,fgets,fputs,fscanf,fprintf,fread已经fwrite八个函数。
1) fgetc函数
函数原型:
fgetc函数是字符输入函数,该函数有一个参数,就是想要获取数据的流,使用fgetc函数的例子如下:
#include<stdio.h>
int main()
{
//打开文件,注意是包含读的形式
FILE* pf = fopen("data.txt", "r");
//判断是否打开成功
if (pf == NULL)
{
perror("fopen");
return 1;
}
//打开成功
//用fgetc函数获取一个字符
char ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这是通过fgetc函数获取一个字符,那如果想要获取多个字符,就需要利用fgetc函数的返回值。fgetc函数如果获取字符成功,那么会返回读取到的字符;如果读取失败,那么会返回EOF(end of file),所以可以通过判断返回值是否为EOF来判断文件是否读取结束,如:
#include<stdio.h>
int main()
{
//打开文件,注意是包含读的形式
FILE* pf = fopen("data.txt", "r");
//判断是否打开成功
if (pf == NULL)
{
perror("fopen");
return 1;
}
//打开成功
//用fgetc函数获取多个字符
char ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fgetc函数适用于所有的流,所以标准输入流(stdin)也是可以的,当把流改为stdin时,就会变为从控制台中由用户输入字符,如:
#include<stdio.h>
int main()
{
#include<stdio.h>
int main()
{
//用fgetc函数获取一个字符
char ch = fgetc(stdin);
printf("%c\n", ch);
return 0;
}
运行结果为:
2) fputc函数
函数原型:
该函数有两个参数,第一个参数是想要向文件中写入的字符,第二个参数为要写入的流,使用fputc函数的例子如下:
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char ch = 'A';
while (ch <= 'Z')
{
fputc(ch, pf);
ch++;
}
fclose(pf);
pf = NULL;
return 0;
}
该程序向data.txt里面写入了A - Z的26个英文字母。
同样的,fputc函数也适用于所有的流,包括标准输出流(stdout),将流改为stdout后,字符就会直接打印在屏幕上,如:
#include<stdio.h>
int main()
{
char ch = 'A';
while (ch <= 'Z')
{
fputc(ch, stdout);
ch++;
}
return 0;
}
运行结果为:
3) fgets函数
函数原型:
该函数包括三个参数,第一个参数为一个 char* 类型的指针,指向了想要从文件中读取字符串后放到的字符数组;第二个参数为想要读取的最大字符个数,但实际上只会读取num - 1个字符,因为最后一个字符会添加为 '\0' ,作为字符串的结束标志; 第三个参数为想要读取数据的流,如:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char arr[100];
fgets(arr, 10, pf);
printf("%s\n", arr);
fclose(pf);
pf = NULL;
return 0;
}
调试结果为:
test.txt 文件中存放了"hello world\n" 和 "hello w" 两行字符串,在上述代码中,只在文件中读取10个字符,但实际上通过调试可以看到在 arr 数组中9个字符,第10个字符变为了'\0'。
该函数也适合与所有的流,所以当把流变为stdin时,该函数会变为从控制台由用户输入数据,如:
#include<stdio.h>
int main()
{
char arr[20] = {0};
fgets(arr, 10, stdin);
printf("%s\n", arr);
return 0;
}
运行结果:
4) fputs函数
函数原型:
该函数有两个参数,第一个参数为想要向流中写入的字符串的地址,第二个参数是想要写入的流,使用 fputs 函数的例子为:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//向文件中写入一行"hello y"字符串
fputs("hello y", pf);
fclose(pf);
pf = NULL;
return 0;
}
需要注意的一点是,fputs函数并不会主动在写入字符串之后,在后面加上换行,所以如果在写入一样字符串之后想要换行,就得在字符串后面加上 '\n' 换行符。
fputs 函数也适用于所有的流,将第二个参数改为stdout后,会将字符串直接打印到屏幕上,如:
#include<stdio.h>
int main()
{
fputs("hello world\n", stdout);
fputs("hello w\n", stdout);
fputs("hello y\n", stdout);
fputs("hello f\n", stdout);
return 0;
}
运行结果:
5) fscanf函数
函数原型:
我们再来看一下scanf函数的原型:
可以看到 fscanf 与 scanf函数参数几乎是相同的,所以使用 fscanf 函数的时候只需要按照 scanf 函数的方式使用 fscanf 函数,只不过需要在前面加个流,如:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int a;
char b;
char arr[20];
fscanf(pf, "%d %c %s", &a, &b, arr);
printf("%d %c %s", a, b, arr);
fclose(pf);
pf = NULL;
return 0;
}
在test.txt文件中,文件内容为10 x helloworld,运行结果为:
fscanf 函数也适用于所有的流,所以当把流变为 stdin 时,fscanf 函数也就变为了 scanf 函数,如:
#include<stdio.h>
int main()
{
int a;
char b;
char arr[20];
fscanf(stdin, "%d %c %s", &a, &b, arr);
printf("%d %c %s", a, b, arr);
return 0;
}
运行结果:
6) fprintf函数
函数原型:
再来看一下 printf 函数的原型:
fprintf 函数与 printf 函数参数几乎一样,也是在前面就多了一个参数流,所以使用 fprintf 函数就只是在使用 printf 函数前面加一个流就可以了,如:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fprintf(pf, "%d %c %s", 10, 'x', "helloworld");
fclose(pf);
pf = NULL;
return 0;
}
fprintf 函数也适用于所有的流,当把第一个参数变为 stdout 的时候,fprintf 函数就变为了 printf 函数,如:
#include<stdio.h>
int main()
{
fprintf(stdout, "%d %c %s", 10, 'x', "helloworld");
return 0;
}
运行结果为:
7) fwrite函数
函数原型:
fwrite 函数是专门用来进行二进制文件书写的函数,该函数的功能是向文件中写入二进制数据,该函数有4个参数,第一个参数是一个void* 类型的指针,指向了要写入文件数据的数组的首元素;第二个参数是写入的每个数据的大小,单位是字节;第三个参数是要写入数据的个数;第四个参数是要写入的文件流。使用 fwrite 函数的例子如下:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int arr[] = {1, 2, 3, 4, 5};
fwrite(arr, sizeof(int), 5, pf);
fclose(pf);
pf = NULL;
return 0;
}
由于是二进制文件写入文件,所以我们直接打开文件是看不懂的:
这时候我们通过编译器自带的二进制编辑器来看懂二进制文件,步骤如下:
第一步:
在解决资源方案管理器中右击源文件,选择添加 -- 现有项,然后将生成的二进制文件添加进来
第二步:
右击文件,然后选择打开方式,向下滑,选择二进制编辑器,点击确定
8) fread函数
除了以上方法可以看懂二进制文件,我们也可以通过fread函数来解读二进制文件,函数原型:
fread 函数和fwrite 函数一样,同样有4个参数,第一个参数是一个void* 类型的指针,指向了要存储读取数据的数组的首元素;第二个参数是读取的每个元素的大小;第三个参数是共读取元素的个数;第四个参数是要读取文件的流,使用 fread 函数的例子如下:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int arr[10] = {0};
int i = 0;
while (fread(arr + i, sizeof(int), 1, pf))
{
i++;
}
for (int j = 0;j < i;j++)
{
printf("%d ", arr[j]);
}
fclose(pf);
pf = NULL;
return 0;
}
运行结果为:
上述代码中,根据 fread 函数的返回值来确定是否读取结束,fread 函数的返回值为成功读取的元素个数,所以上面那个while循环的判断条件才为fread(arr + i, 4, 1, pf),如何读取到了元素就会返回1,进入循环,如果读取失败,那么会返回0,不进循环。
需要注意的是,fread函数和fwrite函数并不像前6个函数适用于所有的流,这两个函数只适用于文件流,不适用于标准流(stdin,stdout,stderr)。
5 文件的随机读写
不同于文件的顺序读写,文件的随机读写是指可以随机读取文件中的数据,不必按照文件光标的顺序依次读取。文件的随机读写主要是通过3个函数实现的,分别的 fseek,ftell 和 rewind函数来实现的。
1) fseek函数
函数原型:
fseek 函数主要实现文件中光标的移动,第一个参数为想要移动光标的文件流;第二个参数为偏移量;第三个参数为偏移量的起始位置。
要注意的是,第二个参数需要取决于第三个参数。第三个参数共有三个取值,分别是:
(1) SEEK_SET
(2) SEEK_CUR
(3) SEEK_END
SEEK_SET 是指文件的起始位置,SEEK_CUR 为文件当前光标的位置,SEEK_END 为文件的末尾位置。想要光标如何移动,根据第3个参数是什么,算出偏移量就可以。如:文件中有 abcdef 六个字符,通过fgetc获取到了一个字符,然后光标位置移到了 b 的前面,想要读到 e 的话,如果 fseek 函数第三个参数为 SEEK_SET,则偏移量为 4;如果第三个参数为 SEEK_CUR,则偏移量为 3,如果第三个参数为 SEEK_END,则偏移量为 -2。该例子写成代码:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char ch = 'a';
for (;ch <= 'f';ch++)
{
fputc(ch, pf);
}
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
ch = fgetc(pf);
printf("%c\n", ch);
//fseek(pf, 4, SEEK_SET);
//fseek(pf, 3, SEEK_CUR);
fseek(pf, -2, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
运行结果为:
2) ftell函数
函数原型:
该函数的功能是返回当前函数光标相对于文件起始位置的偏移量,例子如下:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char ch = 'a';
for (;ch <= 'f';ch++)
{
fputc(ch, pf);
}
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//fseek(pf, 4, SEEK_SET);
//fseek(pf, 3, SEEK_CUR);
fseek(pf, -2, SEEK_END);
size_t ret = ftell(pf);
printf("%zd", ret);
fclose(pf);
pf = NULL;
return 0;
}
运行结果为:
3) rewind函数
函数原型:
该函数是让函数光标返回文件起始位置,例子如下:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char ch = 'a';
for (;ch <= 'f';ch++)
{
fputc(ch, pf);
}
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fseek(pf, -2, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);
rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
运行结果为:
6 文件读取结束判定
1) feof与ferror函数
feof 函数的作用为已知文件已经读取结束,判断是否因为遇到文件尾而结束。
ferror 函数的作用为已知文件已经读取结束,判断是否是因为发生读取错误而结束。
所以并不能用 feof 函数来判断文本文件是否读取结束。
2) 正确判断文件读取结束
正确判断文件是否读取结束,应该合理利用以上函数的返回值,如 fgetc 函数如果读取结束则会返回EOF,判断 fgetc 函数返回值是否为EOF即可;fgets 函数如果读取结束,则返回值为NULL,所以判断返回值是否为NULL即可;再比如,fread 函数的返回值为成功读取的元素个数,所以只要返回值小于所写的读取参数个数(第3个参数),就表明这是最后一次读取。
7) 文件缓冲区
ANSIC标准是采用“缓冲文件系统”处理数据文件的。缓冲文件系统实际上是系统自动在内存中为程序每一个正在使用的文件开辟的一块“文件缓冲区”。从内存中向磁盘输入数据时,数据会先放在缓冲区,然后等缓冲区装满了,再一块送到磁盘。如果是从磁盘向计算机读入数据,也会存在一个内存缓冲区,然后等内存缓冲区装满之后,再从缓冲区逐个地向程序数据区输送数据。
其实缓冲区是为了提高效率而存在的。由于缓冲区的存在,导致程序对于数据的存取和输送的次数不会很频繁,进而提高效率。
所以当程序向文件中写入数据的时候,由于存在文件缓冲区,所以程序会先将数据放在文件缓冲区,当满足“一定条件”时,文件缓冲区才会向文件中输入数据,“一定条件”包括3种情况:
(1) 当文件缓冲区满了时
(2) 主动刷新缓冲区时
(3) 使用fclose函数关闭文件时
可以通过以下这个例子来感受文件缓冲区的存在:
#include<stdio.h>
#include<windows.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("abcdef", pf);
printf("睡眠10秒,已经写数据了,打开文件,但文件中没有内容\n");
//睡眠函数,里面参数单位是毫秒
Sleep(10000);
printf("刷新缓冲区\n");
//刷新缓冲区,将缓冲区里的数据放到文件中
fflush(pf);
printf("再睡眠10秒,发现文件中有内容了\n");
Sleep(10000);
fclose(pf);
pf = NULL;
return 0;
}
到这里,有关文件的内容就讲解完成了,跟文件相关的函数较多,但大多都比较简单,只要多加练习,相信大家就可熟练使用这些函数。