(1)文件操作
1.文件操作是什么?
如果没有文件。我们写的程序的数据存储在内存中。如果程序退出、内存回收,数据就丢失了,等再次运行程序是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件操作。
- 内存中的数据一旦退出,数据会丢失!
- 持久化保存数据,而文件放置于硬盘上,可以永久保存
1.1文件是什么?
文件分两类:程序文件、数据文件。磁盘(硬盘)上的文件是文件。
- 程序文件
程序文件包含源程序文件(后缀.c),目标文件(win环境下为.obj),可执行程序(win环境下为.exe) - 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行时需要从中读取数据的文件,或者输出内容的文件。
1.2文件名
一个文件需要有一个唯一的文件标识,便于识别和引用。
文件包含:文件路径+文件名主干+文件后缀
eg:c:\code\test.txt
为了方便起见。文件标识被称为文件名,也就是可以不包含后缀的成分
2.二进制文件和文本文件
根据数据的组织形式,数据文件被称为文本文件/二进制文件
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件
一个数据在内存中如何存储?
字符一律以ASCII码的形式存储,数值型数据可以用ASCII形式存储,也可以使用二进制形式存储。
3.文件的打开和关闭
3.1源和标准流
3.1.1流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出
操作各不相同,为了⽅便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流
想象成流淌着字符的河。
C程序针对⽂件、画⾯、键盘等的数据输⼊输出操作都是通过流操作的。
⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。
3.1.2标准流
那为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语⾔程序在启动的时候,默认打开了3个流:
• stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
• stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出流中。
• stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。
这是默认打开了这3个流,我们使用scanf、printf等函数可以直接进行输入输出操作的,stdin、stdout、stderr类型是:FILE*
通常称为文件指针。
C语言中,就是通过FILE*
的文件指针来维护流的各种操作的。
3.2文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件信息,这些信息保存在一个结构体变量中,该结构体类型是由系统声明的取名FILE
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构变量,并填充其中的信息。
FILE *pf;//创建一个文件指针变量
定义pf是一个指向FILE类型数据的指针变量,可以使pf指向某个文件的文件信息区(是一个结构变量)。通过该文件信息区中的信息就能够访问该文件,也就i是说:通过文件指针变量能够间接找到与它关联的文件
3.3文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。ANSI C规定使用fopen
和 fclose
来打开和关闭文件。
FILE* open(const char* filename,const char* mode);
int fclose(FILE* stream);
b代表二进制的意思
//打开文件,为了写
//如果文件打开失败,会返回一个空指针
FILE* pf = fopen("data.txt", "w");
if (pf == NULL) {
perror("");
return 1;
}
//写文件、关闭文件
fclose(pf);
pf = NULL;
return 0;
这里补充两点:
- . 表示当前目录 2. … 表示上一级路径
//.表示当前目录,..表示上一级路径
FILE* pf = fopen(".//..//data.txt", "w");
4.文件的顺序读写
4.1顺序读写函数:
函数原型:
int fgetc( FILE *stream );
int fputc( int ch, FILE* stream );
使用:
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL) return 1;
//写文件
fputc('a', pf);
fputc('b', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL) return 1;
//读取文件
char ch = fgetc(pf);
printf("%c", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
练习:写一个代码,完成将data1.txt的内容拷贝到data2.txt中
#include<stdio.h>
int main()
{
//从data1.txt->data2.txt
FILE * pfread = fopen("data1.txt", "r");
if (pfread == NULL)
{
perror("");
return 1;
}
FILE* pfwrite =fopen("data2.txt", "w");
if (pfwrite == NULL)
{
fclose(pfread);
pfread = NULL;//关闭第一个文件
perror("");
return 1;
}
//数据的读写和拷贝
int ch = 0;
while ((ch=fgetc(pfread)) != EOF)
{
fputc(ch, pfwrite);
}
//关闭
fclose(pfread);
fclose(pfwrite);
return 0;
}
fputs和fgets
- fgets:
输出:
如果读取成功,函数返回 str 。
如果遇到文件结尾,已读取到部分数据,那么返回 str 。
如果遇到文件结尾,未读取到任何数据,那么返回 NULL 。
如果遇到文件读取错误,返回 NULL 。 str 中有可能有部分已读取数据。
函数原型:
- fputs:
int fputs(const char*str,FILE* stream);
char* fgets(char* str,int num,FILE* stream);
//num表示最多读取num-1个字符
使用:
FILE* pf = fopen("data.txt", "w");
fputs("abcdef", pf);
//写文件,一次写一行
FILE* pf = fopen("data.txt", "r");
char arr[20] = "xxxxxxxxxxxxxx";
fgets(arr, 10, pf);
从键盘中输入并打印在屏幕:
fgets(arr,10,stdin);
fputs(arr,stdout);
fread 和 fwrite
size_t fwrite(const void*ptr,size_t size,size_t count,FILE*stream);
size_t fread(const void*ptr,size_t size,size_t count,FILE*stream);
使用:
struct Stu s = { "zhangsan",20,90.5f };
FILE* pf = fopen("data.txt", "wb");
//写文件
fwrite(&s, sizeof(s), 1, pf);
return 0;
struct Stu s = { "zhangsan",20,90.5f };
FILE* pf = fopen("data.txt", "rb");
//读文件
fread(&s, sizeof(s), 1, pf);
4.2对三组函数
printf 和 fprintf
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { "zhangsan",20,90.5f };
FILE* pf = fopen("data.txt", "w");
//写文件
fprintf(pf, "%s %d %.2f", s.name, s.age, s.score);
return 0;
}//区别就在多了一个参数,多的参数位放stream
scanf 和 fscanf
fscanf(pf,"%s %d %f",s.name,&(s.age),&(s.score));
sscanf和sprintf
- sscanf:从一个字符串中读取一个格式化的数据
- sprintf:把一个格式化的数据转换成字符串
函数原型:
int sscanf ( const char * s, const char * format, ...);
int sprintf ( char * str, const char * format, ... );
struct Stu s = { "lisi",18,75.5 };
char arr[100] = { 0 };
sprintf(arr, "%s %d %.1f", s.name, s.age, s.score);
printf("%s\n", arr);
struct Stu temp = { 0 };
sscanf(arr,"%s %d %f",temp.name,&(temp.age),&(temp.score));
printf("%s %d %.1f", temp.name, temp.age, temp.score);
5.文件状态测试
feof
用于测试是否文件结尾;ferror
用于测试文件是否读写出错
int feof(FILE* stream);//如果文件结尾返回非0否则返回0
int ferror(FILE* stream);//如果文件读写出错返还非0,否则返回0
因此我们可以写一个函数来判断文件出问题的原因分析:
void fileEofOrError(FILE* stream)
{
if (feof(stream))
{
printf("文件结尾");
}
if (ferror(stream))
{
printf("文件读取出错");
}
}
应当注意使用它必须是文件出现错误的时候才使用!
- 对于fgetc函数判断它的返回值是不是EOF
- 对于fgets函数判断它的返回值是否是NULL
- 对于fread函数判断它返回值是否小于读取的数据块数
if(pflie==EOF)
fileEofOrError(pfile);
6.fflush函数
C语言提供的文件操作函数是带有缓存的,数据会先写入到缓存中,待缓存中的数据积累到一定数量时,再一起写入文件。只有将缓存区的数据写入文件,数据才真正保存在了文件中,此时缓存区的数据无需保留将被清空——刷新缓存
而文件关闭 fclose 或程序结束会刷新缓存。所以,关闭文件 fclose 后,文件内出现了内容。除此之外,还可以主动调用 fflush 函数,主动刷新文件缓存。
int fflush(FILE* stream);
刷新缓存区成功返回0,否则返回EOF,并且ferrror可以检测到文件读写出错。
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE* pFile = fopen("data.txt", "w"); // 写模式
if (pFile == NULL)
{
return -1;
}
char str[] = "Have a good time\n";
for(int i = 0 ; i < 5; i ++)
{
fputs(str, pFile);
}
// 刷新文件缓存区后暂停程序
fflush(pFile);
system("pause");
fclose(pFile);
return 0;
}
7.文件偏移
7.1读模式偏移
int main()
{
FILE* pFile = fopen("data.txt", "r");
if (pFile == NULL)
{
return -1;
}
char ch;
while(1)
{
ch = fgetc(pFile);
if (ch == EOF)
{
fileEofOrError(pFile);
break;
}
putchar(ch);
}
fclose(pFile);
return 0;
}
为什么每一次的 fgetc 函数能顺序获取到文件中的字符呢?
文件结构 pFile 中,保存了一个当前文件读写位置的指针。文件由 fopen 函数打开后,这个指针指向文件中第一个字节,当任意文件操作函数读写相应长度的字节后,指针也会偏移相应的长度。
7.2fseek函数
C语言还提供了一个专门的文件指针移动函数 fseek
int fssek(FILE* stream,long offset,int origin);
- FILE * stream 文件结构指针
long offset 文件指针偏移量
origin 从什么位置开始偏移。 - 其中 origin 可以使用以下3种宏定义作为参数:
SEEK_SET
文件开头(文件第一个字节)SEEK_CUR
当前文件位置SEEK_END
文件结尾(文件最后一个字节后)
输出:如果成功,返回0。否则返回一个非零值。并且ferror可以到文件读写出错
例一:从文件开头偏移5字节,文件指针指向‘a’
fseek(pFile, 5, SEEK_SET);
例二:从文件结尾偏移-5个字节,文件指针将指向 'i’
fseek(pfile,-5,SEEK_END);
7.3 ftell函数
ftell
——获取当前文件指针位置
long ftell(FILE* stream);
获取成功,返回当前文件指针位置;获取失败,返回-1。
#include <stdio.h>
int main()
{
FILE* pFile = fopen("data.txt", "r");
if (pFile == NULL)
{
return -1;
}
char ch;
// 偏移到文件结尾
fseek(pFile, 0, SEEK_END);
// 获取当前文件指针位置
long length = ftell(pFile);
printf("size of file %ld\n", length);
fclose(pFile);
return 0;
}
7.4 rewind函数
rewind函数可以将文件指针回到文件最开始的位置
void rewind(FILE* stream);
8.更新文件
如果,我们想将文件中的每个 “Have a good time\n” 的首字母,字符 ‘H’ 改为小写的 ‘h’ 。应该怎样做呢?
那么,好像可以使用 “r” 模式读取文件,但是 “r” 模式不能写入文件。更不能用 “w” 模式了,它会清空原有文件的内容。那么如果文件需要同时读写,应该怎么办呢?
注意:
- “w+“模式:更新模式,可读可写,但是清空原文件内容
- “r+”模式:更新模式,可读可写
显然r+模式更适应大多数的情况。
char ch;
while(1)
{
ch = fgetc(pFile);
if (ch == EOF)
{
fileEofOrError(pFile);
break;
}
if (ch == 'H')
{
// 文件指针向前移动一个字节
fseek(pFile, -1, SEEK_CUR);
ch = fputc('h', pFile);
if (ch == EOF)
{
fileEofOrError(pFile);
break;
}
}
}
对于以更新模式 + 打开的文件,这里有一个必须要注意的地方:
- 文件从写操作转换为读操作前,必须使用 fflush , fseek , rewind 其中一个函数。
- 文件从读操作转换为写操作前,必须使用 fseek , rewind 其中一个函数
在代码中读写操作转换的地方加入必要函数。如果仅需要读写操作转换,但无需变动文件指针。可以在当前位置处偏移0字节。fseek(pFile, 0, SEEK_CUR);
int main()
{
FILE* pFile = fopen("data.txt", "r+");
if (pFile == NULL)
{
return -1;
}
char ch;
while(1)
{
ch = fgetc(pFile);
if (ch == EOF)
{
fileEofOrError(pFile);
break;
}
if (ch == 'H')
{
// 读转写
fseek(pFile, -1, SEEK_CUR);
ch = fputc('h', pFile);
if (ch == EOF)
{
fileEofOrError(pFile);
break;
}
// 写转读
fflush(pFile);
}
}
fclose(pFile);
return 0;
}