一、为什么要使用文件
我们之前写的程序在运行起来的时候,我们可以给程序增加或删除数据,此时的数据都是存在内存中。当程序执行完毕退出的时候,之前程序中增减或减少的数据就不存在了,等程序下一个运行的时候,数据又会重新录入。
如果我们想把程序中的数据记录来,只有在我们选择删除的时候,数据才不复存在。这就涉及到数据持久化的问题。我们一般数据持久化的方法是把数据放在磁盘文件、存放到数据等方式。因此,我们可以使用文件的方式将数据直接存放在电脑的硬盘上,做到了数据的持久化。
二、什么是文件
文件(file)通常是磁盘或固态硬盘上的一段已命名的存储区。它是指一组相关数据的有序集合。这个数据集合有一个名称,叫做文件名。文件名 是文件的唯一标识,以便用户识别和引用。文件名包括 3 个部分:文件路径 + 文件名主干 + 文件后缀名。所有的文件都通过流进行输入、输出操作。C 把文件看作一系列连续的字节,每个字节都能被单独读取。与文本流和二进制流对应,文本可以分为 文本文件 和 二进制文件。
- 文本文件,也称 ASCII 文件。这种文件在保留的时,每一字符对应一个字节,用于存放对应的 ASCII 码。
- 二进制文件,不保存 ASCII 码,而是按二进制的编码方式来保存文件内容。
文件在程序中是以流的形式来操作的。流 是指数据源(文件)和程序(内存)之间的经历的路径。输入流 指的是数据从数据源(文件)到程序(内存)的路径。输出流 指的是数据从程序(内存)到数据源(文件)的路径。
当我们提到输入时,这意味着要向程序写入一些数据。输入可以是以文件的形式或命令行中进行。C 语言提供了一系列内置的函数来读取给定的输入,并根据需要写入到程序中。
当我们提到输出时,这意味着要在屏幕上、打印机上或任意文件中显示一些数据。C 语言提供了一些列内置的函数来输出数据到计算机屏幕上和保存数据到文本文件或二进制文件中。
C 语言把所有的设备都当作文件,所以设备被处理的方式与文件相同。以下三个文件会在程序执行时自动打开,以访问键盘或屏幕。
标准文件 | 文件指针 | 设备 |
---|---|---|
标准输入 | stdin | 键盘 |
标准输出 | stdout | 屏幕 |
标准错误 | stderr | 屏幕 |
三、文件的基本操作
文件的基本操作包括文件的打开和关闭,除了标准的输入、输出文件外,其它所有的文件都必须先打开再使用,而使用后也必须关闭该文件。
3.1、文件指针
文件指针并不指向实际的文件,它是一个指向文件有关信息的指针,这些信息包括文件名、状态 和 当前位置,它们保存在一个结构体变量中。在使用文件时需要在内存中为其分配空间,用来存放文件的基本信息。该结构体类型是由系统定义的,C 语言规定该类型为 FILE 型,其声明如下:
#ifndef _FILE_DEFINEDstruct _iobuf {char *_ptr;int _cnt;char *_base;int _flag;int _file;int _charbuf;int _bufsiz;char *_tmpfname;};typedef struct _iobuf FILE;
#define _FILE_DEFINED
#endif
3.2、文件的打开
fopen() 函数用来打开一个文件,打开文件的操作就是创建一个流。fopen() 函数的原型在 stdio.h 中,其函数原型如下:
FILE *fopen( const char *restrict filename, const char *restrict mode );
其中,filename 是指要被打开的文件的文件名,mode 是指对打开的文件要进行读操作还是写操作。如果使用 fopen() 函数打开文件成功,则返回一个有确定执行的 FILE 类型指针;若打开失败,则返回 NULL;
mode 的具体参数值如下:
模式 | 含义 | 说明 |
---|---|---|
r | 以只读模式打开文件,文件的指针将会放在文件的开头 | 文件必须存在 |
r+ | 打开文件后,可以读取文件内容,也可以写入新的内容覆盖原有内容(从文件头开始覆盖) | |
rb | 以二进制的格式打开文件,并且采用只读模式。文件的指针将会放在文件的开头,一般用于非文本文件 | |
rb+ | 以二进制的格式打开文件,并且采用读写模式。文件的指针将会放在文件的开头,一般用于非文本文件 | |
w | 以只写模式打开文件 | 文件存在,则将其覆盖,否则创建新的文件 |
w+ | 打开文件后,先清空所有内容,使其变为一个空的文件,对这个空文件有读写权限 | |
wb | 以二进制的格式打开文件,并且采用只写模式。文件的指针将会放在文件的开头,一般用于非文本文件 | |
wb+ | 以二进制的格式打开文件,并且采用读写模式。文件的指针将会放在文件的开头,一般用于非文本文件 | |
a | 以追加模式打开一个文件 | 如果文件已存在,文件指针放在文件的末尾(即新内容会被写入到已有内容之后),否则,创建新文件 |
a+ | 以读写模式打开文件 | |
ab | 以二进制的格式打开文件,并且采用追加模式。文件的指针将会放在文件的开头,一般用于非文本文件 | |
ab+ | 以二进制的格式打开文件,并且采用读写模式。文件的指针将会放在文件的开头,一般用于非文本文件 |
新的 C11 新增了带 x 字母的写模式,与以前的写模式相比具有更多的特性。第一,如果以传统的一种写模式打开一个现有文件,fopen() 会把该文件的长度截为 0,这样就丢失了该文件的内容。但是使用带 x 字母的写模式,即使 fopen() 操作失败,原文件的内容也不会被删除。第二,如果环境允许,x 模式的独占特性使得其它程序或线程无法访问正在被打开的文件。
通常打开文件失败的原因有以下几个方面:
- 指定的盘符或路径不存在;
- 文件名中含有无效字符;
- 以 r 模式打开一个不存在的文件;
如果使用任何一种 w模式(不带 x 字母)打开一个现有文件,该文件的内容会被删除,以便程序在一个空白文件中开始操作。然而,如果使用带 x 字母的任何一种模式,将无法打开一个现有文件。
3.3、文件的结尾
从文件中读取数据的程序在读到文件结束时要停止。如果 fgetc() 函数在读取一个字符时发现是文件结尾,它将返回一个特殊值 EOF。所以 C程序 只有在读到超过文件末尾时才会发现文件的结尾。
#define EOF (-1)
3.4、文件的关闭
文件在使用完毕后,应使用 fclose() 函数将其关闭,fclose() 函数 和 fopen() 函数一样,函数原型也在 stdio.h 中,其函数原型如下:
int fclose( FILE *stream );
fclose() 函数也返回一个值,当正常完成关闭操作时,fcolse() 函数返回值为 0,否则返回 EOF。fclose() 函数关闭 stream 指定的文件,必要时刷新缓冲区。
在程序结束之前应关闭所有文件,这样做的目的是防止因为没有关闭文件而造成的数据流失;
四、文件的读写操作
4.1、fputc()与fgetc()函数
fputc() 函数的作用是把一个字符写到磁盘文件(strem所指向的文件)中去。如果函数写入成功,则返回值就是写入的字符;如果写入失败,就返回 EOF。fputc() 函数的一般形式如下:
int fputc( int ch, FILE *stream );
其中,ch 是要写入的字符,它可以是一个字符常量,也可以是一个字符变量。stream 是文件指针变量。
fgetc() 函数的作用是从指定的文件(fp 指向的文件)读入一个字符赋给指定的变量。该文件必须必须是以只读或读写的方式打开。当文件遇到文件结束符将返回一个文本结束标志 EOF。fgetc() 函数的一般形式如下:
int fgetc( FILE *stream );
其中,strem 表示要读取的文件对象;
新建一个 main.c 文件,它的内容如下:
#include <stdio.h>int main(void)
{FILE *fp;char writeCh,readCh;if((fp = fopen("text.txt","w")) == NULL) // 以只写模式打开文件{printf("打开文件失败");return 0;}printf("请输入一个字符:");scanf("%c",&writeCh);if((fputc(writeCh,fp)) == EOF) // 写入一个字符{printf("写入失败!\n");}else{printf("写入成功!\n");}fclose(fp); // 关闭文件,保留写入的内容if((fp = fopen("text.txt","r")) == NULL) // 以只读模式打开文件{printf("打开文件失败");return 0;}readCh = fgetc(fp); // 从文件中读取一个字符printf("从文件中读取的字符为:%c\n",readCh);fclose(fp); // 关闭文件return 0;
}
这里,我们在终端中先使用 GCC 编译程序,然后运行生成的可执行程序。
gcc main-c -o main
./main
4.2、fputs()与fgets()函数
fputs() 函数与 fputc() 函数类似,区别在于 fputc() 函数每次指向文件写入一个字符,而 fputs() 函数每次向文件写入一个字符串,其中字符串可以是字符串常量,也可以是字符数组名、指针或变量。该函数不会字符串结尾的 '\0'写入。如果调用成功,返回一个非零值,否则返回 EOF。fputs() 函数的一般形式如下:
int fputs( const char *restrict str, FILE *restrict stream );
其中,str 表示要写入的字符串,stream 表示要写入的文件指针。
fgets() 函数与 fgetc() 函数类似,区别在于 fgetc() 函数每次从文件中读出一个字符,而 fgets() 函数每次从指定的文件中读取一个字符串到字节数组中。当读取到换行符('\n')或文件结束符(EOF)表示读取结束。如果成功则返回 str 指向的地址,如果没读入任何字符遇到 EOF,则 str 会指向的位置保留原有的内容,函数返回 NULL。。fgets() 函数的一般形式如下:
char *fgets( char *restrict str, int count, FILE *restrict stream );
其中,str 用来存放从文件中读取的字符串,count 用来指定读取的字节数(包括 '\0'),strem 表示要读取的文件对象。
#include <stdio.h>
#include <string.h>int main(void)
{FILE *fp;char wiriteStr[100], readStr[100];if((fp = fopen("text.txt","w")) == NULL) // 以只写模式打开文件{printf("打开文件失败");return 0;}printf("请输入一个字符串:\n");scanf("%s",writeStr); if((fputs(wiriteStr,fp)) == EOF) // 写入一个字符串,不能写入中文{printf("写入失败!\n");}else{printf("写入成功!\n");}fclose(fp); // 关闭文件,保留写入的内容if((fp = fopen("text.txt","r")) == NULL) // 以只读模式打开文件{printf("打开文件失败");return 0;}fgets(readStr,strlen(wiriteStr)+1,fp); // 从文件中读取一个字符串printf("从文件读取的字符串为:\n");printf("%s\n",readStr);fclose(fp); // 关闭文件return 0;
}
4.3、fprintf()与fscanf()函数
fprintf() 函数与 printf() 函数类似,它们之间最大区别在于 fprintf() 函数读写的对象不是终端,而是文件。fprintf() 函数的一般形式如下:
int fprintf( FILE *restrict stream, const char *restrict format, ... );
其中,strem 表示要操作的文件对象,format 是格式化字符串,与 printf() 函数类似,由格式化占位符和普通字符组成。格式化占位符以 % 开头,用于指明参数值如何格式化。该函数返回成功匹配的参数的个数。
fscanf() 函数与 scanf() 函数类似,它们之间最大区别在于 fscanf() 函数读写的对象不是终端,而是文件。fscanf() 函数的一般形式如下:
int fscanf( FILE *restrict stream, const char *restrict format, ... );
其中,strem 表示要操作的文件对象,format 是格式化字符串,与 scanf() 函数类似,由格式化占位符和普通字符组成。格式化占位符以 % 开头,用于指明参数值如何格式化。该函数返回成功匹配的参数的个数。
#include <stdio.h>int main(void)
{FILE *fp;char writeStr[100], readStr[100];if((fp = fopen("text.txt","w")) == NULL) // 以只写模式打开文件{printf("打开文件失败");return 0;}printf("请输入一个字符串:\n");scanf("%s",writeStr); // 写入一个字符串,不能写入中文,字符串不能有空格fprintf(fp,"%s",writeStr);fclose(fp); // 关闭文件,保留写入的内容if((fp = fopen("text.txt","r")) == NULL) // 以只读模式打开文件{printf("打开文件失败");return 0;}fscanf(fp,"%s",readStr); // 从文件中读取一个字符串printf("从文件中读取的字符为:\n");printf("%s\n",readStr);fclose(fp); // 关闭文件return 0;
}
由于 fgets() 保留了换行符,fputs() 就不会再添加换行符;
4.4、fwrite()与fread()函数
fwrite() 函数的作用是将 buffer 所指向的内容输出 count 次,每次写 size 字节到 stream 所指向的文件中。返回值是实际写入文件中的元素个数。fwrite() 函数的一般格式如下:
size_t fwrite( const void *restrict buffer, size_t size, size_t count, FILE *restrict stream );
其中,buffer 是一个指针,要写入文件的数据的地址,size 表示要写的字节数,count 表示要写多少个 size 字节的数据相,fp 表示要写入的文件。
fread() 函数的作用是从 stream 所指向的文件中读入 count 次,每次读 size 字节,读入的信息保存在 buffer 地址中。返回值是实际读取到的元素个数。fread() 函数的一般形式如下:
size_t fread( void *restrict buffer, size_t size, size_t count, FILE *restrict stream );
其中,buffer 是一个指针,要读入文件的数据的地址,size 表示要读的字节数,count 表示要读多少个 size 字节的数据相,stream 表示要写读的文件。
#include <stdio.h>
#include <string.h>int main(void)
{FILE *fp;char writeStr[100],readStr[100];if((fp = fopen("text.txt","w")) == NULL) // 以只写模式打开文件{printf("打开文件失败");return 0;}printf("请输入一个字符串:\n");scanf("%s",writeStr); fwrite(writeStr,sizeof(char),strlen(writeStr)+1,fp); // 连续写入一个字符到文件fclose(fp); // 关闭文件,保留写入的内容if((fp = fopen("text.txt","r")) == NULL) // 以只读模式打开文件{printf("打开文件失败");return 0;}fread(readStr,sizeof(char),strlen(writeStr)+1,fp); // 从文件中连续读取一个字符printf("从文件中读取的内容为:\n");printf("%s\n",readStr);fclose(fp); // 关闭文件return 0;
}
五、标准I/O的机理
通常使用 标准I/O 的第 1 步是调用 fopen() 打开文件。fopen() 函数不仅打开一个文件,还创建了缓冲区(在读写模式下会创建两个缓冲区)以及一个包含文件和缓冲区数据的结构。另外,fopen() 函数返回一个指向该结构的指针,以便其它函数知道如何找到该结构。假设把给指针赋值为一个指针变量 fp,我们说 fopen() 函数 “打开一个流”。如果以文本模式打开该文件,就获得一个文本流;如果以二进制模式打开该文件,就获得一个二进制流。
这个指针变量 fp 指向的结构包含一个指定流中当前位置的文件位置指示器。除此之外,它还包括错误和文件结尾的指示器、一个指向缓冲区开始初处的指针、一个文件标识符和一个计数(统计实际拷贝进缓冲区的字节数)。
我们主要考虑文件输入。通常,使用 标准I/O 的第 2 步是调用一个定义在 stdio.h 中的输入函数,如 fscanf()函数、getc()函数、fgetc()函数、fgets()函数。一调用这些函数,文件中缓冲区大小数据块就被拷贝到缓冲区中。缓冲区的大小因实现而异,一般是 512字节 或是它的倍数。最初调用函数,除了填充缓冲区后外,还要设置 fp 所指向的结构中的值。尤其是要设置流中当前位置和拷贝进缓冲区的字节数。通常,当前位置从 字节0 开始。
在初始化结构和缓冲以后,输入函数按要求从缓冲区中读取数据。在它读取数据是,文件位置指示器被设置为指向刚读取字节的下一个字符。由于 stdio.h 系列的所有输入函数都使用相同的缓冲区,所以调用任何一个函数都将从上一次函数停止调用的位置开始。
当输入函数发现以读取缓冲区的所有的字符时,会请求把下一个缓冲大小的数据块从文件拷贝到该缓冲区中。以这种方式,输入函数可以读取文件中的所有内容,直到文件结尾。函数在读取缓冲区中最后一个字符后,把结尾指示器设置为真。于是,下一次被调用的输入函数将返回 EOF。
输出函数以类似的方式把数据写入缓冲区。当缓冲区被填满时,数据将被拷贝至文件中。
六、文件的定位
6.1、fseek()函数
fseek() 函数的作用时移动文件内部位置指针,fseek() 函数的一般形式如下:
int fseek( FILE *stream, long offset, int origin );
其中,stream 指定要操作的文件对象。offset 表示要移动的字节数,要求位移量是 long 型数据,以文件长度大于 64KB 时不会出错。当用常量表示位移量时,要求加后缀 L。origin 表示从何处开始计算位移量,规定的起始点有 文件首、文件当前位置 和 文件尾。
起始点 | 表示符号 | 数字表示 |
---|---|---|
文件首 | SEEK_SEK | 0 |
文件当前位置 | SEEK_CUR | 1 |
文件尾 | SEEK_END | 2 |
#include <stdio.h>
#include <string.h>int main(void)
{FILE *fp;char writeStr[100],readStr[100];if((fp = fopen("text.txt","w")) == NULL) // 以只写模式打开文件{printf("打开文件失败");return 0;}printf("请输入一个字符串:\n");scanf("%s",writeStr);if((fputs(writeStr,fp)) == EOF) // 写入一个字符串到文件中,不能写入中文{printf("写入失败!\n");}else{printf("写入成功!\n");}fclose(fp); // 关闭文件,保留写入的内容if((fp = fopen("text.txt","r")) == NULL) // 以只读模式打开文件{printf("打开文件失败");return 0;}fseek(fp,5L,0); // 文件指针偏移5fgets(readStr,strlen(writeStr)+1,fp); // 从文件中读取一个字符串printf("从文件读取的字符串为:\n");printf("%s\n",readStr);fclose(fp); // 关闭文件return 0;
}
6.2、rewind()函数
rewind() 函数的作用是使文件位置指针从新返回文件的开头,该函数没有返回值。rewind() 函数的一般格式如下:
void rewind( FILE *stream );
其中,stream 指要操作的文件对象。
#include <stdio.h>
#include <string.h>int main(void)
{FILE *fp;char writeStr[100],readStr[100];if((fp = fopen("text.txt","w")) == NULL) // 以只写模式打开文件{printf("打开文件失败");return 0;}printf("请输入一个字符串:\n");scanf("%s",writeStr); if((fputs(writeStr,fp)) == EOF) // 写入一个字符串到文件中,不能写入中文{printf("写入失败!\n");}else{printf("写入成功!\n");}fclose(fp); // 关闭文件,保留写入的内容if((fp = fopen("text.txt","r")) == NULL) // 以只读模式打开文件{printf("打开文件失败");return 0;}fseek(fp,5L,0); // 文件指针偏移5fgets(readStr,strlen(writeStr)+1,fp); // 从文件中读取一个字符串printf("从文件读取的字符串为:\n");printf("%s\n",readStr);rewind(fp); // 文件指针回到头部fgets(readStr,strlen(writeStr)+1,fp); // 从文件中读取一个字符串printf("从文件读取的字符串为:\n");printf("%s\n",readStr);fclose(fp); // 关闭文件return 0;
}
6.3、ftell()函数
ftell() 函数的作用是得到流式文件中的当前位置,用相对于文件开头的偏移量来表示。当 ftell() 函数返回 -1L 时,表示出错。ftell() 函数的一般形式如下:
long ftell( FILE *stream );
其中,stream 表示要操作的文件。
#include <stdio.h>
#include <string.h>int main(void)
{FILE *fp;char str[100];long position;if((fp = fopen("text.txt","w")) == NULL) // 以只写模式打开文件{printf("打开文件失败");return 0;}printf("请输入一个字符串:\n");scanf("%s", str); if((fputs(str,fp)) == EOF) // 写入一个字符串到文件中,不能写入中文{printf("写入失败!\n");}else{printf("写入成功!\n");}fclose(fp); // 关闭文件,保留写入的内容if((fp = fopen("text.txt","w")) == NULL) // 以只读模式打开文件{printf("打开文件失败");return 0;}fseek(fp,5L,0); // 文件指针偏移5position = ftell(fp);printf("文件指针当前位置为:%ld\n",position);fclose(fp); // 关闭文件return 0;
}
6.4、fgetpos()函数与fsetpos()函数
fseek() 函数 与 ftell() 函数 潜在的问题时,它们都把文件大小限制在 long 类型能表示的范围内。ANSI C 新增了两个处理较大文件的新定位函数:fgetpos() 函数 和 fsetpos() 函数。这两个函数不使用 long 类型的值表示位置,它们使用一种新的类型:fpos_t(代表 file position type,文件定位类型)。fpos_t 类型不是基本类型,它根据其它类型来定义。fpos_t 类型的变量或数据对象可以在文件中指定一个位置,它不能是数组类型。
fgetpos() 函数的原型如下:
int fgetpos( FILE *restrict stream, fpos_t *restrict pos );
调用该函数时,它把 fpos_t 类型的值放在 pos 指向的位置上,该值描述了文件中的当前位置距开头的字节数。如果成功,fgetpos() 函数返回 0;如果失败,返回非 0。
fsetpos() 函数的原型如下:
int fsetpos( FILE *stream, const fpos_t *pos );
调用该函数时,使用 pos 指向位置上的 fpos_t 类型值来设置文件指针指向偏移该值后指定的位置。如果成功,fsetpos() 函数返回 0。如果失败,则返回非 0。fpos_t 类型的值可以通过之前调用 fgetpos() 函数获得。
#include <stdio.h>
#include <string.h>int main(void)
{FILE *fp;char str[100];fpos_t position;if((fp = fopen("text.txt","w")) == NULL) // 以只写模式打开文件{printf("打开文件失败");return 0;}printf("请输入一个字符串:\n");scanf("%s", str); if((fputs(str,fp)) == EOF) // 写入一个字符串到文件中,不能写入中文{printf("写入失败!\n");}else{printf("写入成功!\n");}fclose(fp); // 关闭文件,保留写入的内容if((fp = fopen("text.txt","w")) == NULL) // 以只读模式打开文件{printf("打开文件失败");return 0;}fgetpos(fp,&position); // 文件指针偏移5printf("文件指针当前位置为:%ld\n",position.__pos);position.__pos = 5;fsetpos(fp,&position);printf("文件指针当前位置为:%ld\n",position.__pos);fclose(fp); // 关闭文件return 0;
}
6.5、feof()函数与ferror()函数
如果标准输入函数返回 EOF,则通常表明函数已经到达文件结尾。然而,出现读取错误时,函数也会返回 EOF。feof() 函数与 ferror() 函数用于区分这两种情况。
feof() 函数的原型如下:
int feof( FILE *stream );
当上一次输入调用检测到文件末尾时,feof() 函数返回一个非零值,否则返回 0。
ferror() 函数的原型如下:
int ferror( FILE *stream );
当读或写出现错误,ferror() 函数返回一个非零值,否则返回 0。
#include <stdio.h>int main(void)
{FILE *fp;char ch;char writeStr[100];if((fp = fopen("text.txt","w")) == NULL) // 以只写模式打开文件{printf("打开文件失败");return 0;}printf("请输入一个字符串:\n");scanf("%s",writeStr); if((fputs(writeStr,fp)) == EOF) // 写入一个字符串,不能写入中文{printf("写入失败!\n");}else{printf("写入成功!\n");}fclose(fp); // 关闭文件,保留写入的内容if((fp = fopen("text.txt","r")) == NULL) // 以只读模式打开文件{printf("打开文件失败\n");return 0;}while((ch = fgetc(fp)) != EOF){putchar(ch);}putchar('\n');// 判断是什么原因结束的if(ferror(fp)){printf("文件读取错误!\n");}else if(feof(fp)){printf("文件读取到末尾!\n");}putchar('\n');fclose(fp); // 关闭文件return 0;
}
在文件读取过程中,不能用 feof() 函数的返回值直接用来判断文件是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束;
文本文件读取结束,判断返回值是否为 EOF(fgetc())或者 NULL(fgets());
二进制文件的读取结束判断,判断返回值是否小于实际要读的个数(fread());
七、文件缓冲区
7.1、什么是文件缓冲区
ANSI C 标准采用 “缓冲文件系统” 处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用大的文件开辟一个 “文件缓冲区”。从内存向磁盘输出数据会发送在内存中的缓冲区,装满缓冲区后才一起发送到磁盘上。如果从磁盘向加计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据发送程序数据区(程序变量等)。缓冲区地大小根据 C 编译系统决定的。
7.2、setvbuf()函数
setvbuf() 函数的原型如下:
int setvbuf( FILE *restrict stream, char *restrict buffer, int mode, size_t size );
setvbuf() 函数创建了一个供 标准 I/O 函数 替换使用的缓冲区。在打开文件后且未对流进行其它操作之前,调用该函数。指针 stream 识别待处理的流,buffer 指向待使用的存储区。如果 buffer 的值不是 NULL,则必须创建一个缓冲区。变量 size 告诉 setvbuf() 函数数组的大小(size_t 是一种派生的整数类型)。mode 的选择如下:_IOFBF
表示 完全缓冲(在缓冲区满时刷新);_IOLBF
表示 行缓冲(在缓冲区满时或写入一个换行符时);_IONBF
表示 无缓冲。如果操作成功,函数返回 0,否则返回一个非零值。
7.3、fflush()函数
fflush() 函数的原型如下:
int fflush( FILE *stream );
调用 fflush() 函数引起输出缓冲区中所有的未写入数据发送到 stream 指定的输出文件。这个过程称为 刷新缓冲区。如果 stream 是空指针,所有输出缓冲区都被刷新。在输入流中使用 fflush() 函数的效果是未定义的。只要最近一次操作不是输入操作,就可以用该函数来更新流(任何读写模式)。
#include <stdio.h>
#include <unistd.h>int main(void)
{FILE * fp = fopen("text.txt", "w");fputs("hello world!", fp); // 先将代码放在缓冲区中printf("睡眠10秒-已经写数据,打开test.txt文件,发现文件没有内容\n");sleep(10);// fflush()函数在高版本的VS上不能使用fflush(fp); // 刷新缓冲区,才将输入缓冲区内容过的数据写到文件(磁盘)printf("再睡眠10秒,再次打开test.txt文件,发现文件有内容\n");sleep(10);printf("关闭文件!\n");fclose(fp); // fclose()函数再关闭文件时,也会刷新缓冲区return 0;
}
因为有缓冲区的存在,C 语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件问题,丢失数据。
八、文件的系统调用
系统调用是操作系统内核提供给应用程序,供其可以间接访问硬件资源的接口,关于操作系统内核、应用程序等概念。
我们可以通过 open()
系统调用用于打开一个标准的文件描述符。
/*** @brief 打开一个标准的文件描述符* * @param __path 文件路径* @param __oflag 用于指定打开文件的方式* @param ... __oflag参数使用O_CREAT创建文件时的权限* @return __fortify_function 成功返回非负的文件描述符,失败返回-1,并设置全局变量errno以指示错误原因*/
__fortify_function int open (const char *__path, int __oflag, ...);
其中,参数 __oflag
用来指定文件的打开方式,可以是以下选项的组合。
O_RDONLY
:以只读方式打开文件。O_WRONLY
:以只写方式打开文件。O_RDWR
:以读写方式打开文件。O_CREAT
:如果文件不存在,则创建一个新文件。O_APPEND
:将所有写入操作追加到文件的末尾。O_TRUNC
:如果文件存在并且以写入模式打开,则截断文件长度为 0。O_EXCL
:当与 O_CREAT 一起使用时,只有当文件不存在时才创建新文件。O_SYNC
:同步 I/O。O_NONBLOCK
:非阻塞 I/O。
Linux 系统有文件权限的保护,默认创建的文件会删除其它用户的写权限。
打开文件之后,我们可以使用 write()
系统调用来写入文件。
/*** @brief 向文件描述符所指向的文件写入数据* * @param __fd 文件描述符* @param __buf 指向要写入的数据的缓冲区* @param __n 要写入数据的字节数* @return ssize_t 成功返回实际写入的字节数,失败返回-1*/
extern ssize_t write (int __fd, const void *__buf, size_t __n) __wur __attr_access ((__read_only__, 2, 3));
然后,我们还可以通过 read()
函数读取文件的内容。
/*** @brief 从文件描述符所指向的文件读取内容* * @param __fd 文件描述符* @param __buf 指向读取数据的缓冲区* @param __nbytes 表示读取的最大字节数* @return __fortify_function 成功返回成功读取的字节数,失败返回-1*/
__fortify_function __wur ssize_t read (int __fd, void *__buf, size_t __nbytes);
最后,我们还需要调用 close()
函数关闭一个文件描述符。
/*** @brief 关闭文件描述符* * @param __fd 文件描述符* @return int 成功返回0,失败返回-1*/
int close (int __fd);
在 Linux 中,当我们打开或者创建一个文件(或套接字)时,操作系统会提供一个文件描述符(File Descriptor,FD)。文件描述符是一个非负数,我们可以通过它来进行读写等操作。文件描述符本身只是操作系统为应用程序操作底层资源(如文件、套接字等)所提供的一个引用或句柄。
在 Linux 中,文件描述符 0、1、2 是有特别含义的。
0
:标准输入(stdin)的文件描述符。1
:标准输出(stdout)的文件描述符。2
:标准错误(stderr)的文件描述符。
在 Linux 中,每个文件描述符都关联到内核一个 struct file
类型的结构体数据,该结构体的关键字段如下:
struct file {...atomic_long_t f_count; // 引用计数,管理文件对象的生命周期struct mutex f_pos_lock; // 保护文件位置的互斥锁loff_t f_pos; // 当前文件位置(读写位置)...struct path f_path; // 记录文件路径struct inode *f_inode; // 指向与文件相关联的inode对象指针,该对象用于维护文件元数据,如文件类型、访问权限等const struct file_operations *f_op; // 指向文件操作函数表的指针,定义了文件支持的操作,如读、写、锁定等...void *private_data; // 存储特定驱动或模块的私有数据...
};
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>int main(void)
{char write_data[] = "hello world!";char read_data[128] = {0};int fd = 0;int length = 0;// 以只读模式打开文件描述符,如果文件不存在则创建if ((fd = open("text.txt", O_WRONLY | O_CREAT, 0664)) == -1){perror("open file failed!");exit(EXIT_FAILURE);}// 向文件描述符所指向的文件中写入数据if (write(fd, write_data, strlen(write_data)) < 0){perror("write data failed!");exit(EXIT_FAILURE);}// 关闭文件描述符if (close(fd) < 0){perror("close file failed!");exit(EXIT_FAILURE);}// 以只读模式打开文件描述符if ((fd = open("text.txt", O_RDONLY)) == -1){perror("open file failed!");exit(EXIT_FAILURE);}// 从文件描述符所指向的文件读取数据if ((length = read(fd, read_data, sizeof(read_data))) < 0){perror("read data failed!");exit(EXIT_FAILURE);}// 将读取的数据写入到终端中read_data[length++] = '\n';read_data[length] = '\0';write(STDOUT_FILENO, read_data, length);// 关闭文件描述符if (close(fd) < 0){perror("close file failed!");exit(EXIT_FAILURE);}return 0;
}