1. 什么是文件
文件是指存储在计算机存储设备上数据的集合,这些数据可以是文本,图片,程序,音频,视频等多种形式。
在我们磁盘(硬盘,比如我们计算机里面的c盘,d盘)上面的文件就是文件。
在程序设计中,我们一般谈的文件从文件功能的角度来分,一般有两种,程序文件和数据文件。
1.1 程序文件
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)
这些都是程序文件
1. 单个或多个源程序文件(.c)经过编译器,编译处理生成对应的目标文件
2. 单个或多个目标文件和链接库一起经过编译器处理生成最终的可执行程序
1.2 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
我们本章讨论的是数据文件。
在我们以前的各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结构显示到显示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读到内存中使用,这里处理的就是磁盘上的文件。
1.3 文件名
一个文件要有一个唯一的文件标识,以便于用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如:
c/code/test.txt
这里c/code/是文件路径
test是文件名主干
.txt是文件后缀
.txt是文件后缀
为了方便起见,文件标识通常称为文件名。
1.4 文件的作用
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次的数据的,如果要将数据永久化的保存,我们可以使用文件
2. 二进制文件和文本文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
2.1 二进制文件
· 数据在内存中以二进制的形式存储,如果不加转换的输出到外存的文件中,这就是二进制文件
2.2 文本文件
· 数据在存储前进行转换,以ASCII字符的形式存储的文件就是文本文件。
3. 数据在文件中的存储
字符一律以ASCII形式存储,数值型的数据既可以用ASCII形式存储,也可以使用二进制的形式存储。
举例:
假如有整数10000,如果以ASCII码的形式存储输出到磁盘,则磁盘中占5个字节(每一个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。
· 字符1的二进制序列为:00110001,字符0的二进制序列为:00110000
测试代码:
#include <stdio.h>
int main()
{
int a = 10000;
FILE * pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1,pf);//将4个字节的数据1次性以二进制的形式写到文件中
fclose(pf);
pf = NULL;
return 0;
}
在vs中怎么打开这个二进制文件呢?
第一步:我们右击源文件,找到添加选项,点开它的子选项,找到现有项,点击
第二步:点开之后,我们选择这个test.txt点击添加
第三步:我们在vs上面找到这个添加的文本右击找到打开方式,点击
第四步:我们往下找到这个二进制编辑器点击它,然后确定
这个就是我们打开的二进制的文本文件
这里的0000 0000 1027 0000是10000以16进制形式小端存放的。
4. 文件的打开与关闭
4.1 流与标准流
4.1.1 流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
c程序针对文件,画面,键盘等的数据输入输出操作都是通过流操作的。
一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
4.1.2 标准流
那么为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?
因为c语言程序在启动的时候,默认打开了3个流
1. stdin - 标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据
2. stdout - 标准输出流,大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中
3.stderr - 标准错误流,大多数环境中输出到显示器界面
这时默认打开了这三个流,我们使用scanf,printf等函数就可以直接进行输入输出操作的。
stdin,stdout,stderr三个流的类型是 FILE*,通常被称为文件指针
c语言中,就是通过FILE*的文件指针来维护流的各种操作的
4.2 文件指针
在缓冲文件系统中,关键的概念是"文件类型指针",简称"文件指针"。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中,该结构体类型是由系统声明的,取名FILE。
例如,vs2022编译环境提供的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*的指针变量:
FIEL* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量,可以使pf指向某个文件的信息区(是一个结构体变量)。通过该文件信息区中的信息能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件。
4.3 文件的打开与关闭
文件在读写之前,我们先要打开文件,在使用结束之后我们需要关闭文件。
这与我们之前学的动态内存开辟一样。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*类型的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSI C规定使用fopen函数打开文件,fclose来关闭文件
fopen函数:
头文件:stdio.h
声明:FILE * fopen ( const char * filename, const char * mode );
filename:字符串,表示要打开的文件名
mode:字符串,表示打开文件的形式
作用:使用给定的形式mode打开filename所指向的文件
如果文件成功打开,该函数将返回指向 FILE 对象的指针,打开失败则返回 null 指针。
常见的访问形式
文件使用方式 | 含义 | 如果指定文件不存在 |
"r" (只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
"w" (只写) | 为了输出数据,打开一个文本文件 | 建立一个新文件 |
"a" (追加) | 向文本文件尾添加数据 | 建立一个新文件 |
"rb" (只读) | 为了输入数据,打开一个二进制文件 | 出错 |
"wb" (只写) | 为了输出数据,打开一个二进制文件 | 建立一个新文件 |
"ab" (追加) | 向一个二进制文件尾添加数据 | 建立一个新文件 |
"r+" (读写) | 为了读和写,打开一个文本文件 | 出错 |
"w+" (读写) | 打开一个文件,建一个新的文件 | 建立一个新文件 |
"a+" (读写) | 打开一个文件,在文件尾进行读写 | 建立一个新文件 |
"rb+" (读写) | 为了读和写打开一个二进制文件 | 出错 |
"wb+" (读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新文件 |
"ab+" (读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新文件 |
fclose函数:
头文件:stdio.h
声明:int fclose ( FILE * stream );
stream:指向指定要关闭的流的FILE对象的指针。
作用:关闭与stream(流)关联的文件,并且刷新缓冲区。
如果流关闭成功,返回0,如果关闭失败,返回EOF。
代码举例:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
//判断文件是否打开
if (pf == NULL)
{
perror("fopen");
return 1;
}
//打开成功,写文件
//...
//关闭文件
fclose(pf);//这里fclose和free一样不会把它置为空,为了防止它变成野指针,我们手动给它置为空
pf = NULL;
return 0;
}
5. 文件的顺序读写
5.1 字符输入输出函数
fputc函数
头文件:stdio.h
声明:int fputc ( int char, FILE * stream );
char:被写入的字符
stream:指向标识输出流的FILE对象的指针作用:把参数char指定的字符写入到指定的流stream去。
如果写入成功,返回被写入字符的ASCII码值,写入失败,返回EOF。
举例:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
//判断文件是否打开
if(pf == NULL)
{
perror("fopen");
return 1;
}//打开成功
//我们要把abc写入到文件里面去
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
//关闭文件
fclose(pf);
pf = NULL;//防止pf变成野指针
return 0;
}
怎么查看我们写入的结果呢?
fgetc函数:
头文件:stdio.h
参数:int fgetc ( FILE * stream );
stream:指向标识输出流的FILE对象的指针
作用:从指定的流stream中获取一个字符
如果成功,就会把读到的字符返回过来,如果发生错误,或者遇到文件末尾,则会返回EOF
举例:
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;
}
5.2 文本行输入输出函数
fputs函数
头文件:stdio.h
声明:int fputs ( const char * str, FILE * stream );
str:被写入的字符串
steam:指向标识输出流的FILE对象的指针
作用:把字符串写入到指定的流stream中,但不包括空字符
如果成功将返回一个非负值,失败则返回EOF
举例:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
//判断文件是否打开
if (pf == NULL)
{
perror("fopen");
return 1;
}//打开成功
fputs("hello world", pf);
//关闭文件
fclose(pf);
pf = NULL;//防止野指针
return 0;
}
fgets函数
头文件:stdio.h
声明:char * fgets ( char * str, int num, FILE * stream );
str:指向字符数组的指针
num:要读取的最大字符数(包括空字符)
stream:指向标识输入流的FILE对象的指针。
作用:从流stream中读取一行数据,存放到str指向的字符数组中,当读取到(num-1)个字符,或者读取到换行符,文件末尾就会终止,以情况先发生而定。
如果读取成功,返回str指向空间的起始地址,如果在读取时遇到文件末尾或者在读的时候遇到错误,返回空指针。
举例:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
//判断文件是否打开
if (pf == NULL)
{
perror("fopen");
return 1;
}//打开成功
//创建一个字符数组来接受读取的数据
char arr[20] = "xxxxxxxxxxxxxxxx";
//读取数据存入到arr字符数组里面
fgets(arr, 5, pf);
printf("%s", arr);
//关闭文件
fclose(pf);
pf = NULL;//防止野指针
return 0;
}
这里我们读取了5个字符,但是只打印了4个字符,因为我们在读取时会把'\0'也读取进去
5.3格式化输入输出函数
fprintf函数
头文件:stdio.h
参数:int fprintf ( FILE * stream, const char * format, ... );
stream:指向标识输出流的FILE对象的指针。
format:包含要写入流的文本的C字符串,它可以选择包含嵌入的格式说明符,这些说明符将替换为后续附加参数中指定的值,并根据请求进行格式设置。
作用:按照一个格式向输出流输出数据如果写入成功,返回写入的字符总数,如果写入失败,返回负数。
举例:
struct S
{
char name[20];
int age;
int high;
};
int main()
{
struct S s = { "张三",18,175 };
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}//打开成功
//将s里面的数据写入文件里面
fprintf(pf,"%s %d %d",s.name,s.age,s.high);
//关闭文件
fclose(pf);
pf = NULL;//防止野指针
return 0;
}
看上面的代码的fpritnf函数的格式是不是和printf函数的格式差不多,只是多了一个文件指针。
fscanf函数
头文件:stdio.h
声明:int fscanf ( FILE * stream, const char * format, ... );
stream:指向FILE对象的指针,该对象标识要从中读取数据的输入流
format:包含一系列字符的 C 字符串,这些字符控制如何处理从流中提取的字符(空格字符,非空格字符和格式说明符)
作用:按照指定格式从输入流输入数据
如果成功,该函数返回成功匹配和赋值的个数,如果遇到文件末尾或发生读取错误,返回EOF
举例:
struct S
{
char name[20];
int age;
int high;
};
int main()
{
struct S s = {0 };
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}//打开成功
//从文件中读取数据存放到s里面
fscanf(pf, "%s %d %d", s.name, &(s.age), &(s.high));//这里name数组名是地址,age和hign是变量,需要取地址
//打印在屏幕上看看
printf("%s %d %d", s.name, s.age, s.high);
//关闭文件
fclose(pf);
pf = NULL;//防止野指针
return 0;
}
5.4 二进制输入输出函数
fwrite函数
头文件:stdio.h
声明:size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
ptr: 指向被写入的元素数组的指针
size: 被写入的每个元素的大小,以字节为单位
count:被写入的元素的个数
stream:指向指定输出流的FILE对象的指针
作用:把ptr指向的数组中的数据以二进制的形式写入到指定的流stream中
如果成功,该函数返回写入成功元素的个数,如果读取时遇到文件末尾或者读取错误,返回EOF
举例:
int main()
{
int arr[5] = { 1,2,3,4,5 };
//想要将arr里面的数据以二进制的形式写入文件
//打开文件
FILE* pf = fopen("test.txt", "wb");
//判断文件是否打开
if (pf == NULL)
{
perror("fopen");
return 1;
}//打开成功
//写入
int sz = sizeof(arr) / sizeof(arr[0]);
fwrite(arr, sizeof(arr[0]), sz, pf);
//关闭文件
fclose(pf);
pf = NULL;//防止野指针
return 0;
}
这里我们打开文件,发现里面是一些看不懂的二进制
fread函数
头文件:stdio.h
声明:size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
ptr:指向大小至少为 (size*count) 字节的内存块的指针
size:要读取的每个元素的大小,单位是字节
count:要读取的元素个数
stream:指向指定输入流的FILE对象的指针
举例:
int main()
{
int arr[5] = { 0 };
//打开文件
FILE* pf = fopen("test.txt", "rb");
//判断文件是否打开
if (pf == NULL)
{
perror("fopen");
return 1;
}//打开成功
//读取数据,写入到arr数组里面
int i = 0;
while (fread(&arr[i], sizeof(int), 1, pf))
{
printf("%d ",arr[i]);
i++;
}
fclose(pf);
pf = NULL;//防止野指针
return 0;
}
5.5 流输入输出
我们知道,c语言程序在启动的时候,自动打开三个流,stdin--标准输入流,stdout标准输出流,stderr--标准错误流,那么我们是否可以使用流来完成输入输出操作呢?
自然是可以的
举例:
int main()
{
int ch = fgetc(stdin);//从标准输入流(键盘)中输入数据
fputc(ch, stdout);//从标准输出流(屏幕)中输出数据
return 0;
}
5.6 拓展
sprintf函数与sscanf函数
头文件:stdio.h
声明:int sprintf ( char * str, const char * format, ... );
str:指向一个字符数组的指针,该数组存储了C字符串
format:包含要写入流的文本的C字符串,它可以选择包含嵌入的格式说明符,这些说明符将替换为后续附加参数中指定的值,并根据请求进行格式设置。
作用:将格式化的数据转换为字符串
如果成功,将返回写入的字符总数。此计数不包括自动追加在字符串末尾的空字符。如果失败,返回负数。
头文件:stdio.h
声明:int sscanf ( const char * str, const char * format, ...);
str: C字符串,是函数检索数据的源。
format:包含一系列字符的 C 字符串,这些字符控制如何处理从流中提取的字符(空格字符,非空格字符和格式说明符)
作用:将字符串按照一定格式转换为格式化数据
如果成功,该函数返回成功匹配和赋值的个数,如果遇到文件末尾或者读取错误,则返回EOF
举例:
struct S
{
char name[20];
int age;
int hign;
};
int main()
{
char ch[100] = { 0 };
struct S s = { "zhangsan",19,170 };
//将这个结构体的数据转化为字符串
sprintf(ch, "%s %d %d", s.name, s.age, s.hign);
//打印观察
printf("1.以字符串形式打印:%s\n", ch);
//我们将这个字符串内容还原成结构体的数据
struct S c = { 0 };
//打印观察
sscanf(ch, "%s %d %d", c.name, &(c.age), &(c.hign));
printf("2.以格式化形式打印:%s %d %d", c.name, c.age, c.hign);
return 0;
}
区别:
printf,fprintf,sprintf,scanf,fscanf,sscanf之前的区别
函数 | 功能 |
printf | 把数据以格式化的形式打印在标准输出流上 |
fprintf | 把数据以格式化的形式打印在指定的输出流上 |
sprintf | 把格式化的数据转换成字符串 |
scanf | 从标准输入流上读取格式化的数据 |
fscanf | 从指定的输入流上读取格式化的数据 |
sscanf | 在字符串中读取格式化的数据 |
6. 文件的随机读写
6.1 fseek函数
头文件:stdio.h
声明: int fseek ( FILE * stream, long int offset, int origin );
stream : 指向标识流的FILE对象的指针
offset:偏移量
origin:,起始位置,用作偏移量的考察位置
作用:将文件指针设置到流stream的文件从orgin参数位置开始,为给定的偏移量offset处
如果成功,返回0,失败则返回非0的值。
origin的三种起始位置选择:
SEEK_SET | 文件的开头 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件的末尾 |
举例:
假设我们文件中放的字符串abcdef
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//从文件指针的起始位置向右偏移4个字节
fseek(pf, 4, SEEK_SET);//e
int ch = fgetc(pf);
printf("%c\n", ch);
//从文件结束位置向左偏移3个字节
fseek(pf, -3, SEEK_END);//d
ch = fgetc(pf);
printf("%c\n", ch);
//从文件当前位置向左偏移2个字节
fseek(pf, -2, SEEK_CUR);//c
ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;//防止野指针
return 0;
}
图解
6.2 ftell函数
头文件:stdio.h
声明:long int ftell ( FILE * stream );
stream: 指向标识流的FILE对象的指针
作用:返回文件指针相对于起始位置的偏移量
如果成功,返回位置指标的当前值,如果失败,返回一个long int 类型的-1
我们可以通过ftell来求文件的长度,文件里面还是存放的abcdef
举例:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
//判断文件是否打开
if (pf == NULL)
{
perror("fopen");
return 1;
}//打开成功
//将文件指针指向文件的末尾
fseek(pf, 0, SEEK_END);
printf("%d ", ftell(pf));//计算文件指针想对于起始位置的偏移量
//关闭文件
fclose(pf);
pf = NULL;//防止野指针
return 0;
}
6.3 rewind函数
头文件:stdio.h
声明:void rewind ( FILE * stream );
stream:指向标识流的FILE对象的指针
作用:让文件指针的位置回到文件的起始位置
没有返回值
举例:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
//判断文件是否打开
if (pf == NULL)
{
perror("fopen");
return 1;
}//打开成功
//从文件末尾位置向左偏移3个字节
fseek(pf, -3, SEEK_END);
printf("%c ",fgetc(pf));//读取d
//这时假设我们想让文件指针回到起始位置
rewind(pf);
printf("%c", fgetc(pf));//读取a
//关闭文件
fclose(pf);
pf = NULL;//防止野指针
return 0;
}
7. 文件读取结束的判定
7.1 被错误使用的feof
在我们学习c语言文件的操作时,常常会有人误以为feof是用来判断文件是否结束的函数,其实,这时不准确的,feof的作用是:当文件读取结束的时候,判断读取结束的原因是否是,遇到文件末尾结束。
ferror:是用来判断文件读取结束的原因,是不是读取的时候发生了错误。
8. 文件缓冲区
ANSIC标准采用"缓冲文件系统"处理的数据文件,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块"文件缓冲区"。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
我们可以利用下列代码证明缓冲区的存在:
#include <stdio.h>
#include <windows.h>
//VS2019 WIN11环境测试
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语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件,如果不做,可能导致读写文件的问题。