文件操作
在程序设计中,我们一般谈的文件有两种:程序文件、数据文件
程序文件:包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
数据文件:文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
文件类型
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件;
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
缓冲区的大小根据C编译系统决定的。
文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
即:每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
不同的编译器,大小可能有所差异
如:FILE* fp;
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
文件打开和关闭
那么如何打开文件呢?这里介绍一个函数 fopen
FILE * fopen ( const char * filename, const char * mode );
参数1:文件路径
参数2:打开方式
在使用fopen时,就会创建一个要打开文件的,文件信息区 并返回信息区的首地址,若打开失败,就会返回一个NULL空指针
例如:
FILE* fp1 = fopen("text.txt","r"); 在当前工程文件路径中查找 text.txt文件
或
FILE* fp2 = fopen("D:Code\\text.txt","r"); 在D盘Code文件里找 text.txt文件
if(fp2 == NULL)
{
perror("fopen");
}
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。
int fclose ( FILE * stream );
和free相似,只要传入指针即可
FILE* fp2 = fopen("D:Code\\text.txt","r"); 在D盘Code文件里找 text.txt文件
if(fp2 == NULL)
{
perror("fopen");
}
fclose(fp2);
fp2 = NULL;
注意,fclose也是不可传入NULL的
文件顺序读写
下面介绍顺序读写的几个常用函数
字符输入输出函数 fgetc、fputc
int fputc ( int ch, FILE *fp );
ch是要输出的字符(输入的字符会转为ascii码值),fp是流(文件,stdin键盘,stdout屏幕等)
如果没有发生错误,则返回被写入的字符。如果发生错误,则返回 EOF,并设置错误标识符。
int fgetc (FILE *fp);
fp是流,从哪写入(文件,键盘,屏幕等)
该函数返回值以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。
示例1:fputc
每次从ch中向文件里输出一个字符
int main()
{
FILE* fp = fopen("text.txt","w");w是写操作,会在指定路径创建这个文件,即使有这个文件也会被重新创建覆盖
if(fp == NULL)
{
perror("fopen");
return 1;
}
char ch = 'a';
for(ch = 'a';ch<='z';ch++)
fputc(ch,fp);
每次从ch中向文件里输出一个字符,文件中的位置指针就指向下一个位置
fclose(fp);
fp = NULL;
}
示例2:fgetc
每次从文件读取一个字符存到ch中
int main()
{
FILE* fp = fopen("text.txt","r");r是只读操作
if(fp == NULL)
{
perror("fopen");
return 1;
}
char ch = 0;
while( (ch = fgetc(fp)) != EOF)
printf("%c\n",ch);
每次通过函数从文件中读取的字符,并返回读取的字符存入ch,并通过字符型式打印
这里面有一个位置指针,每次读取一个字符,文件中的位置指针就会指向下一个字符,所以他是顺序读
直到文件末尾或出错,则返回EOF
fclose(fp);
fp = NULL;
}
fgets 、fputs
char *fgets(char *str, int n, FILE *stream)
参数1:这是指向一个字符数组的指针,该数组存储了要读取的字符串
参数2:这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
参数3:这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。
返回值:如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针
如果发生错误,返回一个空指针。
并且fgets会保存换行符
int fputs(const char *str, FILE *stream)
参数1: 这是一个数组,包含了要写入的以空字符终止的字符序列。
参数2: 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流。
返回值:该函数返回一个非负值,如果发生错误则返回 EOF。
示例1:fputs
每次向流中存入一行字符串(包括\0)
int main()
{
FILE* fp = fopen("text.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
printf("%d",fputs("hello\n",fp));
printf("%d",fputs("你好\n",fp));
fclose(fp);
fp = NULL;
}
需要注意的是,换行是要手动输入的
示例2:fgets
每次从流中读取一行字符串,最多读取 n个字符(包括\0)
int main()
{
char ch[256] = {0};
FILE* fp = fopen("text.txt","r");
if(fp == NULL)
{
perror("fopen");
return 1;
}
第一行
fgets(ch,256,fp); 256的意思是一行中最大读取数是256个,实际读取到\0就结束了
printf("%s",ch); 所以第一行最大读取数实际就是256-1个,因为有\0
第二行
fgets(ch,256,fp);
fputs(ch,stdout); stdout的意思是屏幕,就是从ch中的字符串输出到屏幕上
或者
while(fgets(ch,256,fp) != NULL)
fputs(ch,stdout);
当文件位置指针到末尾时,也就是读取到了末尾,就会返回NULL
fclose(fp);
fp = NULL;
}
fprintf、fscanf
int fprintf(FILE *stream, const char *format, ...)
比起平常使用的printf多了一个FILE* stream,意思是输出到哪个流上
int fscanf(FILE *stream, const char *format, ...)
比起平常使用的scanf多了一个FILE* stream,意思是从哪个流输入数据
示例1:fprintf
struct stu
{
char name[20];
int age;
double b;
};
int main()
{
struct stu ps = {{"张三"},20,98.8};
FILE* fp = fopen("text.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
fprintf(fp,"%s %d %lf\n",ps.name,ps.age,ps.b);
fprintf(fp,"%s %d %lf",ps.name,ps.age,ps.b);
fprintf(fp,"%s %d %lf",ps.name,ps.age,ps.b);
fclose(fp);
fp = NULL;
}
注意这里,第一次输出有\n换行符,则文件中位置就会换行,第二次没有换行符,文件中也就没有换行
示例2:fscanf
struct stu
{
char name[20];
int age;
double b;
};
int main()
{
struct stu ps = {{"张三"},20,98.8};
FILE* fp = fopen("text.txt","r");
if(fp == NULL)
{
perror("fopen");
return 1;
}
struct stu pa = {0};
fscanf(fp,"%s %d %lf",pa.name,&(pa.age),&(pa.b));
printf("%s %d %lf\n",pa.name,pa.age,pa.b);
fclose(fp);
fp = NULL;
}
使用上述两个函数要注意:
1.fprintf想在文件中换行,需要手动输入\n
2.fscanf不要忘记除了字符串以为的取地址
3.使用这两个函数一定要格式对应,怎么存的格式就怎么读出,且顺序一定要一样
还有相同的两个函数 sprintf sscanf
struct stu
{
char name[20];
int age;
double b;
};
int main()
{
struct stu s = {{"张三"},5,25.2};
struct stu q = {0};
char buf[256] = {0};
输出到字符数组中
sprintf(buf,"%s %d %lf",s.name,s.age,s.b);
printf("%s\n",buf); //打印字符串
从字符数组中读取
sscanf(buf,"%s %d %lf",q.name,&(q.age),&(q.b));
printf("%s %d %lf\n",q.name,q.age,q.b); //打印结构体内容
return 0;
}
作用是:
把输出的格式化内容转化为字符串;
从字符串读取内容转化为相应的格式
二进制顺序读写fread 、fwrite(只针对文件)
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
参数1:这是指向要被写入的元素的指针。
参数2:这是要被写入的每个元素的大小,以字节为单位。
参数3:这是元素的个数,每个元素的大小为 size 字节。
参数4:这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。
返回值:如果成功,该函数返回一个 size_t 对象,表示元素的总数,该对象是一个整型数据类型。如果该数字与 nmemb 参数不同,则会显示一个错误
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
参数1:这是指向带有最小尺寸 size*nmemb 字节的内存块的指针。
参数2:这是要读取的每个元素的大小,以字节为单位。
参数3:这是元素的个数,每个元素的大小为 size 字节。
参数4:这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流。
返回值:成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。如果总数与 nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾。
示例1:fwrite
struct stu
{
char name[20];
int age;
double b;
};
int main()
{
struct stu s = {{"张三"},5,5.6};
FILE* fp = fopen("text.txt","wb");
if(fp == NULL)
{
perror("fopen");
return 1;
}
从s中读取2个 struct stu类型大小的数据,并以二进制形式存入到fp指向的文件中
fwrite(&s,sizeof(struct stu),2,fp);
这个2,是最多读取,这里只有一个struct stu这么大的数据,所以读完一次就不读了
fclose(fp);
fp = NULL;
}
示例2:fread
struct stu
{
char name[20];
int age;
double b;
};
int main()
{
struct stu s = {0};
FILE* fp = fopen("text.txt","rb");
if(fp == NULL)
{
perror("fopen");
return 1;
}
从fp指向的文件中,读取2个struct stu大小的数据,并存入到s中
但实际之前只写入了一个,所以读完一个就停止了
fread(&s,sizeof(struct stu),2,fp);
fprintf(stdout,"%s %d %lf\n",s.name,s.age,s.b);
输出到屏幕中
fclose(fp);
fp = NULL;
}
注意:这两个函数也是要格式必须对应,且不能更换次序位置
文件随机读写
fseek
文件中会有一个位置指针,我们可以通过更改位置指针来达到随机读写
int fseek ( FILE * stream, long int offset, int origin )
参数1:这是指向 FILE 对象的指针,该 FILE 对象标识了流。
参数2:这是相对 whence 的偏移量,以字节为单位。
参数3:SEEK_CUR-文件指针当前位置、SEEK_END-文件指针末尾的地址、SEEK_SET—文件指针开始位置
返回值:如果成功,则该函数返回零,否则返回非零值。
例1:
已知已经创建了一个文件
如何只打印c,请看代码:
int main()
{
char ch = 0;
FILE* fp = fopen("text.txt","r");
if(fp == NULL)
{
perror("fopen");
return 1;
}
fseek(fp,2,SEEK_SET);
ch = fgetc(fp);
fputc(ch,stdout);
当要读到f时
fseek(fp,-2,SEEK_END); //这里-2说明,edn是在g后面的位置
ch = fgetc(fp);
fputc(ch,stdout);
fclose(fp);
fp = NULL;
}
示例2:更改指定位置的内容
int main()
{
char ch = 0;
FILE* fp = fopen("text.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
fputc('a',fp);
fputc('b',fp);
fputc('c',fp);
fseek(fp,-2,SEEK_CUR);
先往文件中写入 a,b,c
在改变位置指针到b的位置
将b替换为w
fputc('w',fp);
fclose(fp);
fp = NULL;
}
ftell
返回文件指针相对于起始位置的偏移量
long int ftell(FILE *stream)
以刚才的代码为例:
int main()
{
char ch = 0;
long sp = 0;
FILE* fp = fopen("text.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
fputc('a',fp);
fputc('b',fp);
fputc('c',fp);
fseek(fp,-2,SEEK_CUR);
fputc('w',fp);
sp = ftell(fp);
printf("%ld",sp);
fclose(fp);
fp = NULL;
}
rewind
让文件指针的位置回到文件的起始位置
void rewind ( FILE * stream );
继续以上面的代码为例
int main()
{
char ch = 0;
long sp = 0;
FILE* fp = fopen("text.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
fputc('a',fp);
fputc('b',fp);
fputc('c',fp);
fseek(fp,-2,SEEK_CUR);
fputc('w',fp);
sp = ftell(fp);
printf("%ld\n",sp);
rewind(fp);
sp = ftell(fp);
printf("%ld",sp);
fclose(fp);
fp = NULL;
}
文件结束的判定
被错误使用的 feof
牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
- 文本文件读取是否结束,判断返回值是否为EOF (fgetc),或者NULL(fgets)
例如:
fgetc判断是否为EOF.
fgets判断返回值是否为NULL. - 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数。
总结
本章节最重要的就是理解文件指针对文件打开和关闭,打开后一定要判断是否为空指针,关闭后,不要忘了把指针置空
对于fputc、fgetc、fgets、fputs等相关函数,要注意他们的使用方法以及参数和返回值