一. 什么是文件
1. 文件基本概念
广义上磁盘上的文件都是文件。但在程序设计中我们把文件分为两类:程序文件、数据文件。
程序文件:包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
数据文件:文件的内容不一定是程序,还可能是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
下面我们讨论的是数据文件。
2. 文件标识
一个文件要有一个唯一的文件标识,以便用户识别和引用。其中文件标识包括三个部分:文件路径 + 文件名 + 文件后缀。
3. 文件类型
根据数据的组织形式,数据文件又分为为文本文件和二进制文件。
文本文件:如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
二进制文件:数据在内存中以二进制的形式存储,如果不加转换地输出到外存,默认就是二进制文件。
规定:字符一律按ASCII码形式存储,数值型数据既可以用ASCII码形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(一个整型数据占4个字节)。
4. 文件控制块
为了描述一个文件相关的信息,每个被使用的文件都在内存中开辟了一个相应的文件控制块,用来存放文件的相关信息(如文件的名字,文件状态及
文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有由系统声明的,取名FILE。
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。每当打开一个文件的时候,操作系统会根据文件的情况自动为该文件创建一个FILE类型的结构体变量,并填充其中的信息,使用者不必关心具体的实现细节
C将文件的内容存储到顺序字节流里,其实就是一块缓冲区,这个字节流以文件结束符EOF(end of file)作为结束标志。在文件控制块中有一个指针 _ptr 指向这个字节流的起始位置,相当于数组名;还有一个位置指示器 _charbuf 标识字节流当前的位置,相当于数组的下标。
二. 文件操作的函数
前面已经介绍了文件相关的基本知识和概念。那么在语言层面上(比如C语言),如何对一个文件进行打开、关闭、读写数据的操作呢?C语言提供了一系列文件操作相关的库函数,包含在头文件stdio.h里。
1. 文件的打开与关闭
1.1 文件打开函数 — fopen
FILE* fopen ( const char* filename, const char* mode );
参数介绍:
- filename:如果我们要打开的文件与我们当前正在运行的源代码在同级目录下,那么可直接写文件名字。不在同级目录下的话需要写明文件所在的路径(绝对路径或相对路径都可以)。
- mode:文件打开方式,常用的包括以下几种。
打开方式 | 含义 |
---|---|
“r” | read:打开文件进行输入操作。该文件必须存在。 |
“w” | write:为输出操作创建一个空文件。如果已存在同名文件,则丢弃其内容,并将该文件视为新的空文件。 |
“rb” | read:以二进制形式打开文件,进行输入操作。该文件必须存在。 |
“wb” | write:以二进制形式打开文件,为输出操作创建一个空文件。如果已存在同名文件,则丢弃其内容,并将该文件视为新的空文件。 |
功能:以特定的方式打开文件。操作系统会根据第一个参数的路径找到或创建该文件,并新建一个存储该文件信息的文件控制块,最后返回文件控制块的地址。注意:文件刚打开时,位置指示器指向最开始位置。
返回值:
- 打开成功:返回该文件的文件控制块的地址。
- 打开失败:返回空指针,即NULL。
函数使用举例
在当前路径下创建一个新文件log.txt,因为事先不存在这个文件,所以我们采用"w"模式打开。
// 1、打开文件
FILE* pf = fopen("log.txt", "w");
if(pf == NULL)
{
printf("open error\n");
return 1;
}
// 2、对文件进行一系列操作...
// 3、关闭文件
return 0;
运行结果:
1.2 文件关闭函数 — fclose
就像malloc动态开辟空间,使用完毕之后需要手动free释放掉这块空间一样的道理。我们通过fopen函数打开一个文件,操作系统为我们动态创建了这个文件的文件控制块,不使用的话需要fclose函数来释放它。注意并不是删除该文件,这个文件依然没有改变,只是删除fopen函数创建的存储该文件信息的文件控制块。
函数原型
int fclose ( FILE* stream );
参数:文件控制块的地址。
功能:释放开辟的文件控制块的空间。形象说就是关闭一个文件。
返回值:关闭成功返回0,失败返回EOF。
1.3 文件操作的大致流程
#include <stdio.h>
int main()
{
// 1、打开文件
FILE* pf = fopen("log.txt", "w");
if(pf == NULL)
{
printf("open error\n");
return 1;
}
// 2、对文件进行一系列操作
// .......
// 3、关闭文件
fclose(pf);
pf = NULL;
return 0;
}
2. 文件的顺序读写
在上面,我们介绍了最基本的打开、关闭文件,其本质是创建、释放文件控制块。当然这只是开始,最重要的是我们拿到文件控制块后,如何通过它来完成文件的读写操作。
对文件数据的读写可以分为顺序读写和随机读写。顺序读写,即挨着顺序逐个字符的对文件中的数据进行写入和读取。下面我们介绍C语言中对文件进行顺序读写的一系列函数。
2.1 字符的写入和读取函数 — fputc 和 fgetc
一、fputc
int fputc ( int character, FILE* stream );
参数
- character:被写入的字符。
- stream:文件控制块的地址。
功能:字符被写入字节流的位置指示器所指示的位置,然后该指示器将自动前进1。
返回值:写入成功返回这个被写入的字符。如果写入错误,则返回EOF并设置错误指示符(ferror)。
函数使用举例
在当前目录不下创建一个新的文件log.txt,写入小写字母序列a~z。
void Testfputc()
{
// 1、打开文件
FILE* pf = fopen("log.txt", "w");
// 2、使用fputc对文件进行字符的写入
int i = 0;
for(i = 'a'; i <= 'z'; ++i)
{
fputc(i, pf);
}
// 3、关闭文件
fclose(pf);
pf = NULL;
}
最终当前目录下生产一个文件log.txt。
二、fgetc
int fgetc ( FILE* stream );
参数:文件控制块地址。
功能:返回指定文件控制块的内部位置指示器当前指向的字符。然后将文件位置指示器+1。
返回值:
- 成功的话返回提取到的字符。
- 如果调用时字节流位于文件结束位置,则该函数返回EOF并设置文件结束指示符(feof)。如果发生其他读取错误,该函数也返回EOF,但设置其错误指示符(ferror)。
函数使用举例
从上面刚刚写入完成的log,txt里读取数据。
void Testfgetc()
{
// 1、用"r"方式打开已经存在的文件
// 刚打开时位置指示器指向字符的开始位置
FILE* pf = fopen("log.txt", "r");
// 2、使用fgetc依次读取文件的每一个字符
int ch = 0;
while((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
printf("\n");
// 3、关闭文件
fclose(pf);
pf = NULL;
}
编译运行
注意:读取的过程只是在移动文件信息区里的_ptr指针,文件内容并没有任何更改。
2.2 字符串的写入和读取函数 — fputs 和 fgets
一、fputs
int fputs ( const char* str, FILE* stream );
参数
- str:写入的字符串。
- stream:被写入的文件控制块指针。
功能:写一个字符串到字节流中,写入的字符串遇到’\0’结束,并且最后的’\0’不会被写入字节流。
返回值:成功返回一个非负的数字。当写入错误时,函数返回EOF并设置错误指示符(ferror)。
函数使用举例
我们对log.txt写入一个字符串“hellow world”。
void Testfputs()
{
// 1、以"w"方式打开文件
FILE* pf = fopen("log.txt", "w");
// 2、使用fputs写入一行字符串
const char* s = "hello world";
fputs(s, pf);
// 3、关闭文件
fclose(pf);
pf = NULL;
}
查看当前目录下生成的log.txt
二、fgets
char * fgets ( char* str, int num, FILE* stream );
参数:
- str:把文件里读取到的字符串放到str里。
- num:需要读取的字符的个数。
- stream:文件信息区地址。
功能:从字节流中读取字符,并将它们作为C字符串存储到str中,直到(num-1)字符被读取,或者到达换行符或文件结束符,以最先发生的为准。
返回值:
- 调用成功返回str。
- 如果读取到了字节流最后的文件结束符,则返回的指针为空指针,并设置文件结束符(feof)。
- 如果发生读取错误,则设置错误指示符(ferror)并返回一个空指针。
关于fgets读取结束后的几种处理方式说明
- 如果确实可以读取到 num-1 个字符,在把num-1个字符复制到目的地str之后,最后会自动附加一个’\0\到str,这样合起来一共是n个字符。
- 如果中途读取到字节流中的文件结束符,那么读取结束,最后依然会附加一个’0’。
- 如果中途遇到’\n’,读取结束,'\n’也一起读取到str中。
函数使用举例
读取前面写入的字符串"hello world‘"到字符数组str中
void Testfgets()
{
// 1、用"r"方式打开已经写入过的log.txt
FILE* pf = fopen("log.txt", "r");
// 2、使用fgets从文件里读取字符串放到str中
char str[12] = {0};
fgets(str, 20, pf);
printf("%s\n", str);
// 关闭文件
fclose(pf);
pf = NULL;
}
编译运行:
2.3 格式化写入和读取函数 — fprintf 和 fscanf
一、fprintf
int fprintf ( FILE* stream, const char* format, ... );
参数:第一个参数是被写入文件控制块地址,第二个参数格式控制序列,其用法就和printf一样。
功能:格式化写入数据。
返回值:写入成功返回写入的数据个数.如果发生写错误,则设置错误指示符(ferror)并返回一个负数。
函数使用举例
struct Person
{
char name[20];
int age;
};
void Testfprintf()
{
// 1、打开文件
FILE* pf = fopen("log.txt", "w");
// 2、对文件进行格式化写入
Person p = {"zhangsan", 18};
fprintf(pf, "%s %d", p.name, p.age);
// 3、关闭文件
fclose(pf);
pf = NULL;
}
查看当前目录下的log.txt文件
二、fscanf
int fscanf ( FILE* stream, const char* format, ... );
参数:第一个参数是被读取文件控制块地址,第二个参数格式控制序列,其用法就和scanf一样。
功能:从字节流里格式化读取数据到指定实参中。
返回值:
- 如果成功,函数将返回成功填充参数列表的项数。
- 如果发生读取错误或在读取时到达文件结束,则设置对应的指示符(feof或ferror)。返回EOF。
函数使用举例
struct Person
{
char name[20];
int age;
};
void Testfscanf()
{
// 1、打开前面格式化写入之后的文件
FILE* pf = fopen("log.txt", "r");
// 2、用fscanf格式化读取文件内容
Person p;
fscanf(pf, "%s %d", p.name, &p.age);
// 3、打印文件中读取出来的数据
printf("%s %d\n", p.name, p.age);
// 4、关闭文件
fclose(pf);
pf = NULL;
}
编译运行:
2.4 二进制写入和读取函数 — fwrite 和 fread
一、fwrite
size_t fwrite ( const void* ptr, size_t size, size_t count, FILE* stream );
参数:
- ptr:指向要写入的元素数组的指针。
- size:每个元素所占字节大小。
- count:元素个数。
- stream:文件控制块地址。
功能:
返回值:
- 写入成功返回写入的元素个数count。
- 如果返回的数字与count不同则写入错误,设置错误指示符(ferror)。如果size或count为零,则函数返回零,错误指示符保持不变。
函数使用举例
void Testfwrite()
{
// 1、以"wb"方式打开文件,写入内容将转化为二进制形式
FILE* pf = fopen("log.txt", "wb");
// 2、把数组arr的内容以二进制形式写入到文件中
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
fwrite(arr, sizeof(int), sizeof(arr)/sizeof(int), pf);
// 3、关闭文件
fclose(pf);
pf = NULL;
}
编译运行后,看到log.txt里都是一些看不懂的二进制乱码。
二、fread
size_t fread ( void* ptr, size_t size, size_t count, FILE* stream );
参数:和fwrite的一样,就不在重复介绍了,要说差别的话就是第一个参数作为输出型参数要被修改,所以是非const的。
功能:从字节流中读取count个元素,每个元素的大小为size字节,并将它们存储在ptr指定的内存块中。
返回值:
- 返回成功读取的元素总数count。
- 如果这个数字与count参数不同,则可能是在读取时发生了读取错误或到达了文件结束。在这两种情况下,都设置了标识符,可以分别用ferror和feof来检查。
函数使用举例
读取刚刚写入到文件里的二进制存储的数据。
void Testfread()
{
// 1、以"rb"方式打开文件,进行二进制内容的读取
FILE* pf = fopen("log.txt", "rb");
// 2、读取文件内容到数组arr中
int arr[10] = {0};
fread(arr, sizeof(int), 10, pf);
int i = 0;
for(i = 0; i < 10; ++i)
{
printf("%d ", arr[i]);
}
printf("\n");
// 3、关闭文件
fclose(pf);
pf = NULL;
}
编译运行:
3. 文件的结束判定
我们在使用读取文件内容的函数时,读取结束有两种特殊的情况,即读取过程中出现错误或读取到文件结束标志,这时都返回相同的值,但它们设置了不同标识符,可以分别用ferror和feof来检查和加以区分读取文件结束的原因。
读取相关的函数 | 返回值 |
---|---|
fgetc | 出错设置指示符(ferror),读取到EOF设置标识符(feof)。但都返回EOF。 |
fgets | 出错设置指示符(ferror),读取到EOF设置标识符(feof)。但都返回NULL。 |
fscanf | 出错设置指示符(ferror),读取到EOF设置标识符(feof)。但都返回EOF。 |
fread | 返回的值小于要求读取的完整项的数目时,可能是读取数据时发生错误(设置标识符ferror),也可能是在达到读取的规定数目之前遇到文件结尾(设置标识符feof)。但都返回小于要求读取的完整项的数目。 |
3.1 ferror
int ferror ( FILE* stream );
只需传入文件控制块地址,函数内部会去检查里面的错误标识符是否为ferror,即检查是否发生错误,若使用时没有发生错误,则ferror函数返回0;否则,ferror函数将返回一个非零的值。
函数使用举例
if(ferror(pf))
{
printf("文件读取发生错误而结束");
}
3.2 feof
feof函数的功能也是判断使用某一文件指针的过程中,是否读取到文件末尾,若使用时没有读取到文件末尾,则feof函数返回0;否则,feof函数将返回一个非零的值。调用feof函数时,也只需将待检查的文件指针传入即可。
函数使用举例
if (feof(pf))
{
printf("文件读取到文件末尾而结束");
}
3.3 实际中ferror和feof的配合使用
在实际使用中,读取文件结束后,要配合ferror和feof一起来使用,判断文件是正常结束、发生错误而结束还是读取到文件尾而结束。
void Testfgetc()
{
// 1、用"r"方式打开已经存在的文件,内容为:abcdef
FILE* pf = fopen("log.txt", "r");
// 2、使用fgetc依次读取文件的第一个字符
int ch = 0;
ch = getc(pf);
// 判断读取情况
if(ferror(pf))
{
printf("文件读取发生错误而结束");
}
else if(feof(pf))
{
printf("文件读取到文件末尾而结束");
}
else
{
printf("读取字符成功");
}
// 3、关闭文件
fclose(pf);
pf = NULL;
}
4. 文件的随机读写
前面我们介绍的顺序读写的函数,只能往后走不能往回走,这样使用起来不是很灵活,比如看下面这个例子:
文件log.txt已经存在,且内容为:abcdef
void Test()
{
FILE* pf = fopen("log.txt", "r");
// 读取第一个字符,位置指示器向后移动一位
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);// a
// 再次读取一个字符,位置指示器继续往后移动一位
ch = fgetc(pf);
printf("%c\n", ch);// b
fclose(pf);
pf = NULL;
}
读取了两个字符之后,位置指示器移到了字符c的位置,想要再次读取最开始的字符a,有什么办法能让位置指示器回到’a’位置呢?
4.1 fseek
int fseek ( FILE* stream, long int offset, int origin );
功能:将与字节流关联的位置指示器设置为新位置。
参数:
- stream:文件控制块地址。
- offset:相较于origin位置,想要偏移的量。
- origin:偏移前的初始位置(注意并非文件信息区的起始位置),C对origin定义了以下三个常量:
常量 | 含义 |
---|---|
SEEK_SET | 字节流第一个位置 |
SEEK_CUR | 字节流所在当前位置 |
SEEK_END | 字节流EOF的后一个位置(这个位置是无效的) |
返回值:成功返回0,出错返回非0值,并设置错误标识符(ferror)。
函数使用举例
对于开头的例子我们使用fseek有三种方法可以让位置指示器回到最开始的’a’位置。
1.方法一:相对起始位置开始偏移
fseek(pf, 0, SEEK_SET);
ch = fgetc(pf);
printf("%c\n", ch); //a
2.方法二:相对当前位置开始偏移
fseek(pf, -2, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch); //a
3.方法三:相对EOF后一个位置开始偏移
fseek(pf, -7, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch); //a
4.2 ftell
long int ftell ( FILE* stream );
获得字节流当前的位置相到起始位置的偏移量。只需传入文件控制块地址即可,调用成功返回偏离量,失败返回-1。
函数使用举例
依然是最开始的那个例子,我们读取了两个字符后来带’c’位置,距离最开始的位置偏移了两个字符。
long distance = ftell(pf);
printf("%ld\n", distance);//2
4.3 rewind
void rewind ( FILE* stream );
让位置指示器回到最开始的位置,只需传入文件控制块地址即可,无返回值。
函数使用举例
使用rwind让该文件的我只指示器回到最开始’a’的位置。
// 让位置指示器回到最开始位置,即'a'位置
rewind(pf);
// 直接读取就是'a'
ch = fgetc(pf);
printf("%c\n", ch);//a
fclose(pf);