C文件操作1
一、为什么使用文件
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失 了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。
二、什么是文件
磁盘(硬盘)上的文件是文件。 但是在程序设计中,我们⼀般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)
2.1 程序文件
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows 环境后缀为.exe)
2.2 数据文件
文件的内容不⼀定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。
在以前各章所处理数据的输⼊输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。 其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
2.3 文件名
⼀个文件要有⼀个唯⼀的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名
三、文件的打开和关闭
3.1 流和标准流
3.1.1 流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流(stream)的概念,我们可以把 流 想象成 流淌着字符的河。
C程序针对文本件、画面、键盘等的数据输入输出操作都是通过流操作的。⼀般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
3.1.2 标准流
那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢? 那是因为C语言程序在启动的时候,默认打开了3个流:
• stdin - 标准输入流(键盘),在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
• stdout - 标准输出流(屏幕),大多数的环境中输出至显示器界⾯,printf函数就是将信息输出到标准输出 流中。
• stderr - 标准错误流(屏幕),大多数环境中输出到显示器界⾯。
这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。
stdin、stdout、stderr 三个流的类型是: FILE * ,通常称为文件指针。
C语⾔中,就是通过 FILE* 的文件指针来维护流的各种操作的。
3.2 文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了⼀个相应的文件信息区,用来存放文件的相关信息(如文件的名 字,文件状态及文件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名FILE。
每当打开⼀个文件的时候,系统会根据文件的情况自动创建⼀个FILE结构的变量,并填充其中的信息。
⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下⾯我们可以创建⼀个 FILE* 的指针变量:
FILE* pf;//文件指针变量
定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是⼀个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件。
3.3 文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回⼀个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSI C 规定使用 fopen 函数来打开文件, fclose 来关闭文件。
它们需要包含头文件 #include <stdio.h>
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );
【fclose 如果流成功关闭,则返回零值。 若关闭失败,则返回 EOF。】
mode表示文件的打开模式,下面都是文件的打开模式:
---------------------------------------------------------------------------------------------------------------------------------
文件使用方式 含义 如果指定文件不存在
“r”(只读) 为了输入数据,打开⼀个已经存在的文本文件 出错
“w”(只写) 为了输出数据,打开⼀个文本文件 建立⼀个新的文件
“a”(追加) 向文本文件尾添加数据 建立⼀个新的文件
“rb”(只读) 为了输入数据,打开⼀个⼆进制文件 出错
“wb”(只写) 为了输出数据,打开⼀个⼆进制文件 建立⼀个新的文件
“ab”(追加) 向⼀个⼆进制文件尾添加数据 建立⼀个新的文件
“r+”(读写) 为了读和写,打开⼀个文本文件 出错
“w+”(读写) 为了读和写,建议⼀个新的文件 建立⼀个新的文件
“a+”(读写) 打开⼀个文件,在文件尾进行读写 建立⼀个新的文件
“rb+”(读写) 为了读和写打开⼀个⼆进制文件 出错
“wb+”(读写) 为了读和写,新建⼀个新的⼆进制文件 建立⼀个新的文件
“ab+”(读写) 打开⼀个⼆进制文件,在文件尾进行读和写 建立⼀个新的文件
---------------------------------------------------------------------------------------------------------------------------------
例如:当我们想读取此文件路径 D: \VS2022 c语言 \code \ 下的 test.txt
要想正确读取,我们首先是要创建文件路径,而在创建之前,我们应该先勾选一下选项,再进行创建:
#include <stdio.h>
#include <string.h>
int main()
{FILE* pf = fopen("D:\\VS2022 c语言\\code\\test.txt","r");if(pf == NULL)//需要对返回值进行判断{perror("fopen");return 1;}else{printf("打开文件成功\n");}//读文件//……//关闭文件fclose(pf);pf = NULL;return 0;
}
介绍一下什么是绝对路劲和相对路径:
绝对路径:D:\\VS2022 c语言\\code\\test.txt
相对路劲:test.txt(指的是当前目录下的 test.txt)
如果我们想以相对路径的方式进行打开文件操作,那我们要先找到当前的目录,然后将之前的文件test.txt剪切到当前目录下:
FILE* pf = fopen("test.txt","r");
除了以上的方式,想表示当前目录,还可以用 "./" 表示:
FILE* pf = fopen("./test.txt", "r");
而 "../" 表示上级目录下的 text.txt:
将文件test.txt 剪切到上一个目录下:
FILE* pf = fopen("../test.txt", "r");
那么 "../../" 在文件路径中表示 上上级目录(即当前目录的父目录的父目录)
前面我们介绍了文件的12种打开模式,而其中出错会建立一个新的文件是什么意思呢?
现在我们拿 "w" 来进行举例:
"w" - 只写,建⽴⼀个新的文件(覆盖已有文件或创建新文件(仅在路径、权限等条件允许时))
如果目标文件已存在,"w" 模式会清空文件内容(即覆盖原有数据),并将文件指针定位到开头,准备写入新内容。
int main()
{FILE* pf = fopen("test.txt","w");if(pf == NULL){perror("fopen");}else{printf("打开文件成功\n");}//写文件//……//关闭文件fclose(pf);pf = NULL;return 0;
}
我们打开文件进行写入操作:
当程序运行起来时,"w" 模式会清空文件内容(即覆盖原有数据),即建立一个新的文件:
四、文件的顺序读写
4.1 顺序读写函数介绍
这些函数都包含头文件 #include <stdio.h>
------------------------------------------------------------------------------------
函数名 功能 适用于
fgetc 字符串输入函数 所有输入流
fputc 字符串输出函数 所有输出流
fgets 文本行输入函数 所有输入流
fputs 文本行输出函数 所有输出流
fscanf 格式化输入函数 所有输入流
fprintf 格式化输出函数 所有输出流
fread 二进制输入 文本输入流
fwrite 二进制输出 文本输出流
-----------------------------------------------------------------------------------
上⾯说的适用于所有输入流⼀般指适用于标准输入流和其他输入流(如文件输入流);所有输出流⼀ 般指适用于标准输出流和其他输出流(如文件输出流)。
4.1.1 fgetc 和 fputc
fputc
向流写入字符
int fputc ( int character, FILE * stream );
- 将一个字符写入流并推进位置指示器。该字符会被写入流的内部位置指示器所指示的位置,随后该位置指示器会自动向前移动一位。
- 如果操作成功,则返回所写入的字符。如果发生写入错误,则返回 `EOF` 并设置错误指示器(`ferror`)。
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);//关闭文件 fclose(pf);pf = NULL;return 0;
}
注意:要先运行起来后才能看到写入文件的字符(其他的函数都一样)
fgetc
从流中获取字符
int fgetc ( FILE * stream );
- 返回指定流的内部文件位置指示器当前所指向的字符。随后,内部文件位置指示器将前进到下一个字符。如果在调用该函数时流已到达文件末尾,函数将返回 `EOF` 并为该流设置文件结束指示器(`feof`)。如果发生读取错误,函数将返回 `EOF` 并为该流设置错误指示器(`ferror`)。
- 如果操作成功,将返回读取到的字符(提升为 `int` 类型的值)。返回类型为 `int` 是为了能包含特殊值 `EOF`(-1),该值表示操作失败:如果位置指示器位于文件末尾,函数将返回 `EOF` 并设置流的文件结束指示器(`feof`)。如果发生其他读取错误,函数同样返回 `EOF`,但会设置其错误指示器(`ferror`)。
int main()
{FILE* pf = fopen("test.txt","r");if(pf == NULL) {perror("fopen");return 1;}//读文件 int ch = fgetc(pf);printf("%c\n",ch);ch = fgetc(pf);printf("%c\n",ch);ch = fgetc(pf);printf("%c\n",ch);//关闭文件 fclose(pf);pf = NULL;return 0;
}
4.1.2 fgets 和 fputs
fputs
向流写入字符串
int fputs ( const char * str, FILE * stream );
- 将 `str` 所指向的 C 字符串写入流。该函数从指定地址(`str`)开始复制,直到遇到终止空字符 `'\0'` 为止。这个终止空字符不会被复制到流中。
- 如果操作成功,将返回一个非负数值。如果发生错误,函数将返回 `EOF` 并设置错误指示器(`ferror`)。
int main()
{FILE* pf = fopen("test.txt","w");if(pf == NULL) {perror("fopen");return 1;}//写文件//测试写一行数据 fputs("hello world\n",pf);fputs("haha\n",pf);//关闭文件 fclose(pf);pf = NULL;return 0;
}
fgets
从流中获取字符串
char * fgets ( char * str, int num, FILE * stream );
- 从流中读取字符,并将它们作为 C 字符串存储到 `str` 中,直到读取了 `(num - 1)` 个字符,或者遇到换行符或文件结尾,以先发生的情况为准。换行符会使 `fgets` 停止读取,但该函数会将其视为有效字符,并将其包含在复制到 `str` 的字符串中。在复制到 `str` 的字符之后,会自动追加一个终止空字符。
- 如果操作成功,函数将返回 `str`。如果在尝试读取一个字符时遇到文件末尾,则会设置文件结束指示器(`feof`)。如果在读取任何字符之前就遇到这种情况,返回的指针将是一个空指针(并且 `str` 的内容保持不变)。如果发生读取错误,将设置错误指示器(`ferror`),并且也会返回一个空指针(但 `str` 所指向的内容可能已经改变)。
int main()
{FILE* pf = fopen("test.txt","r");if(pf == NULL) {perror("fopen");return 1;}//读文件//测试读一行数据 char buf[20] = {0};fgets(buf,5,pf);printf("%s\n",buf);//hell - 读4个字符,是因为第5个字符需要放'\0'fgets(buf,5,pf);printf("%s\n",buf);//关闭文件 fclose(pf);pf = NULL;return 0;
}
4.1.3 fscanf 和 fprintf
fprintf
向流写入格式化数据
int fprintf ( FILE * stream, const char * format, ... );
- 将 `format` 所指向的 C 字符串写入流。如果 `format` 中包含格式说明符(以 `%` 开头的子序列),则 `format` 后面的额外参数将被格式化,并插入到结果字符串中,以替换各自的格式说明符。在 `format` 参数之后,函数期望至少有 `format` 中指定数量的额外参数。
- 如果操作成功,将返回所写入的字符总数。如果发生写入错误,将设置错误指示器(`ferror`),并返回一个负数。
我们对比printf函数,它们的区别就是fprintf函数是向流写入数据,而print函数只是将数据打印到屏幕上,而 …… 表示可变参数列表
printf可以这样:printf("hehe");、printf("%s","hehe");、printf("%s %d","hehe",100);等可以在添加,那么fprintf就是多了一个FILE*类型的指针变量
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {"zhangsan",20,95.5};FILE* pf = fopen("test.txt","w");if(pf == NULL) {perror("fopen");return 1;}//格式化的写入文件fprintf(pf,"%s %d %f\n",s.name,s.age,s.score);//关闭文件 fclose(pf);pf = NULL;return 0;
}
fscanf
从流中读取格式化数据
int fscanf ( FILE * stream, const char * format, ... );
- 从流中读取数据,并根据 `format` 参数将数据存储到额外参数所指向的位置。额外参数应指向已分配好的对象,这些对象的类型要与格式字符串中相应格式说明符所指定的类型一致。
- 如果操作成功,函数将返回参数列表中成功填充的项数。由于匹配失败、读取错误或到达文件末尾,此计数可能与预期的项数相符,也可能更少(甚至为零)。如果在读取时发生读取错误或到达文件末尾,会设置相应的指示器(`feof` 或 `ferror`)。并且,如果在任何数据都未能成功读取之前就出现上述情况,则返回 `EOF`。
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {0};FILE* pf = fopen("test.txt","r");if(pf == NULL) {perror("fopen");return 1;}//格式化的读取文件fscanf(pf,"%s %d %f",s.name,&(s.age),&(s.score));//打印看数据printf("%s %d %f\n",s.name,s.age,s.score);//zhang san 20 95.500000 //关闭文件 fclose(pf);pf = NULL;return 0;
}
4.1.4 通过标准输入输入流进行操作
前面我们知道,任何一个C语言程序运行的时候,默认打开3个流:stdin、stdout、stderr
上面这些函数适用于所有输出、输入流,因此,我们除了可以通过文件进行输出、输入操作,还可以通过stdin等标准输入、输出流进行操作
例如:
int main()
{int ch = fgetc(stdin);fputc(ch,stdout);return 0;
}
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {0};fscanf(stdin,"%s %d %f",s.name,&(s.age),&(s.score));fprintf(stdout,"%s %d %f\n",s.name,s.age,s.score);return 0;
}
4.1.5 fread 和 fwrite
这两个函数只适用于文件输入、输入流(二进制文件)
fwrite
向流写入数据块
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
- 从 `ptr` 所指向的内存块中,将包含 `count` 个元素(每个元素大小为 `size` 字节)的数组写入流的当前位置。流的位置指示器会按写入的总字节数向前移动。在函数内部,它将 `ptr` 所指向的块视为一个由 `(size * count)` 个 `unsigned char` 类型元素组成的数组,并依次将这些元素写入流,就好像对每个字节都调用了 `fputc` 函数一样。
- 函数将返回成功写入的元素总数。如果该数值与 `count` 参数不同,则说明写入错误导致函数未能完成操作。在这种情况下,将为流设置错误指示器(`ferror`)。如果 `size` 或 `count` 为零,函数将返回零,且错误指示器保持不变。`size_t` 是一种无符号整数类型。
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {"张三",20,95.5};FILE* pf = fopen("test.txt","wb");if(pf == NULL) {perror("fopen");return 1;}//写文件//测试二进制的写函数fwrite(&s,sizeof(struct S),1,pf); //关闭文件 fclose(pf);pf = NULL;return 0;
}
fread
从流中读取数据块
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
- 从流中读取一个包含 `count` 个元素的数组,每个元素大小为 `size` 字节,并将它们存储到 `ptr` 指定的内存块中。流的位置指示器会按读取的总字节数向前移动。如果读取成功,读取的总字节数为 `(size * count)`。
- 函数将返回成功读取的元素总数。如果该数值与 `count` 参数不同,则说明在读取时要么发生了读取错误,要么已到达文件末尾。在这两种情况下,都会设置相应的指示器,可分别使用 `ferror` 和 `feof` 进行检查。如果 `size` 或 `count` 为零,函数将返回零,并且流的状态和 `ptr` 所指向的内容都将保持不变。`size_t` 是一种无符号整数类型。
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {0};FILE* pf = fopen("test.txt","rb");if(pf == NULL) {perror("fopen");return 1;}//读文件//测试二进制的读函数fread(&s,sizeof(struct S),1,pf);printf("%s %d %f\n",s.name,s.age,s.score); //关闭文件 fclose(pf);pf = NULL;return 0;
}
4.2 对比一组函数
scanf/fscanf/sscanf
printf/fprintf/sprintf
首先认识一下sscanf 和 sprintf
sprintf
将格式化数据写入字符串
int sprintf ( char * str, const char * format, ... );
- 构建一个字符串,其内容与使用 `printf` 函数并传入 `format` 参数时所打印的文本相同,但这些内容不会被打印出来,而是作为 C 字符串存储在 `str` 所指向的缓冲区中。缓冲区的大小应足够大,以容纳整个生成的字符串;内容后面会自动追加一个终止空字符。在 `format` 参数之后,函数期望至少有 `format` 所需数量的额外参数。
- 如果操作成功,将返回所写入的字符总数。此计数不包括自动追加在字符串末尾的额外空字符。如果操作失败,将返回一个负数。
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {"zhangsan",20,95.5};char buf[100] = {0};sprintf(buf,"%s %d %f",s.name,s.age,s.score);printf("%s\n",buf);return 0;
}
sscanf
从字符串中读取格式化数据
int sscanf ( const char * s, const char * format, ...);
- 从字符串 `s` 中读取数据,并根据 `format` 参数将数据存储到额外参数所指定的位置,就好像使用了 `scanf` 函数一样,只不过这里是从字符串 `s` 读取数据,而非从标准输入(`stdin`)读取。额外参数应指向已分配好的对象,这些对象的类型要与格式字符串中相应格式说明符所指定的类型一致。
- 如果操作成功,函数将返回参数列表中成功填充的项数。此计数可能与预期的项数相符,也可能在匹配失败的情况下更少(甚至为零)。如果在成功解析任何数据之前就出现输入失败的情况,则返回 `EOF`。
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {"zhangsan",20,95.5};char buf[100] = {0};sprintf(buf,"%s %d %f",s.name,s.age,s.score);printf("%s\n",buf);//按照打印字符串struct S tmp = {0};sscanf(tmp,"%s %d %f",tmp.name,&(tmp.zge),&(tmp.score));printf("%s %d %f\n",tmp.name,tmp.age,tmp.score);//打印结构体数据return 0;
}
总结:
scanf - 从键盘上读取格式化的数据 stdin
printf - 把数据写到(输出)屏幕上 stdout
fscanf - 针对所有输入流的格式化的输入函数:stdin、打开的文件
fprintf - 针对所有输出流的格式化的输出函数:stdout、打开的文件
sscanf - 从一个字符串中,还原出一个格式化的数据
sprintf - 把格式化的数据,存放在(转换成)一个字符串中