一、文件与流
为什么要使用文件:程序的处理结果在程序结束后就消失,要想将程序运行的结果保存下来,就可以将相关内容保存在文件(file)中。
针对文件、键盘、显示器、打印机等外部设备的数据的读写操作都是通过流(stream)进行操作的。我们可以将流想象成流淌着字符的河流。
二、标准流
stdin:标准输入流
用于读取普通输入的流。常用函数有scanf、getchar。
stdout:标准输出流
用于写入普通输出的流。常用函数有printf、putchar。
stderr:标准错误流
用于写出错误的流。通常是将错误信息显示到屏幕上。
三、FILE型
定义于<stdio.h>中,用于记录控制流所需要的信息,包含:
- 文件位置指示符:记录当前访问位置。
- 错误指示符:记录是否发生了错误的读取。
- 文件结束指示符:记录是否到达文件末尾。
FILE类型的指针变量指向的是文件信息区(结构体),包含文件名(由路径、文件名、文件类型组成)等信息。通过文件信息能够间接找到关联的文件。在不同的编译器中具体的实现方法是有差异的。
四、打开文件fopen
打开文件的操作称为打开。库函数中的fopen函数用于打开文件。
//fopen函数的原型
FILE * fopen ( const char * filename, const char * mode );
使用该函数需要两个参数:第一个是需要的文件的文件名("文件名.文件类型"),第二个是打开该文件的方式。
fopen函数会为要打开的文件新建一个流。
如果打开成功,则返回一个FILE类型的指针,指向该文件的流 。否则返回一个空指针。
FILE* pf1 = fopen("test1.txt", "w");
//以只读的方式打开test1.txt,由于该文件之前不存在,所以会自动创建,文件在工程文件下
//如果以只写的方式打开一个不存在的文件会报错
//判断是否创建成功
if (pf == NULL)//创建失败返回值为null
{
perror(fopen);
return 1;
}
文件的打开方式有:
(图源:明解C语言第三版)
需要注意的点:
(图源:明解C语言第三版)
五、关闭文件fclose
关闭文件的操作称为关闭,库函数中的fclose函数用于关闭文件。
//fclose函数的原型
int fclose ( FILE * stream );
使用该函数需要的参数:指向想要关闭的文件的指针 。
如果关闭成功,则返回0。检查到错误时返回EOF(End-of-File,返回-1)。
//关闭文件
fclose(pf);
//在关闭文件后,可以将原先指向文件的指针置为空指针从而避免称为野指针
pf = NULL;
六、文件的顺序读写
6.1fputc
fputc:向文件中写入字符,适用于所有输出流
//函数原型
int fputc ( int character, FILE * stream );
该函数的参数为一个int类型的字符(传递字符实际上就是传递字符的ASCII码值,所以可以设计为int类型)以及一个指向目标文件的流的指针。
int main()
{
FILE* pf2 = fopen("test2.txt", "w");
char c;
if (pf2 == NULL)
{
perror("fopen");
return 1;
}
for (c = 'a'; c <= 'z'; c++)
{
fputc(c, pf2);
//将字符c写入pf2指向的文件
}
fclose(pf2);
pf2 = NULL;
return 0;
}
6.2fgetc
fgetc:从文件中读取字符,适用于所有输入流
//函数原型
int fgetc ( FILE * stream );
该函数的参数只需要一个指向想要读取的文件的指针。
如果读取成功,返回读取到的字符的ASCII码值;
如果读取到文件末尾或者读取过程中遇到错误,返回(EOF)。
int main()
{
FILE* pf2 = fopen("test2.txt", "r");
if (pf2 == NULL)
{
perror("fopen");
return 1;
}
int count = 0;
int ch = 0;
while ((ch=fgetc(pf2))!= EOF)
//没有碰到文件末尾或者错误时,则继续循环
{
printf("%c", ch);
//可以计算文件有多少个字符
count++;
}
printf("count: %d\n", count);
fclose(pf2);
pf2 = NULL;
return 0;
}
上面这段代码可以用来统计文件中有多少个字符。
6.3fputs
fputs:向文件中写入字符串
//函数原型
int fputs ( const char * str, FILE * stream );
第一个参数为指向想要写入的字符串的指针,第二个参数为指向目标文件的指针。
如果写入成功,则返回一个非负值。否则返回EOF并设置一个错误指示器。
6.4fgets
fgets:从文件中读取字符串
//fgets函数的原型
char * fgets ( char * str, int num, FILE * stream );
该函数能够将目标文件中的内容拷贝到目标字符串中。
第一个参数是一个指向要存放从文件中拷贝到的内容的字符串的指针,第二个参数是要拷贝的字符的个数,第三个参数是一个指向目标文件的指针。
如果读取正常,返回str指针。
如果遇到文件末尾或者读取过程中发生错误,返回null。
#include<string.h>
int main()
{
FILE* pf3 = fopen("test3.txt", "r");
if (pf3 == NULL)
{
perror("fopen");
return 1;
}
char mystring[255];
fgets(mystring, 255, pf3);//把pf3指向的内容复制到mystring
puts(mystring);
fclose(pf3);
pf3 = NULL;
return 0;
}
如果操作成功,则该函数返回要读取的内容。
如果在尝试读取字符时遇到文件末尾,则设置 eof 指示符 (feof)。如果在读取任何字符之前发生这种情况,则返回的指针为空指针(并且 str 的内容保持不变)。
如果发生读取错误,则设置错误指示符 (ferror),并返回 null 指针(但 str 指向的内容可能已更改)。
6.5fscanf
这是一个格式化输入函数,适用于所有输入流。
//fscanf函数的原型
int fscanf ( FILE * stream, const char * format, ... );
//scanf函数的原型
int scanf ( const char * format, ... );
通过对比观察我们可以发现:fscanf比scanf多了一个参数,即要读取的文件的指针
下面我们通过一个案例看看具体的使用过程
#include<stdio.h>
int main()
{
int total = 0;//人数
char name[100];
double height, weight;
double height_sum = 0;
double weight_sum = 0;
FILE* pf = fopen("data.dat", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
while (fscanf(pf, "%s%lf%lf", name, &height, &weight) == 3)
{
printf("%-10s %5.1f %5.1f\n", name, height, weight);
total++;
height_sum += height;
weight_sum += weight;
}
printf("--------------------------------\n");
printf("average: %5.1f %5.1f\n", height_sum / total, weight_sum / total);
fclose(pf);
pf = NULL;
return 0;
}
我们来看看上面这段代码。
首先打开了要读取的文件"data.dat",再将指向该文件流的指针作为参数传递给fscanf函数获取相关数据,最后输出得到的数据。
6.6fprintf
这是一个格式化输出函数,适用于所有输出流。
//fprintf函数的原型
int fprintf ( FILE * stream, const char * format, ... );
//print函数的原型
int printf ( const char * format, ... );
通过对比观察我们可以发现,fprintf函数比printf函数多了一个参数,即指向要写入数据的文件的指针。
如果写入成功,则返回写入的字符个数。
如果写入错误,则设置错误指示符并返回负数。
如果写入宽字符时发生多字节字符编码错误,则 errno 设置为 EILSEQ 并返回负数。
6.7fread
二进制输入函数
//fread函数的原型
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
这个函数会从指定的文件流中读取数据,每次读取size*nmemb个字节,把它们存储到ptr指向的空间。
函数返回实际读取到的项数。
如果读取错误或读取到了文件末尾,则返回一个小于nmeneb的数。
6.8fwrite
二进制输出函数
//fwrite函数原型
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
这个函数会从ptr指向的空间读取数据,每次读取size*count个字节,存入stream指向的文件流。
函数返回成功写入的字符的个数。
如果写入时出错(返回的数字和成功情况下的不同),在流中会设置一个错误指引(fereror)。
如果size或count为零,则返回零并设置错误指引。
下面是一个关于fwrite以及fread函数的简单使用案例:
#include<stdio.h>
enum
{
SIZE = 5
};
int main()
{
double a[SIZE] = { 1.,2.,3.,4.,5. };
FILE* fp = fopen("test.bin", "wb");//必须使用二进制模式
if (fp == NULL)
{
perror("fopen");
return 1;
}
fwrite(a, sizeof * a, SIZE, fp);//将数组a中的sizeof(a[0])*SIZE个字节的数据写入到fp指向的文件流
fclose(fp);
double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof * b, SIZE, fp);//将fp指向的文件流中的数据复制到b中,读取sizeof(b[0])*SIZE个字节
if (ret_code == SIZE)
{
printf("read successfully\n");
for (int i = 0; i < SIZE; i++)
{
printf("%lf ", b[i]);
}
}
else
{
if (feof(fp))//feof用于判断读取结束的原因是否是因为遇到了文件末尾,如果到达了文件末尾返回非零值,否则返回零
{
printf("error reading test.bin:unexpected end of file\n");
}
else if (ferror(fp))//文件流中发生了错误,则返回1
{
printf("error reading test.bin\n");
}
}
fclose(fp);
fp = NULL;
return 0;
}
首先定义了一个数组a并进行了初始化,然后再将目标文件打开并使用fwrite函数写入数据。
在使用fread函数时,通过判断返回值是否与数组大小相等再进行接下来的操作。
七、文件的随机读写
7.1fseek
该函数的作用是根据指针的偏移量来读取数据
//fseek函数原型
int fseek ( FILE * stream, long int offset, int origin );
第一个参数是目标文件流的指针,第二个参数指的是指针偏移量,第三个参数代表指针偏移的开始位置
int origin有三种参数:
图源:fseek - C++ Reference (cplusplus.com)
第一个为文件的起始位置
第二个为当前指针的位置
第三个为文件的末尾
使用该函数的简单案例:
#include<stdio.h>
int main()
{
FILE* pf = fopen("fseekTest.txt", "r");
char ch;
if (pf == NULL)
{
ferror("fopen");
return 1;
}
fseek(pf, 4, SEEK_CUR);//从当前指针(光标)向后偏移4
ch = fgetc(pf);//得到e
printf("%c\n", ch);
fseek(pf, 2, SEEK_SET);//从起始位置开始
ch = fgetc(pf);
printf("%c\n", ch);
fseek(pf, -3, SEEK_END);//从末尾开始
ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
7.2ftell
该函数用于获取当前光标相对于起始位置的偏移量。
//ftell函数的原型
long int ftell ( FILE * stream );
该函数只需要一个参数:指向文件流的指针。
ftell使用的简单案例:
int main()
{
long int ptrPosition = 0;
char ch;
FILE* pf = fopen("fseekTest.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fseek(pf, 4, SEEK_CUR);//从当前指针(光标)向后偏移4
ch = fgetc(pf);//得到e,读完之后光标向后偏移
printf("%c\n", ch);
ptrPosition=ftell(pf);//获取当前指针位置(光标\偏移量)
printf("%d\n", ptrPosition);
fclose(pf);
pf = NULL;
return 0;
}
7.3rewind
该函数用于将光标设置为起始位置
//rewind函数的原型
void rewind ( FILE * stream );
该函数只需要一个参数:指向文件流的指针。
rewind函数使用的简单案例:
int main()
{
char ch;
long int pfPosition;
FILE* pf = fopen("fseekTest.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fseek(pf, 3, SEEK_SET);
ch = fgetc(pf);
printf("%c\n", ch);
pfPosition = ftell(pf);
printf("%d\n", pfPosition);
rewind(pf);//将指针的位置更改为起始位置
pfPosition = ftell(pf);
printf("%d\n", pfPosition);
fclose("pf");
pf = NULL;
return 0;
}
八、文件读取结束的判定
判断文件结束的原因,我们可以用feof函数。
当我们打开文件流时,会有两个标记,一个是feof(用于判断是否到达文件末尾),另一个是ferror(用于判断文件读取过程是否发生错误)。
当文件读取结束的原因是遇到文件末尾(即EOF)说明读取正常。
如果在文件读取过程中发生错误,可以使用ferror函数来设置标记,表明文件读取结束不正常。
完。