文章目录
文件操作
为什么使用文件?
文件操作可以让我们的数据持久化
什么是文件?:指的是磁盘上面的文件
文件名:一个文件要有为一个文件标识,以便用户识别和辨认,
文件名包括三部分:文件路径+ 文件名主干+文件后缀
为了方便起见:文件标识就是文件名。
文件的分类
在程序设计中有两种文件:程序文件、数据文件(从文件的功能角度来分类)
程序文件:
包括源程序文件(后缀:.c
)、目标文件(windows环境后缀:.obj
)、可执行文件(windows环境后缀.exe
)
数据文件:
文件的内容不一定是程序,而是程序运行时读取的数据,比如程序原需要从中读取数据的文件,或者输出内容的文件
本篇博客主要是在介绍数据文件
文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
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* pf;//文件指针变量
文件的打开与关闭
文件在读写之前应该先打开,在使用完后应该关闭文件。在编写程序的时候打开文件的同时就会返回一个FILE类型的指针指向这一个文件,相当于建立了指针和文件的关系
fopen
与fclose
打开文件的函数是fopen
,关闭文件的函数是fclose
下面我们来了解这两个函数:
这是fopen
函数的函数声明
FILE *fopen( const char *filename, const char *mode );
该函数第二个参数(文件打开方式有多种):
模式字符串 | 含义 | 如果没有对应文件 |
---|---|---|
"r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 打开失败 |
"w"(只写) | 为了输出数据,打开一个文本文件 | 创建一个新的文件 |
"a"(追加) | 向文本文件结尾添加数据 | 创建一个新的文件 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 打开失败 |
“w+”(读写) | 为了读和写,打开一个文本文件 | 创建一个新的文件 |
“a+”(读写) | 打开一个文件,在文件结尾添加数据 | 创建一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个已经存在的二进制文件 | 打开失败 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 创建一个新的文件 |
“ab”(追加) | 向一个二进制文件结尾添加数据 | 创建一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 打开失败 |
“wb+”(读写) | 为了读和写打开一个二进制文件 | 创建一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件结尾添加数据 | 创建一个新的文件 |
这是fclose
函数的函数声明:
int fclose( FILE *stream );
下面我们来具体操作一下打开文件和关闭文件:
”r“—只读文件:我们知道利用这种方式去打开一个文件,如果文件目录中找不到我们需要打开的文件,就会返回一个空指针:
结果也确实是返回了一个空指针
”w“—只写文件:我们在用”w“模式区打开一个还没有创建的文件:
程序正常进行,没有出现错误,这时候我们再去文件目录下看会发现已经创建了一个新的文件:
文件的读与写
了解输入与输出:
文件的顺序读写:
文件以某一个顺序进行读写操作。
我们需要知道一些输入输出函数(会详细介绍):
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式话输出函数 | fprintf | 所有输出流 |
二进制输入函数 | fread | 文件 |
二进制输出函数 | fwrite | 文件 |
fputc
与fgetc
使用fputc
写文件
fputc
是一个字符输出函数,函数声明是:
int fputc( int c, FILE *stream );
例子1(输出数据到一个文件中):
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写文件
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
程序正常执行
我们也可以在文件目录下找到对应的文件:该文件的内容也确实是我们输入的’a’、‘b’、‘c’。
例子2(输出到标准输出中):
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
fputc('a', stdout);
fputc('b', stdout);
fputc('c', stdout);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们可以看到在屏幕上面打印了字符’a’、‘b’、‘c’。
使用fgetc
读文件
此时我们不能再使用“w”
模式打开文件,我们需要使用满足于读文件的打开方式"r"
;
fgetc
函数的声明:
int fgetc( FILE *stream );
我们同样用两种情况来解释来运用该函数
1.从文件中读取字符
此时我们的“data.txt”
文件中存放了字符abcdefg
我们利用fgetc
函数将其一次读出:
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);//可以将流中的字符全部读完
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
读取成功
2.从标准输入中读取字符
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ch = 0;
int count = 5;
while (count--)
{
ch = fgetc(stdin);
printf("%c", ch);
}//从键盘输入中读取五个字符
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fgetc
函数成功从标准输入(键盘)中读取了五个字符:
练习:文件的拷贝
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
//以读的方式打开第一个文件
FILE* pf1 = fopen("test1.txt", "r");
if (pf1 == NULL)
{
printf("test1.txt: %s", strerror(errno));
return 0;
}
//以写的方式打开的二个文件
FILE* pf2 = fopen("test2.txt", "w");
if (pf2 == NULL)
{
printf("test2.txt: %s", strerror(errno));
fclose(pf1);
pf1 = NULL;
//如果第二个文件打开失败,那么在结束前要关闭前面打开的文件。
return 0;
}
//将第一个文件的内容复制到第二个文件中
int ch = 0;
while ((ch = fgetc(pf1)) != EOF)
{
fputc(ch, pf2);
}
//关闭两个文件
fclose(pf1);
pf1 = NULL;
fclose(pf2);
pf2 = NULL;
return 0;
}
文件指针的偏移
那是因为文件指针的作用,每一次读取该文件后,文件指针就会向后偏移一位,这样下一次读取的时候,就会读取到下一个字符:
fgets
与fputs
函数
前面的一组函数一次只能读写一个字符,而下面这一组函数一次可以对一行字符进行读写
fputs
首先是fputs
函数,它可以直接写一行字符
该函数的声明如下:
int fputs( const char *string, FILE *stream );
利用这个函数向文件中写字符
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写文件
fputs("hello world", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
字符串被成功写入文件:
需要注意:
如果我们在输入的时候没有在\n
,那么在文件中写入字符时,两行字符不会隔开,而是打印在同一行。
同样我们可以利用标准输出将其打印在屏幕上面:
接下来我们利用fgets
读取一行的字符
fgets
声明:
char *fgets( char *string, int n, FILE *stream );
我们来利用这个函数去读取文件
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
char buf[1000] = { 0 };
fgets(buf, 1000, pf);
//由于数组buf有1000个元素,所以第二个参数我们设置的最大读取数是1000.
printf("%s", buf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们的文件中一开始保存了“i love beijing”
.
成功读取文件中的字符,并且保存在数组buf
中,然后把数组内存打印出来:
我们也可以利用第二个参数来控制我们需要读取的字符数量
该函数的返回值:
如果该函数成功读取字符,那么它会返回保存字符串的数组的首地址,如果该函数遇到错误或者读取到文件末尾,那么它会返回空指针
上面的两组函数进行的是文本的输入输出,而下面这一组函数可以进行格式的输入输出
fprintf
与pscanf
函数
fprint
#include<stdio.h>
#include<string.h>
#include<errno.h>
struct Stu {
char name[20];
int age;
double score;
};
int main()
{
struct Stu s = { 0 };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
//写格式化的数据
printf("%s %d %lf\n", s.name, s.age, s.score);
fprintf(pf, "%s %d %lf\n", s.name, s.age, s.score);//输入到文件中
fprintf(stdout,"%s %d %lf\n", s.name, s.age, s.score);//打印到屏幕上
fclose(pf);
pf = NULL;
return 0;
}
我们来看一看效果:
打印的第一遍是printf
函数的效果,打印的第二遍是fprintf
的效果
同时,文件中也已经成功写入了数据:
fscanf
同样我们可以利用fscanf
函数去读文件.
#include<stdio.h>
#include<string.h>
#include<errno.h>
struct Stu {
char name[20];
int age;
double score;
};
int main()
{
struct Stu s = { 0 };
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
//读格式化的数据
fscanf(pf,"%s %d %lf", s.name, &(s.age), &(s.score));
printf("%s %d %lf", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
在使用这个函数去读取数据前,我们先确保文件中含有数据
我们成功的将文件中的数据读取到结构体变量s中,并且把他打印出来:
附:sprintf
与sscanf
函数:
这一组函数不是用来进行文件操作的,但是由于前面介绍了很多输入输出函数,所以这里也将这一组函数也解释一下。:
sprintf
:
该函数的原型是:
int sprintf( char *buffer, const char *format [, argument] ... );
#include<stdio.h>
struct Stu {
char name[20];
int age;
double score;
};
int main()
{
struct Stu s = { "zhangsan",19,61.2 };
char buf[100] = { 0 };
sprintf(buf, "%s %d %lf", s.name, s.age, s.score);
printf("%s", buf);
return 0;
}
sscanf
该函数的原型:
int sscanf( const char *buffer, const char *format [, argument ] ... );
#include<stdio.h>
struct Stu {
char name[20];
int age;
double score;
};
int main()
{
struct Stu s = { "zhangsan",19,61.2 };
struct Stu tmp = { 0 };
char buf[100] = { 0 };
//先利用sprintf函数将格式化数据放在buf数组中
sprintf(buf, "%s %d %lf\n", s.name, s.age, s.score);
//打印buf数组
printf("%s", buf);
//再利用sscanf函数将buf数组中的字符串格式化处理放在结构体变量tmp中
sscanf(buf, "%s %d %lf", tmp.name, &(tmp.age), &(tmp.score));
//打印出tmp 的各个成员
printf("%s %d %lf\n", tmp.name, tmp.age, tmp.score);
return 0;
}
fread
与fwrite
函数
fwrite
该函数可以以二进制的方式写,声明是:
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
我们利用这个函数来把一个数据以二进制的方式写入文件
#include<stdio.h>
#include<string.h>
#include<errno.h>
struct Stu {
char name[20];
int age;
double score;
};
int main()
{
struct Stu s[2] = { {"zhangsan",19,65.1},{"lisi",20,62.1} };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
fwrite(&s, sizeof(struct Stu), 2, pf);
fclose(pf);
pf = NULL;
return 0;
}
fread
该函数可以以二进制方式的读文件,函数声明是:
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
同样,我们运用一次该函数:
#include<stdio.h>
#include<string.h>
#include<errno.h>
struct Stu {
char name[20];
int age;
double score;
};
int main()
{
struct Stu s[2] = { 0 };
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
//二进制方式的读文件
fread(s, sizeof(struct Stu), 2, pf);
//打印数组中的内容
printf("%s %d %lf\n", s[0].name, s[0].age, s[0].score);
printf("%s %d %lf\n", s[1].name, s[1].age, s[1].score);
fclose(pf);
pf = NULL;
文件的随机读写
前面说的都是文件的顺序读写,但是其实文件还可以实现随机读写
我们一般通过使用文件指针来进行文件的随机读写
fseek
int fseek( FILE *stream, long offset, int origin );
选项 | 不同选项所对应的位置 |
---|---|
SEEK_CUR | 文件指针当前的位置 |
SEEK_END | 文件末尾的位置 |
SEEK_SET | 文件起始位置 |
我们知道文件每被读取一次,文件指针就会向后移动一位。
我们来使用一次该函数,此前我们文件中的内容是“abcdef”
;
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
//打开文件
FILE *pf= fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//读文件
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
//定位文件指针
fseek(pf, 3, SEEK_CUR);
//该操作将文件指针从指向'c'向后偏移三个位置后为指向'f'.
ch = fgetc(pf);
printf("%c\n", ch);//f
fclose(pf);
//关闭文件
pf = NULL;
return 0;
}
ftell
除了刚才的函数以外,我们还需要了解这样一个函数,改函数可以返回文件指针相对于起始位置的偏移量
该函数的声明是:
long ftell( FILE *stream );//该函数唯一的参数就是一个流,然后返回改文件的偏移量
我们把这个函数运用在刚才的代码段:
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
FILE *pf= fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//读文件
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//定位文件指针
int offset = ftell(pf);
printf("%d",offset);
fclose(pf);
pf = NULL;
return 0;
}
我们在读取两次文件后利用改函数得到偏移量,然后打印出来:
rewind
我们还有一个函数可以改变文件指针的位置,这个函数是rewind,改函数可以让文件指针回到起始位置
该函数的声明是:
void rewind( FILE *stream );//唯一的参数是一个流
我们将改函数运用在刚才的代码中:
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
FILE *pf= fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//读文件
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//让文件指针指向初始位置
rewind(pf);
ch = fgetc(pf);
printf("%c\n",ch);
fclose(pf);
pf = NULL;
return 0;
}
使用了rewind函数后,我们再次读取该文件,然后打印出改字符,结果是打印了第一个字符
文本文件和二进制文件
一个数据在内存中是如何储存的?
字符一律是是使用ASCll
形式储存,数值性数据既可以用ASCll
码值存储,也可以用二进制从事存储。
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
int a = 10000;
fwrite(&a, 4, 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
我们以二进制的形式将数据10000存储在文件中,自然不能用文本编辑器查看:文本编辑器不能解析二进制文件,所以我们只能看到乱码
我们可以在利用VS中的二进制编辑器查看二进制文件:
我们将刚才的二进制文件查看可以得到这样的结果:
由下图可知,这样解析的结果是准确的:
文件读取结束的判定
在读文件的时候,经常会遇到读取文件结束的情况。
被错误使用的feof
该函数的真正用法是当我们已经确定文件读取结束的时候,用该函数来判定文件结束的原因:
- 文件读取失败导致的结束
- 读取到文件结尾导致结束
feof
函数的声明:
int feof( FILE *stream );
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
int ch = 0;
while ((ch = getc(pf)) != EOF)
{
printf("%c\t", ch);
}
if (ferror(pf))//如果在读文件的时候出现错误会执行第一条语句
puts("I/O error when reading");
else if (feof(pf))//如果没有错误则会打印该文件成功达到文件结尾
puts("End of file reached successfully");
fclose(pf);
pf = NULL;
return 0;
}
下面的结果说明本次文件读取结束是由于已经读取到文件末尾。
文件缓冲区
刷新缓冲区
文件缓冲区在没有充满的时候不会将数据输入或者输出。有点事可以大大提高程序的效率,但是有时候我们的程序却需要尽快的输出或输入数据,这是我们就需要刷新缓冲区。
如果我们刷新缓冲区,那么缓冲区的文件也会直接被读取。
我们可以利用函数fflush
刷新文件缓冲区
这里我们用一个例子来解释文件缓冲区:
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<windows.h>
int main()
{
FILE* fp = fopen("test.txt", "w");
if (fp == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
fputs("hello world", fp);
printf("睡眠十秒后刷新缓冲区,此时文件中没有信息\n");
Sleep(10000);
fflush(fp);
printf("睡眠十秒后关闭文件,此时文件中已经有信息了\n");
Sleep(10000);
fclose(fp);
fp = NULL;
return 0;
}
总结
每一步文件操作都和文件指针密切相关,所以我们需要时刻了解文件指针的状态(值得注意:每一次打开文件都需要排除文锦指针为NULL的情况)。文件的读写操作有很多实现方法,文件的读写顺序也分为顺序读写和随机读写,进行顺序读写的函数有很多,他们有各自的特点:有的函数是进行的文本模式操作,有的则是以二进制的形式在操作文件……。文件的随机读写则更需要了解指针的位置。文件读取结束的判定也有值得注意的地方。