1.为什么使用文件
在C语言中,文件操作是非常重要的,因为它允许程序与外部文件进行交互。
文件是储存在硬盘中的,也就是说即使程序终止,数据仍然存在。
通过文件操作,可以实现读取文件中的数据,将数据写入文件,创建新文件,删除文件,以及在文件中移动位置等等。
文件为同时程序提供了灵活性和功能性,它使程序可以处理各种类型的数据,如文本文件、图像文件、音频文件等。这些文件也可以供多个程序可以共享。例如,多个程序可以读取同一个配置文件,也可以将数据写入同一个日志文件。
2.文件是什么
文件是计算机系统中存储数据的基本单元。
在程序设计中,我们一般谈的文件有两种:程序文件、数据文件:
程序文件:
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀 为.exe)。
数据文件:
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内 容的文件。
这篇博客主要讨论的是数据文件,后面将介绍用C语言对数据文件的一些操作。
每个文件都有一个唯一的文件名用来标识它,
文件名通常由文件名前缀和文件扩展名组成。文件名前缀是文件名的主体部分,可以包含任何字符,但通常由字母、数字和下划线组成。文件扩展名是文件名后缀,通常由一个或多个点号后跟一个或多个字符组成,用于指示文件类型。例如,myfile.txt
是一个文本文件,image.jpg
是一个图像文件。
要注意,同一路径下文件名是唯一的,这是我们后面根据路径精准访问文件的基础。
3.文件的打开与关闭
3.1文件指针
C语言的文件指针是一种特殊的指针,它指向文件中的某个位置,并且可以在文件中移动,以便对文件进行各种操作,例如打开、关闭、读取、写入、定位等。
在C语言中,文件指针是通过 FILE
结构体类型来表示的。每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态和文件当前的位置等)。
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。例如,这是Visual Studio中对文件类型的声明:
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,下面我们可以创建一个FILE*的指针变量,它指向系统刚向我们创建的文件信息区(一个结构体变量)的起始位置:
FILE* pf;//文件指针变量
对于同时打开多个文件的情况,一般建议使用多个文件指针来维护多个文件信息区。
3.2文件的打开与关闭
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。
//打开文件
FILE* fopen(const char* filename, const char* mode);
//关闭文件
int fclose(FILE* stream);
其中,对于fopen中的mode,意思是文件的打开方式,大致有以下几种:
(cplusplus.com)
文件打开方式 | 含义 | 若指定文件不存在 |
---|---|---|
"r"只读 | 为了输入数据,打开一个已经存在的文本文件 | ERROR |
"w"只写 | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
"a"追加 | 向文本文件尾添加数据 | 建立一个新的文件 |
"rb"只读 | 为了输入数据,打开一个二进制文件 | ERROR |
"wb"只写 | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
"ad"追加 | 向一个二进制文件尾添加数据 | 建立一个新的文件 |
"r+"读写 | 为了读和写,打开一个文本文件 | ERROR |
"w+"读写 | 为了读和写,建一个新的文件 | 建立一个新的文件 |
"a+"读写 | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
"rb+"读写 | 为了读和写打开一个二进制文件 | ERROR |
"wb+"读写 | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
"ab+"读写 | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
读写文件时,需要:
1.打开文件
2.读写文件
3.关闭文件
那为什么我们在使用scanf,printf的时候似乎不用自己打开文件就能进行操作呢?
对于一个运行中的C语言程序,会默认打开3个流:
1.标准输入流 stdin
2.标准输出流 stdout
3.标准错误流 stderr
它们的类型都是 FILE*
当使用 scanf 函数时,它会从标准输入流读取数据。当使用 printf 函数时,它会将数据写入标准输出流。这些函数是C语言标准库提供的常用函数,简化了程序员对输入输出的操作。底层实现中,它们已经处理了与标准输入输出流相关的文件打开、读取和写入等操作,使得程序员在使用时无需关心文件的具体操作细节。
所以不是在用scanf,printf的时候没有打开文件,而是系统为我们自动打开了,我们没有发觉。
但是,如果我们要从文件读取数据或将数据写入文件,就需要显式打开文件。
4.文件的顺序读写
文件顺序读是指从文件开头开始,依次读取文件中的每个字节;文件顺序写是指从文件开头开始,依次写入文件中的每个字节。
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输出流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输出流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输出流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
4.1函数原型
fgetc
int fgetc ( FILE * stream );
fputc
int fputc ( int character, FILE * stream );
fgets
char * fgets ( char * str, int num, FILE * stream );
需要注意,该函数当读到num-1个字符时停止读取,或提前读到换行符也不再读取了
fputs
int fputs ( const char * str, FILE * stream );
fscanf
int fscanf ( FILE * stream, const char * format, ... );
fprintf
int fscanf ( FILE * stream, const char * format, ... );
fread
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
fwrite
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
使用样例:
写文件:
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
{
if (pf == NULL)
{
perror("fopen");
return 1;
}
}
for (int i = 'a'; i < 'z'; i++)
{
fputc(i, pf);
}
fclose(pf);
pf = NULL;
return 0;
}
读文件 :
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
{
if (pf == NULL)
{
perror("fopen");
return 1;
}
}
for (int i = 'a'; i < 'z'; i++)
{
int ch = fgetc(pf);
printf("%c ", ch);
}
fclose(pf);
pf = NULL;
return 0;
}
4.2几组函数对比
1. `scanf` & `printf`
`scanf`: 从标准输入流读取格式化的数据
`printf`: 向标准输出流写格式化的数据
2. `fscanf` & `fprintf`
`fscanf`: 适用于所有输入流的格式化输入函数
`fprintf`: 适用于所有输出流的格式化输出函数
3. `sscanf` & `sprintf`
`sscanf`: 从字符串中读取格式化的数据
`sprintf`: 将格式化的数据转化为字符串
5.文件的随机读写
随机读写文件通常通过fseek()
和ftell()
函数来实现。这些函数允许你在文件中移动指针,从而实现随机读写的功能。
5.1fseek介绍
函数原型:
int fseek ( FILE * stream, long int offset, int origin );
其中,stream是文件指针,offset是偏移量,origin是offset的参考,一共有三种:
SEEK_SET : 文件的起始位置
SEEK_CUR : 文件指针的当前的位置
SEEK_END : 文件的结束位置
示例:
假设在项目路径下有data.txt的文本文档,内容如下:
如果我们要让文件指针中指向文档内容的指针移动到 'f' 的位置,可以用fseek来实现:
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//定位文件指针到'f'
fseek(pf, 5, SEEK_SET); //从文件开始的位置,定位到'f',偏移量是5
char c = fgetc(pf);
printf("%d\n", c);
fclose(pf);
pf = NULL;
return 0;
}
5.2ftell介绍
每次读完文档中的某个数据时,文件指针中指向文档内容的指针都会移到下一个位置,为了知道这个位置的偏移量,进行其他随机读写,我们可以用ftell函数来获得偏移量
函数原型:
long int ftell ( FILE * stream );
5.3rewind介绍
void rewind ( FILE * stream );
作用:将文件指针中指向文档内容的指针指向文件开始的位置(偏移量为0)
6.文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或二进制文件。
数据在内存中以二进制形式储存,如果不加转换地输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式输出到键盘的,则需要在储存前加以转换。以ASCII码形式储存的文件就是文本文件。
如果有整数10000,如果以ASCII的形式输出到磁盘,则在磁盘中占用5个字节,而二进制输出,只占4个字节。
7.文件读取结束的判断
文本文件读取是否结束,判断函数返回值就可以了,判断返回值是否为EOF (fgetc),或者NULL(fgets)
7.1文本文件示例
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c;
FILE* fp = fopen("test.txt", "r");
if(fp == NULL)
{
perror("fopen");
return 1;
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
fp = NULL;
return 0;
}
7.2二进制文件示例
#include<stdio.h>
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = {1.0,2.0,3.0,4.0,5.0};
double b = 0.0;
size_t ret_code = 0;
FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
fwrite(a, sizeof(*a), SIZE, fp); // 写 double 的数组
fclose(fp);
fp = fopen("test.bin","rb");
// 读 double 的数组
while((ret_code = fread(&b, sizeof(double), 1, fp))>=1)
{
printf("%lf\n",b);
}
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp)) {
perror("Error reading test.bin");
}
fclose(fp);
fp = NULL;
}
8.文件缓冲区
ANSIC标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。文件缓冲区通常分为全缓冲、行缓冲和无缓冲三种类型。
文件缓冲区的意义:
文件缓冲区有助于提高文件I/O操作的效率,减少了频繁的磁盘访问,从而提高了程序的性能。
参考链接:https://zh.cppreference.com/w/c