文章目录
1.为什么使用文件
也许我们都经历过这样的场景,我们将数据存放到数组当中,数组的数据会随着程序的关闭而丢失,那是因为数组中的数据存放在主存当中,数据是易失性的,如何能让数据不丢失?或者说如何让数据持久化的存储?
我们可以选择将数据存放到文件、数据库等中,通过文件系统就可以做到数据的持久化存储。相对于数据库而言,文件系统的使用并不是那么常用,因为数据库有更强大的功能进行数据的读写。
2.文件概述
文件是一种抽象机制,它提供了一种在磁盘上保存信息且方便读取的方法。通俗理解,存放在磁盘上的都称之为文件;但又或许你听说过"Linux"下一切皆文件!键盘、显示器、磁盘都都是文件。
在编写代码是一般通常就分为两种文件:程序文件、数据文件。程序文件一般包括源程序文件(后缀为.c);目标文件(windows环境中后缀为.obj);可执行程序(windows环境中后缀为.exe);数据文件一般是存放程序运行是读写数据内容的文件。
2.1文件名
磁盘的空间很大,一个文件需要唯一的文件标识,以便用户识别和引用,称文件标识为文件名。
文件名包含3部分:文件路径+文件名主干+文件后缀;如:D:\\code\\test.txt
3.文件的打开和关闭
文件指针
在C语言中,有个一个**“文件类型指针”,简称"文件指针"FILE**,在内存中开辟对应的文件信息去,保存文件相关信息(如:文件名、文件的状态、文件当前的位置等),也就是说通过FILE指针指向的结构体,我们都能找到文件操作需要的信息,在使用者的角度只需要知道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;
可以将文件信息区看成一个数组通过:起始地址 + 偏移量的方式来访问!在编写文件的随机访问时更好说明问题。
文件打开和关闭
要对一个文件进行操作,在读写之前将文件打开,读写完毕之后将文件关闭。
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件:
FILE * fopen ( const char * filename, const char * mode )
功能:打开一个文件
参数:
filename:文件名,路径
mode:打开的方式
文件的打开方式:
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
关闭文件
int fclose ( FILE * stream )
功能:关闭一个打开的文件
参数:
stream:文件流
打开和关闭文件示例:
#include<stdio.h>
int main()
{
// test.txt相对路径,表示在进程的工作目录的文件;也可以使用绝对路径D:\code\test.txt
FILE* file = fopen("test.txt", "r");
if (file == NULL)
{
perror("the file open fail!\n");
return 1;
}
printf("the file of operation!\n");
fclose(file);
file = NULL;
return 0;
}
4.流概念
细心可以发现fclose函数参数中是传入一个流对象,int fclose ( FILE * stream )。
为什么会有流的概念
使用C语言程序可以对外部设备进行数据的输入输出操作,但是每个外部设备的组成是不相同的,对不同的外部设备需要不同的C程序,但是对于C语言程序员来说编写对于的程序还需要掌握对应的外部设备,太复杂!头皮发麻!
"流"在计算机体系中,是一种抽象的概念,和文件抽象一样。"流"为我们提供了统一处理输入输出的方法。
一切皆文件磁盘,显示器都是文件,为什么操作磁盘需要我们打开关闭流,而printf输出到显示器不用
那么C程序默认就会开启三个流:
、
5.文件的顺序读写
认识文本文件和二进制文件:
数据在内存当中以二进制补码的方式存储,如果将内存中的数据不加转换的直接输出的到外存当中生成的文件称为二进制文件,如果通过一定的手段转换,如通过ASCII码的形式转换到外存当中生成的文件称为文本文件
5.1字符输入输出函数
int fputc ( int character, FILE * stream )
功能:适用于所有流的字符输入函数
int fgetc ( FILE * stream )
功能:适用于所有流的字符输出函数
返回值:
成功返回该字符的ASCII值,读到结尾(feof)和失败(ferror)都返回EOF
使用字符输入输出函数,完成将字符a-z,写入文件并从文件中读取
使用ferror来判断是否出现错误,feof判断是否正常结束。
写入代码:
int test2()
{
FILE* file = fopen("./text.txt", "w");
if (file == NULL)
{
perror("the file open fail!\n");
return 1;
}
for (char ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, file);
}
fclose(file);
file = NULL;
return 0;
}
读取代码:
如果使用fgetc读取字符,如何判断是正常读到结尾,还是出错
int test3()
{
FILE* file = fopen("./text.txt", "r");
if (file == NULL)
{
perror("the file open fail!\n");
return 1;
}
int ret = 0;
while ((ret = fgetc(file)) != EOF)
{
printf("%c ", ret);
}
printf("\n");
if (ferror(file))
perror("I/O error when reading\n");
if (feof(file))
puts("end of file\n");
fclose(file);
file = NULL;
return 0;
}
上面的两份代码有什么问题呢:
打开和关闭了两次文件,完成a-z的写入和读取,能不能一次打开一次文件就完成!能,文件随机读取见!
5.2文本行输入输出函数
int fputs ( const char * str, FILE * stream )
功能:适用于所有流的文本行输入函数
char * fgets ( char * str, int num, FILE * stream )
功能:适用于所有流的文本行输出函数
参数:
str:定义一个缓冲区指针
返回值:返回指针,使用ferror来判断是否出现错误,feof判断是否正常结束
代码是示例:
int test5()
{
FILE* file = fopen("text.txt", "a");
char buff[256] = { 0 };
printf("please write your input string:");
//通过调试,fgets函数会带一个\n
fgets(buff, 256 -1 , stdin);
size_t len = strlen(buff);
printf("the buff size : %zd",len);
fputs(buff, file);
fclose(file);
file = NULL;
return 0;
}
5.3格式化输入输出函数
int fscanf ( FILE * stream, const char * format, ... )
功能:适用于所有流的格式化输入函数
int fprintf ( FILE * stream, const char * format, ... )
功能:适用于所有流的格式化输入函数
代码示例:
typedef struct Student
{
char name[20];
char sex[1];
int age;
char major[20];
}Student;
int test6()
{
Student stu = { 0 };
printf("请输入:姓名 性别 年龄 专业\n");
int ret = fscanf(stdin, "%s %s %d %s", stu.name, stu.sex, &(stu.age), stu.major);
fprintf(stdout, "%s %s %d %s", stu.name, stu.sex, stu.age, stu.major);
return 0;
}
5.4二进制输入输出函数
二进制输入输出函数常用于图片、视频的传输
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream )
功能:二进制输入
参数:
ptr:指向数据存储位置的指针
size:每个元素的大小(以字节为单位)
count:要读取的元素个数
返回值:成功返回读取的长度,失败返回小于count的值,文件结尾(feof),错误(ferror)
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream )
功能:二进制输出
代码示例:
int test7()
{
FILE* fileSrc = fopen("张若楠.jpg", "rb");
FILE* fileDest = fopen("D:\\test_file\\张若楠.jpg", "wb");
if (fileSrc == NULL || fileDest == NULL)
{
return 1;
}
char buff[1024];
size_t readSize;
while ((readSize = fread(buff, sizeof(char), sizeof(buff), fileSrc)) > 0)
{
fwrite(buff, sizeof(char), readSize, fileDest);
}
if (ferror(fileSrc))
perror("I/O error when reading\n");
if (feof(fileSrc))
puts("end of file\n");
fclose(fileSrc);
fclose(fileDest);
return 0;
}
6.文件的随机读取
在学习文件指针知道,读取文件的信息通过:起始地址 + 偏移量的方式获取。我们通过随机读取函数更一步的理解。
long int ftell ( FILE * stream )
功能:返回文件指针相对于起始位置的偏移量
void rewind ( FILE * stream )
功能:让文件指针的位置回到文件的起始位置,也就是让偏移量为0
int fseek ( FILE * stream, long int offset, int origin )
功能:根据文件指针的位置和偏移量来定位文件指针;origin设定起始的位置+offset偏移量
参数:
offset:移动的字节数(偏移量),正数表示向文件末尾移动,负数表示向文件开头移动
origin:有三个定义的宏,SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末尾)
三个函数的代码示例:
1.探究刚打开文件和读取a-z字符后的地址和偏移量 2.使用w+的读写方法打开一个文件,通过rewind函数将文件指针移动至起始位置,读取写入的a-z的26个字符 3.通过fseek函数,通过文件开头+2个偏移量确定指向的位置完成c-z的读取。
int test8()
{
FILE* file = fopen("rand.txt", "w+");
printf("刚打开文件:地址:%p 偏移量:%d\n", file,ftell(file));
if (file == NULL) return 0;
for (char ch = 'a'; ch < 'z'; ch++)
{
fputc(ch, file);
}
printf("写入a-z字符后:地址:%p 偏移量:%d\n", file, ftell(file));
//要在这里完成a-z字符的读取要让偏移量为0
rewind(file);
printf("使用rewind后:地址:%p 偏移量:%d\n", file, ftell(file));
char buff[32];
fgets(buff, sizeof(buff) - 1, file);
printf("完成从a-z字符的读取:%s\n", buff);
//完成从c-z字符的读取
fseek(file, 2, SEEK_SET);
fgets(buff, sizeof(buff) - 1, file);
printf("完成从c-z字符的读取:%s\n", buff);
fclose(file);
file = NULL;
return 0;
}
7.文件缓冲区
为什么要有缓冲区
IO的效率是很慢,文件缓冲区是为了减少IO访问的次数提高整机效率!
ANSIC 标准采用缓冲文件系统处理的数据文件的,而所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块文件缓冲区。通俗的理解:缓冲区就是内存中大数组,和我们自己定义的char buff[1024]
没有本质的区别。(C语言定义的是用户层缓冲区)
从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。(注意这样的说法会有问题,取决于缓冲区的刷新策略!)
如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)如图:
缓冲区的刷新策略
一般情况:1.立即刷新 2.行刷新(遇到’\n’刷新’\n’前面的)3.全刷新
特殊情况:1.强制刷新(fflush(注意这是C语言库函数)) 2.进程退出
**一般而言,所有的设备都倾向于全刷新!**缓冲区满了,才刷新,这样能减少IO的访问次数,来提高效率,因为在和外部设备进行IO的时候,数据量的大小不是主要的矛盾,和外设建立IO的过程才是最耗费时间的!
而一般而言,显示器是行刷新设备。磁盘是全缓冲设备。显示器执行行刷新策略,是一种妥协,一方面要照顾效率一方面要照顾用户体验!
代码示例,在Linux环境下执行的代码,fflush在高版本的VS下不能用:
#include<stdio.h>
#include<unistd.h>
int main()
{
FILE* file = fopen("buff.txt", "w");
//1."hello" 2."hello\n"
char* str = "hello\n";
fprintf(file, "%s", str);
fprintf(stdout, "%s", str);
sleep(10);
//强制刷新
fflush(file);
sleep(10);
fclose(file);
return 0;
}
1.“hello” 2."hello\n"磁盘都不会刷新;"hello\n"打印其上遇到\n会立马刷新,如果hello进程退出就又打在显示器上。