一.提前说明
说明:我们对文件操作相信并不陌生,但是由于平时课程的关系,文件操作使用也比较少,知识点并不是很多,但容易忘记,忘记了某一天再使用又要重新学,因此我打算写一篇博客,记录C语言文件操作的知识点,巩固知识点,也以便以后回忆。
我们对文件的概念已经非常熟悉了,比如常见的 Word 文档、txt 文件、源文件等。文件是数据源的一种,最主要的作用是保存数据。
二.C语言文件操作
在操作系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。例如:
- 通常把显示器称为标准输出文件,printf 就是向这个文件输出数据;
- 通常把键盘称为标准输入文件,scanf 就是从这个文件读取数据。
1.常见硬件设备对应的文件
在C语言中,常见硬件设备对应的文件包括:
标准输入、输出和错误设备:
标准输入:stdin(通常是键盘)
标准输出:stdout(通常是屏幕)
错误输出:stderr(通常是屏幕)
硬盘、磁盘等存储设备:
文件:用文件名来表示
硬盘:/dev/hd[a-d][1-16] 或 /dev/sd[a-p][1-16]
网络设备:
网络接口卡:/dev/net/tun
串口设备:
串口设备:/dev/ttyS[0-3] (COM1-COM4)
USB串口设备:/dev/ttyUSB[0-3]
并口设备:
并口设备:/dev/lp[0-2]
2.文件操作流程
操作文件的正确流程为:打开文件 --> 读写(操作)文件 --> 关闭文件。文件在进行读写操作之前要先打开,使用完毕要关闭。
所谓打开文件,就是获取文件的有关信息,例如文件名、文件状态、当前读写位置等,这些信息会被保存到一个 FILE 类型的结构体变量中。关闭文件就是断开与文件之间的联系,释放结构体变量,同时禁止再对该文件进行操作。
在C语言中,文件有多种读写方式,可以一个字符一个字符地读取,也可以读取一整行,还可以读取若干个字节。文件的读写位置也非常灵活,可以从文件开头读取,也可以从中间位置读取。
3.文件流
所有的文件(保存在磁盘)都要载入内存才能处理,所有的数据必须写入文件(磁盘)才不会丢失。数据在文件和内存之间传递的过程叫做文件流,类似水从一个地方流动到另一个地方。数据从文件复制到内存的过程叫做输入流,从内存保存到文件的过程叫做输出流。
文件是数据源的一种,除了文件,还有数据库、网络、键盘等;数据传递到内存也就是保存到C语言的变量(例如整数、字符串、数组、缓冲区等)。我们把数据在数据源和程序(内存)之间传递的过程叫做数据流(Data Stream)。相应的,数据从数据源到程序(内存)的过程叫做输入流(Input Stream),从程序(内存)到数据源的过程叫做输出流(Output Stream)。
输入输出(Input output,IO)是指程序(内存)与外部设备(键盘、显示器、磁盘、其他计算机等)进行交互的操作。几乎所有的程序都有输入与输出操作,如从键盘上读取数据,从本地或网络上的文件读取数据或写入数据等。通过输入和输出操作可以从外界接收信息,或者是把信息传递给外界。
我们可以说,打开文件就是打开了一个流。
三.文件操作三剑客
上面已经知道,文件的操作流程为:打开文件 --> 读写(操作)文件 --> 关闭文件。这部分我们就按照这个流程来解释对应的函数。
1.打开文件
在C语言中,操作文件之前必须先打开文件;所谓“打开文件”,就是让程序和文件建立
连接的过程。
fopen() 是一个 C 语言标准库函数,用于打开文件,并返回一个文件指针。其语法如下:
FILE *fopen(const char *filename, const char *mode);
其中,filename 参数是要打开的文件名,可以包含路径信息;mode 参数是以哪种方式打开文件,常见的有以下模式:
"r":以只读方式打开文件,该文件必须存在。
"w":以写方式打开文件,如果文件不存在则创建新文件,如果文件已经存在则清空文件内容。
"a":以追加方式打开文件,在文件末尾添加数据,如果文件不存在则创建新文件。
"r+":以读写方式打开文件,该文件必须存在。
"w+":以读写方式打开文件,如果文件不存在则创建新文件,如果文件已经存在则清空文件内容。
"a+":以读写方式打开文件,在文件末尾添加数据,如果文件不存在则创建新文件。
如果打开文件成功,则返回一个指向 FILE 类型结构体的指针;否则返回 NULL。
下面是一个打开文件的例子:
FILE *fp; if( (fp=fopen("D:\\demo.txt","rb")) == NULL ){ printf("Fail to open file!\n"); exit(0); //退出程序(结束程序) }
2.关闭文件
fclose()
函数用于关闭一个打开的文件。它的语法如下:
int fclose(FILE *stream);
其中,stream 是指向 FILE 对象的指针。如果成功退出,则返回 0 ,否则返回 EOF。
3.读写文件
在C语言中,读写文件比较灵活,既可以每次读写一个字符,也可以读写一个字符串,甚至是任意字节的数据(数据块)。本节介绍以字符形式读写文件。以字符形式读写文件时,每次可以从文件中读取一个字符,或者向文件中写入一个字符。主要使用两个函数,分别是 fgetc() 和 fputc()。
fgetc() 是 C 语言标准库中的一个函数,用于从指定的文件流读取一个字符。它的语法如下:
int fgetc(FILE *stream);
其中,stream 参数是一个 FILE 类型的指针,指向要读取的文件流。函数返回值为读取的字符(作为 int 类型返回)或者在遇到文件结束符(EOF)时返回 EOF。
由于它每次读取的都是一个字符,所以我们可以通过以下代码逐个字符读取一个文件:
//每次读取一个字节,直到读取完毕 while( (ch=fgetc(fp)) != EOF ){ putchar(ch); } putchar('\n'); //输出换行符
fputc() 函数用于将一个字符写入指定的文件流中。其语法如下:
int fputc(int ch, FILE *stream);
其中,ch 表示要写入的字符,stream 表示文件指针。函数返回值为写入的字符,如果发生错误则返回 EOF(-1)。
例如,以下代码将字符 'a' 写入文件:
#include <stdio.h> int main() { char ch = 'a'; FILE *fp = fopen("file.txt", "w"); if (fp != NULL) { fputc(ch, fp); fclose(fp); } return 0; }
这个程序创建了一个名为 file.txt 的新文件,并向其中写入了字符 'a'。注意,打开文件时使用的模式为 "w",表示写操作。如果文件已存在,则会被截断为零长度,即清空文件内容。
那么我们可以通过以下代码实现逐个字符输入文件:
char ch; printf("Input a string:\n"); //每次从键盘读取一个字符并写入文件 while ( (ch=getchar()) != '\n' ){ fputc(ch,fp); }
4.完整流程实现
#include <stdio.h> int main() { FILE *fp_in, *fp_out; int ch; // 打开输入文件 fp_in = fopen("input.txt", "r"); if (fp_in == NULL) { printf("Failed to open input.txt\n"); return 1; } // 打开输出文件 fp_out = fopen("output.txt", "w"); if (fp_out == NULL) { printf("Failed to open output.txt\n"); fclose(fp_in); return 1; } // 逐个字符地读取输入文件,并将其写入输出文件 while ((ch = fgetc(fp_in)) != EOF) { fputc(ch, fp_out); } // 关闭文件指针 fclose(fp_in); fclose(fp_out); return 0; }
这个程序将打开名为 input.txt 的输入文件和名为 output.txt 的输出文件,然后逐个字符地从输入文件中读取字符,并将其写入输出文件中,最后关闭文件指针。
需要注意的是,在使用 fgetc() 函数和 fputc() 函数时,如果文件操作失败(例如文件无法打开、读写权限不足等),函数会返回相应的错误代码,此时应该及时处理错误并关闭文件指针。
四.其他读写方式
1.以字符串的形式读写
fgets() 是一个用于从标准输入流读取字符串的 C 语言函数。它的用法如下:
char *fgets(char *str, int n, FILE *stream);
其中,参数 str 是一个指向字符数组的指针,用来存储读取到的字符串;参数 n 是要读取的最大字符数(包括最后的空字符);参数 stream 是要读取的文件指针,通常使用 stdin 表示标准输入流。
fgets() 函数会从给定的文件指针读取字符,直到遇到换行符或者读取了 n-1 个字符(包括空字符)。如果成功读取了字符,则将它们存储在 str 所指向的字符数组中,并在末尾添加一个空字符 ‘\0’,以表示字符串的结束。
如果无法读取任何字符(例如已经到达文件末尾),则返回 NULL,否则返回 str 所指向的字符数组的指针。
注意,读取到的字符串会在末尾自动添加 ‘\0’,n 个字符也包括 ‘\0’。也就是说,实际只读取到了 n-1 个字符,如果希望读取 100 个字符,n 的值应该为 101。
例如:
#define N 101 char str[N]; FILE *fp = fopen("D:\\demo.txt", "r"); fgets(str, N, fp);
表示从 D:\demo.txt 中读取 100 个字符,并保存到字符数组 str 中。
需要重点说明的是,在读取到 n-1 个字符之前如果出现了换行,或者读到了文件末尾,则读取结束。这就意味着,不管 n 的值多大,fgets() 最多只能读取一行数据,不能跨行。在C语言中,没有按行读取文件的函数,我们可以借助 fgets(),将 n 的值设置地足够大,每次就可以读取到一行数据。
fputs() 是一个 C 语言标准库函数,用于将字符串写入到文件中。它的函数声明如下:
int fputs(const char *str, FILE *stream);
其中 str 参数是要写入的字符串,stream 参数是要写入的文件指针。如果成功写入,该函数会返回一个非负整数,否则返回 EOF。
fputs() 函数会将给定的字符串作为一个整体写入到文件中,不包括字符串结尾处的空字符 ‘\0’。与 fprintf() 和 printf() 不同,fputs() 不会自动在输出的字符串末尾添加换行符。
这里要想写入的字符串是从文本末尾开始,就得注意你的文件打开方式。
例如这里以追加写的方式,读取一个文件并编写:
#include<stdio.h> int main(){ FILE *fp; char str[102] = {0}, strTemp[100]; if( (fp=fopen("D:\\demo.txt", "at+")) == NULL ){ puts("Fail to open file!"); exit(0); } printf("Input a string:"); gets(strTemp); strcat(str, "\n"); strcat(str, strTemp); fputs(str, fp); fclose(fp); return 0; }
2.以数据块的形式读写
fgets() 有局限性,每次最多只能从文件中读取一行内容,因为 fgets() 遇到换行符就结束读取。如果希望读取多行内容,需要使用 fread() 函数;相应地写入函数为 fwrite()。
fread() 是 C 语言标准库中的一个函数,用于从文件中读取数据。它的函数声明如下:
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
其中各个参数的含义如下:
ptr:指向要读取数据的缓冲区。
size:每个数据块的字节数。
count:要读取的数据块数量。
stream:指向要读取的文件或流。
该函数会尝试从指定的文件中读取 count 个数据块,每个数据块的大小为 size。读取到的数据将存储到缓冲区 ptr 中。成功读取的数据块数量将作为函数返回值返回,如果出现错误或到达了文件末尾,则返回值可能小于 count。
需要特别注意的是,在使用 fread() 函数时,应该确保缓冲区大小足够容纳要读取的数据,以避免发生缓冲区溢出等问题。
fwrite() 是 C 语言标准库中的一个函数,用于向文件中写入数据。它的函数声明如下:
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
其中各个参数的含义如下:
ptr:指向要写入数据的缓冲区。
size:每个数据块的字节数。
count:要写入的数据块数量。
stream:指向要写入的文件或流。
该函数会尝试向指定的文件中写入 count 个数据块,每个数据块的大小为 size 字节。待写入的数据存储在缓冲区 ptr 中。成功写入的数据块数量将作为函数返回值返回,如果出现错误,则返回值可能小于 count。
需要特别注意的是,在使用 fwrite() 函数时,应该确保缓冲区大小足够容纳要写入的数据,以避免发生缓冲区溢出等问题。
以下是 fread() 和 fwrite() 函数读写文本文件的一个示例:
#include <stdio.h> #include <stdlib.h> int main() { FILE *fp = fopen("file.txt", "w"); if (fp == NULL) { perror("Failed to open file"); return -1; } const char *str = "Hello, world!"; fwrite(str, sizeof(char), strlen(str), fp); fclose(fp); fp = fopen("file.txt", "r"); if (fp == NULL) { perror("Failed to open file"); return -1; } fseek(fp, 0L, SEEK_END); long int size = ftell(fp); rewind(fp); char *buffer = malloc(size + 1); fread(buffer, sizeof(char), size, fp); buffer[size] = '\0'; printf("%s\n", buffer); free(buffer); fclose(fp); return 0; }
在这个示例中,我们首先以只写模式打开名为 file.txt 的文本文件,并使用 fwrite() 函数将字符串 “Hello, world!” 写入到文件中。然后,我们再次打开同一个文件,并使用 fread() 函数读取文件内容。最后,我们输出读取到的文件内容并释放缓冲区,关闭文件句柄。
3.格式化读写文件
fscanf()和fprintf()函数是C语言中常用的文件格式化读写函数。
fprintf()函数可以将特定格式的数据写入文件中。它的原型为:
Copy Codeint fprintf(FILE *stream, const char *format, ...)
其中,第一个参数是指向要写入的文件的指针,第二个参数是格式化字符串,后面可以跟上要写入的数据。
例如,下面的代码将字符串和整数分别以特定格式写入文件中:
#include <stdio.h> int main() { FILE *fp = fopen("example.txt", "w"); char str[] = "Hello World"; int num = 1234; fprintf(fp, "String: %s, Number: %d\n", str, num); fclose(fp); return 0; }
而fscanf()函数则可以从文件中读取特定格式的数据。它的原型为:
int fscanf(FILE *stream, const char *format, ...)
其中,第一个参数是指向要读取的文件的指针,第二个参数是格式化字符串,后面可以跟上要读取的变量名。
例如,下面的代码从文件中读取字符串和整数,并按照特定格式进行解析:
#include <stdio.h> int main() { FILE *fp = fopen("example.txt", "r"); char str[20]; int num; fscanf(fp, "String: %s, Number: %d", str, &num); printf("String: %s, Number: %d\n", str, num); fclose(fp); return 0; }
需要注意的是,使用fscanf()和fprintf()函数进行文件格式化读写时,需要保证读写数据的格式和数量与指定的格式字符串一致,否则可能会导致读写错误。
4.随机读写
前面介绍的文件读写函数都是顺序读写,即读写文件只能从头开始,依次读写各个数据。但在实际开发中经常需要读写文件的中间部分,要解决这个问题,就得先移动文件内部的位置指针,再进行读写。这种读写方式称为随机读写,也就是说从文件的任意位置开始读写。
移动文件内部位置指针的函数主要有两个,即 rewind() 和 fseek()。
rewind() 用来将位置指针移动到文件开头,前面已经多次使用过,它的原型为:
void rewind ( FILE *fp );
fseek() 用来将位置指针移动到任意位置,它的原型为:
int fseek ( FILE *fp, long offset, int origin );c
参数说明:
fp 为文件指针,也就是被移动的文件。
offset 为偏移量,也就是要移动的字节数。之所以为 long 类型,是希望移动的范围更大,能处理的文件更大。offset 为正时,向后移动;offset 为负时,向前移动。
origin 为起始位置,也就是从何处开始计算偏移量。C语言规定的起始位置有三种,分别为文件开头、当前位置和文件末尾,每个位置都用对应的常量来表示:
起始点 | 常量名 | 常量值 |
文件开头 | SEEK_SET | 0 |
当前位置 | SEEK_CUR | 1 |
文件末尾 | SEEK_END | 2 |
在移动位置指针之后,就可以用前面介绍的任何一种读写函数进行读写了。
五.补充知识点
1.文本文件与二进制文件
文本文件和二进制文件是两种不同类型的文件,它们之间的主要区别在于数据的存储方式和处理方式。
文本文件是一种只包含文本数据的文件,其中的每个字符都按照某种编码格式(如 ASCII 或 UTF-8)被存储为一个字节序列。文本文件通常用于存储简单的文本信息,例如程序源代码、配置文件、日志文件等。由于文本文件中只包含文本数据,因此可以通过普通的文本编辑器打开和编辑。
二进制文件则是一种包含非文本数据的文件,其中的数据可以是任何形式的二进制数据,包括图像、音频、视频、压缩文件等。二进制文件通常采用特定的格式来存储数据,这些格式往往是由厂商或标准委员会定义的。由于二进制文件中的数据通常不是以文本形式存储的,因此无法使用普通的文本编辑器直接打开和编辑。
由于文本文件和二进制文件的存储方式和处理方式不同,因此在进行文件读写操作时需要使用不同的函数和方法。例如,对于文本文件,可以使用 C 语言中的 fputc() 和 fgets() 等函数进行读写;而对于二进制文件,则需要使用 fread() 和 fwrite() 等函数进行读写。
2.EOF说明
EOF 本来表示文件末尾,意味着读取结束,但是很多函数在读取出错时也返回 EOF,那么当返回 EOF 时,到底是文件读取完毕了还是读取出错了?我们可以借助 stdio.h 中的两个函数来判断,分别是 feof() 和 ferror()。
feof() 函数用来判断文件内部指针是否指向了文件末尾,它的原型是:
int feof ( FILE * fp );
当指向文件末尾时返回非零值,否则返回零值。
ferror() 函数用来判断文件操作是否出错,它的原型是:
int ferror ( FILE *fp );
出错时返回非零值,否则返回零值。
3. strcat()函数
strcat() 是一个 C 语言字符串库函数,用于将两个字符串拼接在一起。它的函数声明如下:
char *strcat(char *dest, const char *src);
其中 dest 参数是目标字符串,也就是要将源字符串 src 添加到其后面的字符串。注意,目标字符串 dest 必须具有足够的空间来容纳源字符串 src。
strcat() 函数会将源字符串 src 中的字符依次添加到目标字符串 dest 的末尾,并在最后一个字符之后添加一个空字符 ‘\0’。如果成功拼接了两个字符串,则返回指向目标字符串 dest 的指针。
4. fread() 和 fwrite() 补充
注意看这个两个函数的语法声明,所以我们不能用int类型变量接收它的返回值。size_t 是在 stdio.h 和 stdlib.h 头文件中使用 typedef 定义的数据类型,表示无符号整数,也即非负数,常用来表示数量。
返回值:返回成功读写的块数,也即 count。如果返回值小于 count:
对于 fwrite() 来说,肯定发生了写入错误,可以用 ferror() 函数检测。
对于 fread() 来说,可能读到了文件末尾,可能发生了错误,可以用 ferror() 或 feof() 检测。
5.获取文件长度
ftell() 函数用来获取文件内部指针(位置指针)距离文件开头的字节数,它的原型为:
long int ftell ( FILE * fp );
注意:fp 要以二进制方式打开,如果以文本方式打开,函数的返回值可能没有意义。
先使用 fseek() 将文件内部指针定位到文件末尾,再使用 ftell() 返回内部指针距离文件开头的字节数,这个返回值就等于文件的大小。请看下面的代码:
long fsize(FILE *fp){ fseek(fp, 0, SEEK_END); return ftell(fp); }
六.总结
相信大家也知道C语言的底层性,相对于其他语言来说,C语言的许多功能需要自定义函数实现,这里我们学习文件操作后,可以自定义封装函数,实现文件复制、插入、删除、更改文件内容等操作。