C语言的内容已经差不多要结束了。今天我们了解C语言中的文件操作。
1 为什么使用文件
如果没有文件,我们写的程序的数据存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行永久化的保存,就要使用文件。
2 什么是文件
磁盘上的文件就是文件。但是在程序设计中,我们一般谈的文件有两种:程序文件,数据文件(从文件功能的角度来分类的)。
2.1 程序文件
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
2.2 数据文件
数据文件的内容不一定是程序
,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件或者输出内容的文件。
2.3 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包括三个部分:文件路径+文件名主干+文件后缀。
例如: c:\code\test.txt
为了⽅便起⻅,⽂件标识常被称为⽂件名。
3 二进制文件和文本文件
根据数据的组织形式,数据文件被称为文本文件和二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码值的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
数据在内存中是怎么存储的呢?具体请看数据在内存中的存储方式。
字符一律以ASCII码形式存储,数值型数据既可以用ASCII形式存储,也可以用二进制形式存储
。
如整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出则在磁盘上占用4个字节
#include<stdio.h>
#include<stdlib.h>
int main()
{
//00000000 00000000 00100111 00010000
// 00 00 27 10
int a = 10000;
//打开文件
FILE* pf = fopen("data.txt", "wb");
if (NULL == pf)
{
printf("fopen fail\n");
exit(-1);
}
fwrite(&a, sizeof(int), 1, pf);//以二进制的形式写到文件中
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
4 文件的打开和关闭
4.1 流
我们程序的数据需要输出到各种外部设备,也需要从各种外部设备获取数据,不同的外部设备的输入输出操作各不相同。为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念。
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。
一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
4.2 标准流
有一个问题需要思考一下。我们已经学习了printf和scanf函数,那为什么以前在使用这两个函数的时候没有打开流呢?
那是因为C语言程序在启动的时候,默认打开了3个流:
stdin–标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
stdout–标准输出流,大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中。
stderr–标准错误流,大多数环境输出到显示器界面。
stdin、stdout、stderr
三个流的类型是:FILE*
,通常称为文件指针。
C语言中,就是通过FILE*的文件指针来维护流的各种操作的。
4.3 文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件在内存中
都开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态,文件当前的位置等)。这些信息是保存在一个结构体变量
中的。该结构体类型是由系统声明的,取名为FILE
。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量
,并填充其中的信息。
一般都是通过一个FILE结构的指针
来维护这个FILE结构的变量
。
FILE* pf;//文件指针变量
定义pf
是一个指向FILE类型数据的指针变量
,可以使pf指向某个文件的文件信息区(是一个结构体变量)
。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件
。
4.4 文件的打开与关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
//fopen是用来打开文件的
//filename表示文件名
//mode表示文件的打开方式
FILE* fopen(const char* filename, const char* mode);
//fclose是用来关闭文件的
//成功关闭文件,返回0
//文件关闭失败,返回EOF
int fclose(FILE* stream);
5. 文件的顺序读写
5.1 顺序读写函数介绍
先来搞清楚内存和文件之间的关系。
5.1.1 fgetc函数介绍
//从流得到字符
//成功的话,返回读取到的字符,提升为整型值(ASCII码值)
//失败的话或者遇到文件结束标志,发生读取错误,就返回EOF
int fgetc(FILE* stream);
#include<stdio.h>
#include<stdlib.h>
int main()
{
//以写的方式打开文件
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
printf("fopen fail\n");
exit(-1);
}
//写文件
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.1.2 fputc函数介绍
//写字符到流中
//被写的字符提升为整型int,当写这个字符的时候,内部的值转换为一个
//unsigned char类型
//stream是一个指向标识输出流的文件对象的指针
//成功的话,返回写下的字符
//写入错误,返回EOF
int fputc(FILE* stream);
#include<stdio.h>
#include<stdlib.h>
int main()
{
//以读的方式打开文件
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
printf("fopen fail\n");
exit(-1);
}
//读文件
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
//写到标准输出流中,也就是屏幕上
fputc(ch, stdout);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.1.3 fgets函数介绍
//str是一个指向被拷贝字符串的字符数组的指针
//num被拷贝字符的最大数量
//stream是一个指向标识输入流的文件对象的指针
//成功的话,返回一个字符串
//遇到文件结束标志,设置一个错误标志
//如果在读取任何字符之前,遇到了文件结束标志,返回一个空指针,str指向的内容仍然不变
//读取错误,设置错误标志并返回一个空指针,但是通过str指向的内容也许会改变
char* fgets(char* str,int num,FILE* stream);
#include<stdio.h>
#include<stdlib.h>
int main()
{
//以读的方式打开文件
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
printf("fopen fail\n");
exit(-1);
}
//读文件
char ch[10] = { 'a'};
//读取num-1个字符
char* pch = fgets(ch, 6, pf);
printf("%s\n", pch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.1.4 fputs函数介绍
//str写到流中的字符串的内容
//stream是一个指向标识输出流的文件对象的指针
//成功的话,返回一个非负数
//失败的话,返回EOF并设置错误标志
int fputs(const char* str,FILE* stream);
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
exit(-1);
}
//写文件
fputs("hello everyone\n", pf);
fputs("hello world\n", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.1.5 fscanf函数介绍
//stream是一个指向从标识输入流读取数据的文件对象的指针
//成功的话,该函数返回成功填充参数列表的项数
//失败的话,返回EOF
int fscanf(FILE* stream,const char* format,...);
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int num = 0;
char name[10] = { 0 };
//打开文件
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
exit(-1);
}
//读文件
fscanf(pf, "%d %s", &num, name);
printf("%d %s\n", num, name);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.1.6 fprintf函数介绍
//stream是一个指向标识输出流的文件对象的指针
//成功的话,返回写下字符的总个数
//写入发生错误,设置错误标志并且返回一个负数
int fprintf(FILE* stream,const char* format,...);
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int num = 18;
char name[10] = "小美";
//打开文件
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
exit(-1);
}
//写文件
fprintf(pf, "%d %s\n", num, name);
printf("%d %s\n", num, name);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.1.7 fread函数介绍
//ptr指向了一个内存块,大小至少为size*count个字节
//size读取每个元素的大小,单位是字节
//count元素的个数
//stream是一个指向输入流的文件对象的指针
//成功读取的话,返回的是元素的总个数
size_t fread(void* ptr,size_t size,size_t count,FILE* stream);
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int arr[5] = { 0 };
//打开文件
FILE* pf = fopen("test.txt", "rb");//以二进制的形式写
if (NULL == pf)
{
perror("fopen");
exit(-1);
}
//读文件
fread(arr, sizeof(int), 5, pf);
for (int i = 0; i < 5; i++)
{
printf("%d ", arr[i]);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.1.7 fwrite函数介绍
//ptr指向了一个被写下元素的数组
//size是被写下每个元素的大小,单位是字节
//count元素的个数
//stream指向了一个输出流的文件对象的指针
//成功写入的话,返回的是元素的总个数
size_t fwrite(const void* ptr,size_t size,size_t count,FILE* stream);
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
//打开文件
FILE* pf = fopen("test.txt", "wb");//以二进制的形式写
if (NULL == pf)
{
perror("fopen");
exit(-1);
}
//写文件
fwrite(arr, sizeof(int), 5, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.2 sscanf 和 sprintf
scanf/fscanf/sscanf
printf/fprintf/sprintf
这些函数中只有sscanf,sprintf
没有接触过,接下来我们就来看看这两个函数。
sprintf函数是将格式化的数据转换成字符串
sscanf函数是从字符串提取格式化的数据
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int num = 100;
float score = 3.14f;
char name[10] = "小强";
char str[100] = { 0 };
//将格式化的数据转换成字符串
sprintf(str, "%d %f %s\n", num, score, name);
printf("字符串的形式:%s\n", str);
int num1 = 0;
float score1 = 0;
char name1[10] = { 0 };
//从字符串中提取格式化的数据
sscanf(str, "%d %f %s\n", &num1, &score1, name1);
printf("格式化的形式:%d %f %s\n", num1, score1, name1);
return 0;
}
6 文件的随机读写
6.1 fseek 和 ftell
//offset相对于起始地址的偏移量
//origin文件的起始地址
//origin有3个取值:SEEK_SET(文件的起始地址),SEEK_CUR(文件当前位置),SEEK_END(文件的末尾)
//fseek文件的随机读写
int fseek(FILE* stream,long int offset,int origin);
//返回的是相对于文件起始地址的偏移量
long int ftell(FILE* stream);
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
//以读的方式打开文件
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
exit(-1);
}
//读文件
int ch = fgetc(pf);//a
printf("%c\n", ch);
//文件的随机读写,移动光标的位置
fseek(pf, 2, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);//d
//计算相对于文件起始地址的偏移量
long int r = ftell(pf);
printf("%ld\n", r);//4
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
6.2 rewind
//文件指针的位置回到文件的起始地址
void rewind(FILE* stream);
7 文件读取结束的判定
.
被错误使用的feof
注意:在文件读取过程中,不能用feof函数的返回值直接来判断文件是否结束
。
feof
的作用是:当文件读取结束的时候,判断文件读取结束的原因是否是遇到了文件结束标志结束
。
1.文本文件是否读取结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)。
2.二进制文件的读取结束判断,判断返回值是否小于实际要读的个数(fread)。
8 文件缓冲区
ANSIC标准采用“缓冲文件系统”处理数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中,每一个正在使用的文件开辟一块“文件缓冲区
”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区之后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个的将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定。