目录
一.何为文件
二.文件名
三.文件的打开和关闭
3.1.流
3.2.文件指针
3.3.文件的打开与关闭
打开文件:
模式:
关闭文件:
四.文件的顺序读写
4.1.常见的顺序读写函数
4.2.字符的输入输出fgetc/fputc
输出函数:
输入函数:
4.3.行的输入输出fputs/fgets
输出函数:
输入函数:
4.4. 格式化的输入输出fscanf/fprintf
输出函数:
输入函数:
4.5.字符串的输入输出sscanf/sprintf
输出函数:
输入函数:
4.6.块的输入输出fread/fwrite
输出函数:
输入函数:
4.7.对比一组函数
五.文件的随机读写
5.1.fseek
5.2.ftell
5.3.rewind
六.文件类型
七.文件结束判断
八.文件缓冲区
一.何为文件
在程序设计中,我们一般谈的文件有两种:程序文件和数据文件。
程序文件:
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
数据文件:
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
二.文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。文件名包含3部分:文件路径+文件名主干+文件后缀。例如:c:\code\test.text。文件方便起见,文件标识常被称为文件名。
三.文件的打开和关闭
3.1.流
在C语言中,术语流(stream)表示任意输入的源或任意输出的目的地。许多小型程序都是通过一个流(通常和键盘相关)获得全部的输入,并且通过另一个流(通常和屏幕相关)写出全部的输出。
流是一个高度抽象的概念。C程序只要运行起来,就会默认打开三个流:
- stdin:标准输入流 - 键盘
- stdout:标准输出流 - 屏幕
- stderr:标准错误流 - 屏幕
3.2.文件指针
C语言中对流的访问是通过文件指针(file pointer)实现的。此指针的类型为FILE*(FILE类型在<stdio.h>中声明)。用文件指针表示的特定流具有标准的名字;如果需要,还可以声明另外一些文件指针。例如:如果程序除了标准流之外还需要两个流,则可以包含如下声明:FILE* fp1,*fp2; 虽然操作系统通常会限制可以同时打开的流的数量,但程序可以声明任意数量的FILE*类型变量。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名为FILE。
例如,VS2019编译环境提供的stdio.h头文件中有以下的文件类型申明:
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。下面我们可以创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
3.3.文件的打开与关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。在编程程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
打开文件:
如果要把文件用作流,打开文件时需要调用fopen函数。
FILE* fopen(const char* filename, const char* mode);
第一个参数是含有要打开文件名的字符串。(“文件名”可能包含关于文件位置的信息,如驱动器符或路径。)第二个参数是“模式字符串”,它用来指定打算对文件执行的操作。例如,字符串“r”表明将从文件读入数据,但是不会向文件写入数据。
fopen函数返回一个文件指针。程序可以(且通常将)把此指针存储在一个变量中,稍后在需要对文件进行操作时使用它。当无法打开文件时,fopen函数会返回空指针。这可能是因为文件不存在,也可能是因为文件的位置不对,还可能是因为我们没有打开文件的权限。
注意:
1.在fopen函数调用的文件名中含有字符 \ 时,一定要小心。因为C语言会把字符 \ 看成是转义序列的开始标志。
fopen("c:\project\test1.dat","r");
这个调用会失败,因为编译器会把 \t 看成是转义字符。有两种方法可以避免这一问题。一种方法是用 \\ 代替 \ :
fopen("c:\\project\\test1.dat","r");
另一种方法更简单--只要用 / 代替 \ 就可以了:
fopen("c:/project/test1.dat","r");
2.永远不要假设可以打开文件,每次都要测试fopen函数的返回值以确保不是空指针。如果程序不检查错误,这个NULL指针就会传给后续的I/O函数。它们将对这个指针执行间接访问,并将失败。
模式:
给fopen函数传递哪种模式字符串不仅依赖于稍后将要对文件采取的操作,还取决于文件中的数据是文本形式还是二进制形式。
打开文本文件:
字符串 | 含义 |
"r" | 打开文件用于读 |
"w" | 打开文件用于写(文件不需要存在) |
"a" | 打开文件用于追加(文件不需要存在) |
"r+" | 打开文件用于读和写(从文件头开始) |
"w+" | 打开文件用于读和写(如果文件存在就截去) |
"a+" | 打开文件用于读和写(如果文件存在就追加) |
打开二进制文件:
注意:打开二进制文件时,需要在模式字符串中包含字母b:
字符串 | 含义 |
"rb" | 打开文件用于读 |
"wb" | 打开文件用于写(文件不需要存在) |
"ab" | 打开文件用于追加(文件不需要存在) |
"r+b"或者"rb+" | 打开文件用于读和写(从文件头开始) |
"w+b"或者"wb+" | 打开文件用于读和写(如果文件存在就截去) |
"a+b"或者“ab+” | 打开文件用于读和写(如果文件存在就追加) |
如果一个文件打开是用于读取的,那么它必须是原先已经存在的。但是,如果一个打开是用于写入的,如果它原先已经存在,那么它原来的内容就会被删除。如果它原先不存在,那么就创建一个新文件。如果一个打开用于添加的文件原先并不存在,那么它将被创建。如果它原先已经存在,它原先的内容并不会被删除。无论在哪一种情况下,数据只能从文件的尾部写入。
在mode中添加“a+”表示该文件打开用于更新,并且流既允许读也允许写。但是,如果你已经从该文件读入了一些数据,那么在你开始向它写入数据之前,你必须调用其中一个文件定位函数(fseek,fsetpos,rewind)。在你向文件写入一些数据之后,如果你又想从该文件读取一些数据,你首先必须调用fflush函数或者文件定位函数之一。
关闭文件:
流是用函数fclose关闭的,它的原型如下:
int fclose(FILE* stream);
对于输出流,fclose函数在文件关闭之前刷新缓冲区。如果它执行成功,fclose返回零值,否则返回错误代码EOF。
案例:
int main()
{
//打开文件
FILE* pf= fopen("test.txt","w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读写文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
四.文件的顺序读写
4.1.常见的顺序读写函数
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输入流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输入流 |
二进制输入 | fread | 文件 |
二进制输入 | fwrite | 文件 |
4.2.字符的输入输出fgetc/fputc
本节中,我们将讨论用于读和写单个字符的库函数。这些函数可以处理文本流和二进制流。请注意,本节中的函数把字符作为int型而非char类型的值来处理。这样做的原因之一就是输入函数是通过返回EOF来说明文件末尾(或错误)情况的,而EOF又是一个负的整数常量。
输出函数:
int fputc(int c,FILE* stream);
int putc(int c,FILE* stream);
int putchar(int c);
putchar函数向标准输出流stdout写一个字符,而fputc函数和putc函数是putchar函数向任意流写字符的更通用的版本。虽然putc函数和fputc函数做的工作相同,但是putc通常作为宏来实现(也有函数实现),而fputc函数则只作为函数实现。putchar本身通常也定义为宏:
#define putchar(c) putc((c),stdout)
如果出现了写错误,那么上述这3个函数都会为流设置错误指示器并且返回EOF。否则,它们都会返回写入的字符。
案例:
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
//fputc:字符输出函数
//int fputc(int char, FILE* stream);
//把参数char指定的字符(一个无符号字符)写入到指定的流stream中,并把位置标识符往前移动
//如果没有发生错误,则返回被写入的字符。如果发生错误,则返回 EOF,并设置错误标识符
//fputc('a', pf);
//fputc('b', pf);
//fputc('c', pf);
//fputc('d', pf);
//fputc('e', pf);
//fputc('f', pf);
//fputc('g', pf);
char ch = 'a';
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
输入函数:
int fgetc(FILE* stream);
int getc(FILE* stream);
int getchar(void);
getchar函数从标准输入流stdin中读入一个字符,而fgetc函数和getc函数从任意流中读入一个字符。这三个函数都把字符看成unsigned char类型的值(返回之前转换成int类型)。因此,它们不会返回EOF之外的负值。getc和fgetc之间的关系类似于putc和fputc之间的关系。getc通常作为宏来实现(也有函数实现),而fgetc则只作为函数实现。getchar本身通常也定义为宏:
#define getchar() getc(stdin)
如果遇到了文件末尾,那么这三个函数都会设置流的文本末尾指示器,并且返回EOF。如果产生了读错误,它们则都会设置流的错误指示器,并且返回EOF。为了区分这两种情况,可以调用feof函数或者ferror函数。
注意:
始终要把fgetc,getc或getchar函数的返回值存储在int型的变量中,而不是char类型的变量中。否则把char类型变量与EOF进行比较可能会得到错误的结果。
案例:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
//fgetc:字符输入函数
//int fgetc(FILE* stream);
//从文件指针stream指向的文件中读取一个字符,读取一个字节后,光标位置后移一个字节
//如果读到文件末尾或者读取出错时返回EOF
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ",ch);
}
printf("\n");
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
4.3.行的输入输出fputs/fgets
本节将要介绍读和写行的库函数。虽然这些函数也可有效地用于二进制文本流,但是它们多用于文本流。
输出函数:
int fputs(const char* s,FILE* stream);
int puts(const char* s);
puts函数是用来向标准输出流stdout写入字符串的,在写入字符串中的字符以后,puts函数总会添加一个换行符。fputs函数是puts函数的更通用版本。此函数的第二个实参指明了输出要写入的流。不同于puts函数,fputs函数不会自己写入换行符,除非字符串中本身含有换行符。
当出现写错误时,上面这两种函数都会返回EOF。否则,它们都会返回一个非负的数。
案例:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件:写一行
//fputs():文本行输出函数
//int fputs(const char* str, FILE* stream);
//把字符串写入到指定的流stream中,但不包括空字符
//该函数返回一个非负值,如果发生错误则返回EOF
fputs("qwertyuiop\n", pf);
fputs("xxxxxxxxxx\n", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
输入函数:
char* fgets(char* s,int n,FILE* stream);
char* gets(char* s);
gets函数用来从标准输入流stdin中读取一行的:gets(str)。gets函数逐个读取字符,并且把它们存储在str所指向的数组中,直到它读到换行符时停止(丢弃换行符)。fgets函数是gets函数的更通用版本,它可以从任意流中读取信息。fgets函数也比gets函数更安全,因为它会限制将要存储的字符的数量。下面是使用fgets函数的方法,假设str是字符数组的名字:fgets(str,sizeof(str),fp);此调用将导致fgets函数逐个读入字符,直到遇到首个换行符时或者已经读入了sizeof(str)-1个字符时结束操作,这两种情况哪种先发生都可以。如果fgets函数读入了换行符,那么它会把换行符和其他字符一起存储。(因此,gets函数从来不存储换行符,而fgets函数有时会存储换行符。)
如果出现了读错误,或者是在存储任何字符之前达到了输入流的末尾,那么gets函数和fgets函数都会返回空指针。否则,两个函数都会返回自己的第一个实参(指向保存输入的数组的指针)。与预期一样,两个函数都会在字符串的末尾存储空字符。
案例一:
int main()
{
char arr[256] = { 0 };
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件:读一行
//fgets():文本行输入函数
//char* fgets(char* str, int n, FILE* stream);
//从指定的流stream读取一行,并把它存储在str所指向的字符串内。当读取(n-1)个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定
//如果成功,该函数返回相同的str参数。如果到达文件末尾或者没有读取到任何字符,str的内容保持不变,并返回一个空指针
//如果发生错误,返回一个空指针。
//fgets(arr, 255, pf);
//printf("%s",arr);
//fgets(arr, 255, pf);
//printf("%s", arr);
while (fgets(arr, 256, pf) != NULL)
{
printf("%s",arr);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
案例二:
int main()
{
char arr[256] = "xxxxx";
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fgets(arr, 4, pf);
printf("%s", arr);
fgets(arr, 4, pf);
printf("%s", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
调试分析:
结论:
当读取(n-1)个字符时,它会停止,因为我们需要在字符串末尾添加 \0 作为字符串的结束标志。
4.4. 格式化的输入输出fscanf/fprintf
本节中,我们将介绍使用格式串来控制读/写的库函数。
输出函数:
int fprintf(FILE* stream,const char* format,...);
int printf(const char* format,...);
fprintf函数和printf函数向输出流中写入可变数量的数据项,并且利用格式串来控制输出的形式。这两个函数的原型都是以...符号结尾的,表面后面还有可变数量的实际参数。这两个函数的返回值是写入的字符数,若出错则返回一个负值。
fprintf函数和printf函数唯一的不同就是printf函数始终向stdout(标准输出流)写入内容,而fprintf函数则向它自己的第一个实际参数指定的流中写入内容。printf函数的调用等价于fprintf函数把stdout作为第一个实际参数二进行的调用。
案例:
struct S
{
char name[20];
int age;
double d;
};
int main()
{
struct S s = { "李四",20,95.5 };
//打开文件
FILE* pf = fopen("test2.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
//fprintf:格式化输出函数
//int fprintf(FILE* stream, const char* format, ...)
//发送格式化输出到流stream中
//如果成功,则返回写入的字符总数,否则返回一个负数
fprintf(pf, "%s %d %lf", s.name, s.age, s.d);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
输入函数:
int fscanf(FILE* stream,const char* format,...);
int scanf(const char* format,...);
fscanf函数和scanf函数从输入流读入数据,并且使用格式串来指明输入的格式。格式串的后边可以有任意数量的指针(每个指针指向一个对象)作为额外的实际参数。输入的数据项(根据格式串中的转换说明)进行转换并且存储在指针指向的对象中。scanf函数始终从标准输入流stdin中读入内容,而fscanf函数则从它的第一个参数所指定的流中读入内容。scanf函数的调用等价于以stdin作为第一个实际参数的fscanf函数调用。
如果发生输入错误(即没有输入字符可以读)或者匹配失败(即输入字符和格式串不匹配),那么fscanf函数会提前返回。这两个函数都会返回读入并且赋值给对象的数据项的数量。如果在读取任何数据项之前发生输入失败,那么会返回EOF。
案例:
int main()
{
struct S s = { 0 };
//打开文件
FILE* pf = fopen("test2.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
//fsanf:格式化输入函数
//int fscanf(FILE* stream, const char* format, ...)
//从流stream读取格式化输入
//如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回EOF
//读文件
fscanf(pf, "%s %d %lf", s.name, &(s.age), &(s.d));
//打印
printf("%s %d %lf\n", s.name, s.age, s.d);
//fprintf(stdout, "%s %d %lf\n", s.name, s.age, s.d);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
4.5.字符串的输入输出sscanf/sprintf
本节里描述的函数有一点不同,因为它们与数据流或文件并没有什么关系。相反,它们允许我们使用使用字符串作为流读写数据。
输出函数:
int sprintf(char* s,const char* format,...);
sprintf函数类似于printf函数和fprintf函数,唯一的不同就是sprintf函数把输出写入(第一个实参指向的)字符数组而不是流中。sprintf函数的第二个参数是格式串,这与printf函数和fprintf函数所用的一样。
sprintf函数有着广泛的应用。例如,有些时候可能希望对输出数据进行格式化,但不是真的要把数据写出。这时候就可以使用sprintf函数来实现格式化,然后把结果存储在字符串中直到需要产生输出的时候再写出。sprintf函数还可以用于把数转换成字符格式。
案例:
struct S
{
char name[30];
int age;
double d;
};
int main()
{
char buf[256] = { 0 };
struct S s = { "zhangsan",20,95.5 };
//sprintf:把一个格式化的数据转换成字符串
//int sprintf(char* str, const char* format, ...)
//发送格式化输出到str所指向的字符串
//如果成功,则返回写入的字符总数,不包括字符串追加在字符串末尾的空字符。如果失败,则返回一个负数
sprintf(buf, "%s %d %lf", s.name, s.age, s.d);
printf("%s\n", buf);
return 0;
}
运行结果:
输入函数:
int sscanf(const char* s,const char* format,...);
sscanf函数与scanf函数和fscanf函数都很类似,唯一的不同就是sscanf函数是从(第一个参数指向的)字符串而不是流中读取数据。sscanf函数的第二个参数是格式串,这与scanf函数和fscanf函数所用的一样。sscanf函数对于从由其他输入输入函数读入的字符串中提取数据非常方便。
用sscanf函数代替scanf函数或者fscanf函数的好处之一就是,可以按需要多次检测输入行,而不再只是一次,这样使识别 替换的输入格式和从错误中恢复都变得更加容易了。
案例:
struct S
{
char name[30];
int age;
double d;
};
int main()
{
char buf[256] = { 0 };
struct S s = { "zhangsan",20,95.5 };
struct S tmp = { 0 };
sprintf(buf, "%s %d %lf", s.name, s.age, s.d);
printf("%s\n", buf);//字符串
//sscanf:把一个字符串转换成格式化的数据
//int sscanf(const char* str, const char* format, ...)
//从字符串读取格式化输入
//如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回EOF
//从buf字符串中提取结构体数据
sscanf(buf, "%s %d %lf", tmp.name, &(tmp.age), &(tmp.d));
//打印
printf("%s %d %lf\n", tmp.name, tmp.age, tmp.d);//格式化形式
return 0;
}
运行结果:
4.6.块的输入输出fread/fwrite
fread函数和fwrite函数运行程序在单步中读和写大的数据块。如果小心使用,fread函数和fwrite函数可以用于文本流,但是它们主要还是用于二进制的流。
输出函数:
size_t fwrite(const void* ptr,size_t size,size_t nmemb,FILE* stream);
fwrite函数被设计用来把内存中的数组复制给流。fwrite函数调用中第一个参数就是数组的地址,第二个参数是每个数组元素的大小(以字节为单位),而第三个参数则是要写的元素数量,第四个参数四文件指针,此指针说明了要写的数据位置。例如,为了写整个数组a的内容,就可以使用下列fwrite函数调用:fwrite(a,sizeof(a[0]),sizeof(a)/sizeof(a[0]),fp);
fwrite函数返回实际写入的的元素(不是字节)的数量。如果出现写入错误,那么此数就会小于第三个参数。
案例:
struct S
{
char name[20];
int age;
double d;
};
int main()
{
struct S s = { "李四",20,95.5 };
FILE* pf = fopen("test3.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//fwrite:二进制输出
//size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);
//把ptr所指向的数组中的数据写入到给定流stream中
//如果成功,该函数返回一个size_t对象,表示元素的总数,该对象是一个整型数据类型。如果该数字与nmemb参数不同,则会显示一个错误
//二进制的方式写文件
fwrite(&s, sizeof(struct S), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
输入函数:
size_t fread(void* ptr,size_t size,size_t nmemb,FILE* stream);
fread函数将从流读入数组的元素。fread函数的参数类似于fwrite函数的参数:数组的地址,每个元素的大小(以字节为单位),要读的元素数量已经文件指针。为了把文件的内容读入数组a,可以使用下列fread函数调用:n=fread(a,sizeof(a[0]),sizeof(a)/sizeof(a[0]),fp);
检查fread函数的返回值是非常重要的。此返回值说明了实际读的元素(不是字节)的数量。此数应该等于第三个参数,除非达到了输入文件末尾或者出现了错误。
当程序需要在终止之前把数据存储到文件中时使用fwrite函数是非常方便的。以后程序(或者另外的程序)可以使用fread函数把数据读回内存中来。
案例:
struct S
{
char name[20];
int age;
double d;
};
int main()
{
struct S s = { 0 };
//读文件:以二进制的方式读
FILE* pf = fopen("test3.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//fread:二进制输入
//size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream)
//从给定流stream读取数据到ptr所指向的数组中
//成功读取的元素总数会以size_t对象返回,size_t对象是一个整型数据类型。如果总数与nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾
//二进制的方式读文件
fread(&s, sizeof(struct S), 1, pf);
printf("%s %d %lf\n", s.name, s.age, s.d);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
4.7.对比一组函数
对比两组函数:scanf/fscanf/sscanf,printf/fprintf/sprintf
scanf和printf
- scanf:格式化的输入函数 - stdin
- printf:格式化的输出函数 - stdout
fscanf和fprintf
- fscanf:针对所有输入流的格式化的输入函数 - stdin/文件流
- fprintf:针对所有输出流的格式化的输出函数 - stdout/文件流
sscanf和sprintf
- sscanf:把一个字符串转换成格式化的数据
- sprintf:把一个格式化的数据转换成字符串
五.文件的随机读写
正常情况下,数据以线性的方式写入,这意味着后面写入的数据在文件中的位置是在以前所有写入数据的后面。C同时支持随机访问I/O,也就是以任意顺序访问文件的不同位置。随机访问是通过在读取或写入先前定位到文件中需要的位置来实现的。
5.1.fseek
int fseek(FILE* stream,long int offset,int whence);
fseek函数改变与第一个参数(即文件指针)相关联的文件位置。第二个参数是个(可能为负的)字符计数,注意字符计数是long int类型的。第三个参数说明新位置是根据文件的起始处,当前位置还是文件末尾来计算的。<stdio.h>为此定义了三个宏:
- SEEK_SET:文件的起始处,offset必须是一个非负值
- SEEK_CUR:文件的当前位置,offset的值可正可负
- SEEK_END:文件的末尾处,offset的值可正可负。如果它是正值,它将定位到文件尾的后面
通常情况下,fseek函数返回零。如果产生错误(例如,要求的位置不存在),那么fseek函数就会返回非零值。
顺便说一句,文件定位函数最适合用于二进制流。fseek函数对流是文本的还是二进制的很敏感。在二进制流中,从SEEK_END进行定位可能不被支持,所以应该避免。在文本流中,如果whence是SEEK_CUR或SEEK_END,offset必须是零。如果whence是SEEK_SET,offset必须是一个从同一个流中以前调用ftell所返回的值。
案例一:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//随机写
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
fseek(pf, 2, SEEK_CUR);//当前位置为c,跳过两个字符到e
ch = fgetc(pf);
printf("%c\n", ch);//e
fseek(pf, -1, SEEK_END);//文件指针在末尾时,pf指向'\0'的位置
ch = fgetc(pf);
printf("%c\n", ch);//f
fseek(pf, 4, SEEK_SET);//当前位置为a,跳过四个字符到e
ch = fgetc(pf);
printf("%c\n", ch);//e
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
案例二:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//随机读
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);
fseek(pf, -3, SEEK_CUR);
fputc('w', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
5.2.ftell
long int ftell(FILE* stream);
ftell函数返回流的当前位置,也就是说,下一个读取或写入将要开始的位置距离文件起始位置的偏移量。这个函数允许你保存一个文件的当前位置,这样你可能在将来会返回到这个位置。在二进制流中,这个值就是当前位置距离文件起始位置之间的字符数。
在文本流中,这个值表示一个位置,但它并不一定准确地表示当前位置和文件起始位置之间的字符数,因为有些系统将对行末字符进行翻译转换。但是,ftell函数返回的值总是可以用于fseek函数中,作为一个距离文件起始距离的偏移量。
案例:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//随机读
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);
fseek(pf, -3, SEEK_CUR);
fputc('w', pf);
long pos = ftell(pf);
printf("%ld\n", pos);//2
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
5.3.rewind
void rewind(FILE* stream);
rewind函数会把文件位置设置在起始处。调用rewind(fp)几乎等价于fseek(fp,0L,SEEK_SET),两者的差异是rewind函数不返回值,但是会为fp清楚错误指示器。
案例:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//随机读
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);
fseek(pf, -3, SEEK_CUR);
fputc('w', pf);
long pos = ftell(pf);
printf("%ld\n", pos);//2
rewind(pf);//返回到文件起始位置
pos = ftell(pf);
printf("%ld\n", pos);//0
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
六.文件类型
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在文件中是怎么存储的?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII的形式输出到磁盘,则磁盘中占用5个字节 (每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节 (VS2019已测)。
案例:
int main()
{
int a = 10000;
FILE* pf = fopen("test1.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fwrite(&a, 4, 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
文本文件:
二进制文件:
七.文件结束判断
被错误使用的feof:
int feof(FILE* stream);
//feof:用于判断是否读到了文件的末尾而结束
int ferror(FILE* stream);
//ferror:用于判断是否在读取文件时发生了错误
在文件读取的过程中,不能使用feof函数的返回值直接用来判断文件是否结束。feof主要应用于:当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
文本文件读取是否结束 ,判断返回值是否为EOF (fgetc),或者NULL (fgets)
- fgetc判断是否返回EOF (fgetc读取结束时返回EOF,正常读取时,返回读取到字符的);
- fgets判断返回值是否为NULL (fgets函数在读取结束时返回NULL,正常读取时,返回存放字符串空间的起始地址);
二进制文件读取结束判断,判断返回值是否小于实际要读的个数
- fread判断返回值是否小于实际要读的个数 (fread读取时,返回实际读取到的元素个数,如果发现读取到到的元素个数小于实际要读取的元素个数,这就是最后一次读取 )。
案例一:文本文件的判断
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int ch = 0;
while ((ch = fgetc(pf)) != EOF)//判断文件是否读取结束
{
putchar(ch);
}
printf("\n");
// 判断文件为何读取结束
if (ferror(pf))
{
printf("读取文件中发生了错误\n");
}
if (feof(pf))
{
printf("读取到文件末尾结束\n");
}
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
案例二:二进制文件的判断
#define size 5
int main()
{
int arr[size] = { 1,2,3,4,5 };
FILE* pf = fopen("test2.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fwrite(arr, sizeof(arr[0]), size, pf);
fclose(pf);
int ret[size] = { 0 };
pf = fopen("test2.txt", "rb");
size_t ret_num = fread(ret, sizeof(ret[0]), size, pf);
if (ret_num == size)//判断文件是否读取结束
{
printf("读取全部数据成功\n");
int i = 0;
for (i = 0; i < size; i++)
{
printf("%d ", ret[i]);
}
printf("\n");
}
else//判断文件为何读取结束
{
if (ferror(pf))
{
printf("在读取中出现错误\n");
}
if (feof(pf))
{
printf("读取到文件末尾结束\n");
}
}
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
八.文件缓冲区
ANSIC标准采用"缓冲文件系统"处理数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块"文件缓冲区"。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小由C编译系统决定。其实不难想象缓冲区的存在使得效率提升了。
案例:
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fclose");
return 1;
}
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("数据在缓冲区中,睡眠10秒,打开test.txt文件,发现文件中并没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件中(磁盘)。注:fflush在高版本的VS上不能使用
printf("再睡眠10秒,再次打开test.txt文件,文件中有内容了\n");
Sleep(10000);
//注:为什么刷新缓冲区后还要再睡眠10秒,因为fclose在关闭文件时,也会刷新缓冲区
fclose(pf);
pf = NULL;
return 0;
}