在计算机编程中,文件操作是非常常见和重要的。无论是在开发桌面应用程序、Web应用程序还是嵌入式系统,文件操作都扮演着重要的角色。通过文件操作,我们可以将数据存储在外部文件中,并在需要时读取或修改这些数据。
文件操作有多种形式,包括文件的打开和关闭、顺序读写、随机读写、文本文件和二进制文件的处理、文件读取结束的标志以及文件缓冲区的使用。通过深入了解文件操作,我们可以更好地利用文件来存储和处理数据,提高程序的性能和效率。
在本篇博客中,我们将详细讨论C语言中文件操作的各个方面,并提供示例代码以帮助读者更好地理解和应用这些概念。无论您是初学者还是有一定经验的开发者,阅读本博客都能帮助您更好地掌握文件操作的技巧和方法。
希望通过本博客的阅读,您能够对文件操作有更深入的了解,并能够灵活运用文件操作来解决实际问题。请继续阅读,让我们一起开始探索C语言的文件操作吧!
个人主页:Oldinjuly的个人主页
文章收录专栏:C语言
欢迎各位点赞收藏⭐关注❤️
目录
1.为什么使用文件
2.什么是文件
2.1 程序文件
2.2 数据文件
2.3 文件名
2.4 相对路径和绝对路径
3.文件的打开和关闭
️3.1 文件指针
️3.2 文件的打开和关闭
4.文件的顺序读写
4.1 字符输入/输出函数
fputc
fgetc
4.2 文本行(字符串)输入/输出函数
fputs
fgets
4.3 格式化输入/输出函数
fprintf
fscanf
4.4 二进制输入/输出函数
fwrite
fread
4.5 三种格式化输入输出函数的对比
5.文件的随机读写
5.1 fssek
5.2 ftell
5.3 rewind
6.文本文件和二进制文件
7.文件读取结束的标志
️7.1 feof函数和ferror函数
8.文件缓冲区
这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
文件指的是磁盘上的文件,一般分为程序文件和数据文件。
程序文件包括源程序文件(.c后缀),目标文件(.obj后缀),可执行程序(.exe后缀)
后面会介绍更多在Linux环境下的程序文件,这和后面学的编译链接有关系。
以前处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显 示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理
的就是磁盘上文件。
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
文件路径是用于定位文件在文件系统中的位置的字符串。它可以是相对路径或绝对路径。
相对路径是相对于当前工作目录的路径。当前工作目录是指在执行程序或命令时所处的目录。相对路径不包含完整的路径信息,而是基于当前工作目录来指定文件的位置。例如,假设当前工作目录是"/home/user/",则相对路径"documents/file.txt"指的是"/home/user/documents/file.txt"这个文件。
绝对路径是一个完整的文件路径,从文件系统的根目录开始直到文件本身的路径。它包含所有必要的路径信息,不依赖于当前工作目录。例如,"/home/user/documents/file.txt"就是一个绝对路径。
缓冲文件系统中,最重要的概念就是文件类型指针,简称文件指针。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE。
例如,VS2013编译环境提供的 stdio.h 头文件中有以下的文件类型申明:
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*的指针变量:
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
在文件读写之前应该先打开文件,在使用结束之后应该关闭文件。
步骤是:打开文件-->读写文件-->关闭文件
在C语言中我们使用fopen打开文件,fclose关闭文件。
FILE * fopen ( const char * filename, const char * mode );
int fclose ( FILE * stream );
- fopen函数的返回值是FILE*,在我们写程序的时候,打开文件的同时都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
- fopen的参数:filename是打开的文件名称,mode是打开方式,两个参数都是字符串。
- 如果文件打开失败,返回NULL;所以每次打开文件后我们要判断是否为空。
- 打开文件就要关闭文件,fopen和fclose成对使用。
文件的打开方式主要如下:
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
表格中只介绍了常用的打开方式,但表格中读写文件的操作和输入输出数据又有着什么什么样的联系呢?
在此之前先介绍一下操作系统中流的概念(以下是chatGPT给的答案)
在操作系统中,流(stream)是一种用于输入和输出数据的抽象概念。它可以理解为数据从源头流动到目的地的通道。流可以与文件、设备、网络连接等相关联,用于在程序和外部资源之间进行数据传输。
流可以分为输入流(Input Stream)和输出流(Output Stream)两种类型。
- 输入流:输入流用于从外部资源读取数据到程序中。它提供了从源头读取数据的方法和功能。例如,从文件中读取数据,从键盘接收用户输入等。输入流提供了读取数据的操作,如读取一个字节、读取一行、读取一定数量的数据等。
- 输出流:输出流用于将程序中的数据写入到外部资源中。它提供了将数据发送到目的地的方法和功能。例如,将数据写入文件,将数据发送到网络连接等。输出流提供了写入数据的操作,如写入一个字节、写入一行、写入一定数量的数据等。
流提供了一种方便的方式来处理不同类型的数据传输,无论是文件、设备还是网络连接。通过使用流,程序可以将数据从源头读取到程序中,或者将程序中的数据写入到目的地。流还提供了缓冲、过滤等功能,能够提高数据传输的效率和灵活性。
在编程中,根据不同的编程语言和操作系统,流的实现方式可能会有所不同。常见的流包括标准输入流(stdin)、标准输出流(stdout)和标准错误流(stderr),它们在很多编程语言中都提供了默认的实现。此外,也可以通过编程语言提供的API来创建和操作自定义的流对象。
从ai给的概念中,我们进行总结和补充:
- 流是用于程序和外部资源进行数据传输的通道。
- 输入流用于从外部资源读取数据到程序中。输出流用于将程序中的数据写入到外部资源中。(输入输出都是相对程序而言,不是文件!)
- 读取数据和写入数据可以:读取(写入)一个字节、读取(写入)一行、读取(写入)一定数量的数据。
- 外部资源可以是文件,设备(键盘,显示屏)
- 文件指针也是一种流,是用于程序和文件进行数据传输的流。
- 标准输出流(stdout)其实与显示器相关,标准输入流(stdin)其实与键盘相关。(C程序一旦启动,就默认打开这两个流。这两个流包含在stdio.h头文件中)
一个图解释如下:
回归正题,示例代码展示一下fopen和fclose的使用:
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (!pf)
{
perror("fopen");
return 1;
}
//关闭文件
fclose(pf);
return 0;
}
示例代码中的文件路径是相对路径(在当前工作目录下打开文件),“w”打开方式,如果文件不存在,则建立文件。
可以看出我们成功在工作目录下建立了文件。
int fgetc ( FILE * stream );
int fputc ( int character, FILE * stream );
- 这里的参数stream就是流,可以是文件指针,也可以是标准输入输出流(stdin和stdout)。
- 这两个函数适用于所有输入输出流。
- 这两个函数只针对字符的读写。
两个函数的使用实例
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件,输出
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
//写到显示屏,输出
int i = 0;
for (i = 0; i < 26; i++)
{
fputc('a'+i, stdout);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
int main()
{
//打开文件
FILE* pf = fopen("data.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);
//读操作,从键盘中读,输入
ch = fgetc(stdin);
printf("%c\n", ch);
ch = fgetc(stdin);
printf("%c\n", ch);
ch = fgetc(stdin);
printf("%c\n", ch);
ch = fgetc(stdin);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
char * fgets ( char * str, int num, FILE * stream );
int fputs ( const char * str, FILE * stream );
- 这里的参数stream就是流,可以是文件指针,也可以是标准输入输出流(stdin和stdout)。
- 这两个函数适用于所有输入输出流。
- 这两个函数只针对字符串的读写。
- fgets函数的num参数表示读取多少个字符,‘\0’占一个字符。
两个函数的使用实例:
int main()
{
FILE* pf = fopen("data.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件 - 写一行,输出
fputs("hello bit ", pf);
fputs("hello xiaobite\n", pf);
//写到屏幕上,输出
fputs("hello bit ", stdout);
fputs("hello xiaobite\n", stdout);
fclose(pf);
pf = NULL;
return 0;
}
int main()
{
FILE* pf = fopen("data.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件 - 读一行。输入
char arr[100] = { 0 };
fgets(arr, 11, pf);
printf("%s\n", arr);
fgets(arr, 15, pf);
printf("%s\n", arr);
//从键盘读,输入
fgets(arr, 100, stdin);
printf("%s\n", arr);
fclose(pf);
pf = NULL;
return 0;
}
int fprintf ( FILE * stream, const char * format, ... );
int fscanf ( FILE * stream, const char * format, ... );
- 所谓格式化,就是类似printf和scanf函数的格式化,const char * format, ...就是格式化列表。
- 这里的参数stream就是流,可以是文件指针,也可以是标准输入输出流(stdin和stdout)。stdin和stdout做参数时,就是printf和scanf函数的功能,所以fprintf和fscanf函数的功能更加广泛。
- 这两个函数适用于所有输入输出流。
- 这两个函数针对所有类型的读写。
两个函数的使用实例
struct S
{
int a;
float s;
};
int main()
{
FILE* pf = fopen("data.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件,输出
struct S s = { 100, 3.14f };
fprintf(pf, "%d %f", s.a, s.s);
//写到屏幕上,输出
fprintf(stdout, "%d %f", s.a, s.s);
fclose(pf);
pf = NULL;
return 0;
}
struct S
{
int a;
float s;
};
int main()
{
FILE* pf = fopen("data.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件,输入
struct S s1 = {0};
struct S s2 = { 0 };
//从文件中读
fscanf(pf, "%d %f", &(s1.a), &(s1.s));
//从键盘中读
fscanf(stdin, "%d %f", &(s2.a), &(s2.s));
//写到屏幕上,输出
fprintf(stdout, "%d %f\n", s1.a, s1.s);
fprintf(stdout, "%d %f\n", s2.a, s2.s);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
至此,以上的函数都是操作文本文件的函数,这里介绍的是二进制文件的操作函数。关于二进制文件,后面介绍。
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );。
- 注意:这里的参数stream就是流,只是针对文件流。
- 这两个函数只适用于文件的输入输出流。
- 这里读写的数据都是二进制数,用户是看不到的。
- 这里是针对二进制文件的读写,文件的打开方式要用wb和rb。
- 参数ptr是程序中读写数据的指针,size是数据的大小(单位字节),count是读写的数据个数。
struct S
{
int a;
float s;
char str[10];
};
int main()
{
struct S s = { 99, 6.18f, "bit" };
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fwrite(&s, sizeof(struct S), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
我们发现这里的数据是乱码,我们看不见。
struct S
{
int a;
float s;
char str[10];
};
int main()
{
struct S s = { 0 };
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
fread(&s, sizeof(struct S), 1, pf);
printf("%d %f %s\n", s.a, s.s, s.str);
fclose(pf);
pf = NULL;
return 0;
}
int sprintf ( char * str, const char * format, ... );
int sscanf ( const char * s, const char * format, ...);
参数char*就是进行数据转换的字符串。
下面只需展示sscanf和sprintf函数的使用即可
struct S
{
int a;
float s;
char str[10];
};
int main()
{
char arr[30] = { 0 };
struct S s = { 100, 3.14f, "hehe" };
struct S tmp = {0};
sprintf(arr, "%d %f %s", s.a, s.s, s.str);
printf("%s\n", arr);
sscanf(arr, "%d %f %s", &(tmp.a), &(tmp.s), tmp.str);
printf("%d %f %s\n", tmp.a, tmp.s, tmp.str);
return 0;
}
上面的读写函数都是顺序读写,下面介绍文件的随机读写,这里就涉及了文件指针的偏移操作。
针对文件指针,进行以下补充:
- 文件指针一开始默认指向文件的开头位置。
- 每次从文件中读取一次数据,文件指针都会向后偏移。
根据参考位置和偏移量来定位文件指针。
int fseek ( FILE * stream, long int offset, int origin );
stream:一个指向FILE结构的指针,表示要设置文件指针的文件流。
offset:表示要偏移的字节数,可以是正数或负数。
origin:表示移动的参考位置,可以取以下三个值:
- SEEK_SET:从文件开头开始偏移 offset 个字节。
- SEEK_CUR:从当前文件指针位置开始偏移 offset 个字节。
- SEEK_END:从文件末尾开始偏移 offset 个字节。
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
//定位文件指针到f
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
ch = fgetc(pf);
printf("%c\n", ch);//c
fseek(pf, -3, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
返回文件指针相对起始位置的偏移量。
long int ftell ( FILE * stream );
让文件指针回到文件的起始位置。
void rewind ( FILE * stream );
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而
二进制形式输出,则在磁盘上只占4个字节。
ASCII形式存储,首先把对应十进制数转化成字符,然后存储对应字符ASCII码值的二进制。
在文件读取的过程中,不能用feof函数的返回值来直接判断文件是否读取结束。
feof函数的作用是:当文件读取结束时,判断读取结束的原因是否是:遇到文件尾结束。
总的来说,feof函数是判断文件读取结束原因的函数,返回真说明是遇到文件尾读取结束的。而ferror函数判断文件读取结束是否是因为发生了错误而结束的,返回真说明文件读取过程中发生了错误。
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
#include
#include
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区,没有写入文件中
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。
感谢观看,望三连
相关函数的cplusplus链接:
fopenhttps://legacy.cplusplus.com/reference/cstdio/fopen/?kw=fopen
fclosehttps://legacy.cplusplus.com/reference/cstdio/fclose/?kw=fclose
fgetchttps://legacy.cplusplus.com/reference/cstdio/fgetc/?kw=fgetc
fputchttps://legacy.cplusplus.com/reference/cstdio/fputc/?kw=fputc
fgetshttps://legacy.cplusplus.com/reference/cstdio/fgets/?kw=fgets
fputshttps://legacy.cplusplus.com/reference/cstdio/fputs/?kw=fputs
fprintfhttps://legacy.cplusplus.com/reference/cstdio/fprintf/?kw=fprintf
fscanfhttps://legacy.cplusplus.com/reference/cstdio/fscanf/?kw=fscanf
fwritehttps://legacy.cplusplus.com/reference/cstdio/fwrite/?kw=fwrite
freadhttps://legacy.cplusplus.com/reference/cstdio/fread/?kw=fread
fseekhttps://legacy.cplusplus.com/reference/cstdio/fseek/?kw=fseek
ftellhttps://legacy.cplusplus.com/reference/cstdio/ftell/?kw=ftell
rewindhttps://legacy.cplusplus.com/reference/cstdio/rewind/?kw=rewind
feofhttps://legacy.cplusplus.com/reference/cstdio/feof/?kw=feof
其他博客链接
指针进阶https://blog.csdn.net/qq_63981383/article/details/131648902?spm=1001.2014.3001.5502字符串函数https://blog.csdn.net/qq_63981383/article/details/131681078?spm=1001.2014.3001.5502C语言自定义类型https://blog.csdn.net/qq_63981383/article/details/131693302?spm=1001.2014.3001.5502动态内存管理https://blog.csdn.net/qq_63981383/article/details/131725015?spm=1001.2014.3001.5502