1.什么是文件
如果将数据持续化保存在电脑中,就要使用文件。
2.文件分类
在程序设计中,我们一般把文件分为两类:程序文件和数据文件。
2.1程序文件
程序文件包括程序源文件(后缀是.c)、目标文件(windows环境后缀是.obj)、可执行程序(windows环境后缀为.exe)。
2.2数据文件
文件内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
接下来主要讨论数据文件。
2.3文件名
文件名包含三部分,文件路径+文件名主干+文件后缀
例如:c:\code\test.txt
3.二进制文件和文本文件
根据数据文件的存储类型,分为二进制文件和文本文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储器前转换。以ASCII字符的形式存储的文件就是文本文件。
比如数字10000在文件中的储存。
在VS上打开二进制文件的方法。
测试代码:
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//⼆进制的形式写到⽂件中
fclose(pf);
pf = NULL;
return 0;
}//只是一个例子,下面会详细介绍
图中显示的就是10000的二进制存储形式(小端存储)。
4.文件的打开和关闭
4.1流和标准流
4.1.1流
C程序中针对文件、画面、键盘等的数据输入输出都是通过流的形式。
所以一般想要输入输出,都要先打开流。
4.1.2标准流
我们在敲代码的时候,可以直接scanf输入数据,printf输出数据,这是因为c语言程序中默认打开三个流:
stdin:标准输入流,在大多数环境中键盘输入,scanf就是从标准输入流中读取数据。
stdout:标准输出流,在大多数环境中输出至显示屏,printf就是将信息输出到标准输出流中。
stderr:标准错误流,在大多数环境中输出至显示屏。
这三个流的类型是:*FILE,文件指针。
4.2文件指针
缓冲文件系统中,最关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件在内存中会开辟相应的文件信息区,在这个信息区内会存放文件的相关信息(如文件名字,文件状态和文件当前所在位置等),这些信息会存放在结构体变量中。该结构体类型是由系统声明的,命名为FILE。
例如,以下文件声明
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
当然,每个编译器下的文件声明会有所不同,但都大同小异。
我们不用关心其中的细节,这些都是系统的活,我们只用知道,每打开一次文件,系统会自动生成这些东西就可以了。
在通常情况下,我们通过文件指针来维护FILE结构体变量。
FILE* pfile;//文件指针变量
定义pfile是指向FILE数据类型的指针变量,pfile可以指向某个文件的文件信息区,通过该文件信息区就可以访问该文件。
4.3文件的打开和关闭
文件在读写之前应该先打开文件,结束以后关闭文件。
ANSIC规定用fopen打开文件,用fclose关闭文件。
mode表示文件的使用方式。
直白来讲,“读”是输入操作,“写”是输出操作。
测试代码:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");//可以显示错误信息
return 1;
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
此时,在该路径下就会出现test.txt文件。
5.文件的顺序读写
函数名称 | 含义 | 功能 |
fgets | 字符输入函数 |
所有输入流(包括stdin)
|
fputs | 字符输出函数 | 所有输出流(包括stdout) |
fgetc | 文本行输入函数 | 所有输入流(包括stdin) |
fputc | 文本行输出函数 | 所有输出流(包括stdout) |
fscanf | 格式化输入函数 | 所有输入流(包括stdin) |
fprintf | 格式化输出函数 | 所有输出流(包括stdout) |
fread | 二进制输入 | 文件 |
fwrite | 二进制输出 | 文件 |
5.1fputs函数和fgets函数
注意:fgets函数读取 (num-1) 个字符。
测试代码:
int main()
{
//打开文件
FILE* pf = fopen("data1.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("abcdef", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
此时,用记事本打开该文件,会出现如下结果:
测试代码:
int main()
{
//打开文件
FILE* pf = fopen("data1.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取
char arr[5] = "xxx";
fgets(arr, 3, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
其实,只读取了文件中(3-1)个字符,前两个字符是ab,第三个字符是‘\0’。
5.2fgetc函数和fputc函数
character在这里是字符的意思。
测试代码:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
for (int i = 0; i < 26; i++)
{
//fputc('a' + i, pf);
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 ret = fgetc(pf);
printf("%d\n", ret);//以ASCII的形式输出,没有用循环,只读取第一个字符
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.3fscanf函数和fprintf函数
对比一下,scanf和printf函数
其实就是在scanf和printf的基础上多了一个参数,剩下的就是scanf和printf的基本使用方法
测试代码:
struct Var
{
char name[20];
int age;
float score;
};
int main()
{
struct Var v = { "yangweimu",20,80.5f };
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror(pf);
return 1;
}
fprintf(pf, "%s %d\n", v.name, v.age, v.score);
fclose(pf);
pf = NULL;
return 0;
}
在该路径下,文件就会显示如下:
测试代码:
将文件中的数据读取出来
struct Var
{
char name[20];
int age;
float score;
};
int main()
{
struct Var v = {0};
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
return 1;
}
//写读文件
fscanf(pf, "%s %d %f", v.name, &(v.age), &(v.score));
//name是一个数组,数组名本来就是地址,所以不用加&
fprintf(stdout, "%s %d %.1f\n", v.name, v.age, v.score);
//在屏幕上输出
fclose(pf);
pf = NULL;
return 0;
}
5.4fread函数和fwrite函数
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = {"zhangsan", 20, 90.5};
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
return 1;
}
//二进制的形式写文件
fwrite(&s, sizeof(s), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
在该路径下会出现如下结果,因为fwrite函数是二进制输出,所以以文本文件打开有些字符无法识别。
测试代码:
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { 0 };
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
return 1;
}
//二进制的形式读文件
fread(&s, sizeof(s), 1, pf);
printf("%s %d %.1f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
6.文件的随机读写
6.1fseek
通过文件指针的位置和偏移量来定位文件指针。
测试代码:
//test.txt文件中存放的是26个英文字母
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fseek(pf, 0, SEEK_SET);
int ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
因为用的是SEEK_SET,所以是文件开始,移动是0个,所以打印第一个字符。 (其实就是光标的移动,肯定是打印光标后面的字符。)
其他情况
测试代码:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//fseek(pf, 0, SEEK_SET);
//printf("%c\n", ch);
fseek(pf, -2, SEEK_END);
int ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
测试代码:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//fseek(pf, 0, SEEK_SET);
//printf("%c\n", ch);
fseek(pf, -2, SEEK_END);//y
fseek(pf, -3, SEEK_CUR);//v
int ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
6.2ftell函数
返回文件指针相对于起始位置的偏移量。
测试代码:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//fseek(pf, 0, SEEK_SET);
//printf("%c\n", ch);
fseek(pf, -2, SEEK_END);
fseek(pf, -3, SEEK_CUR);
int ch = fgetc(pf);
int n = ftell(pf);
printf("%c\n", ch);
printf("%d\n", n);
fclose(pf);
pf = NULL;
return 0;
}
7.文件读取结束的判定
feof函数的作用:当文件读取结束的时候,判定是否是遇到文件尾结束。
注意!!不能用feof函数判定文件结束!只是检查作用!!
例如:fgets 判断返回值是否是NULL
fgetc判断是否是EOF
测试代码:
#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);
return 0;
}
over~