文中的源码都在这里哦!!!
一、文件的基本概念
一个文件通常是磁盘上一段命名的存储区
磁盘文件(通常用的文件):指一组相关数据的有序集合,通常存储在外部介质(如磁盘)上,使用时才调入内存
设备文件(与硬件有关的文件):在操作系统中把每一个与主机相连的输入、输出设备看作是一个文件,把它们的输入、输出等同于对磁盘文件的读和写。例如:键盘(标准输入文件)、屏幕(标准输出文件)
-
以下说的是磁盘文件
(一)磁盘文件的存取过程
磁盘文件,一般保存在硬盘、光盘、U盘等掉电不丢失的磁盘设备中,在需要时调入内存
在内存中对文件进行编辑处理后,保存到磁盘中(内存只是文件的处理区,不是存储区)
程序与磁盘之间交互,不是立即完成,系统或程序可根据需要设置缓冲区,以提高存取效率,延长磁盘寿命(在缓冲区进行暂时存储,直到存储的数据量达到一定的数量,不然存储一次打开一次磁盘,会缩短磁盘寿命)
(二)文件分类(重要)
1、物理上
-
所有的磁盘文件都是二进制存储,以字节为单位进行顺序存储(空间是连续的)
2、逻辑上
程序文件:有后缀名,主要用于存储程序代码
数据文件:程序运行时读写的数据
数据文件可分为ASCII文件(文本文件)和二进制文件——C语言可识别的文件
-
文本文件
基于字符编码,常见编码有ASCII、UNICODE等,一般可以使用文本编辑器直接打开
例如:数5678以ASCII存储形式为
ASCII码:00110101 00110110 00110111 00111000
是每个字符的ASCII码值
效率低,需要查表,但便于查看
-
二进制文件
基于值编码,根据应用指定某个值的意思
把内存中的数据按其在内存中的存储形式原样输出到磁盘上
例如:5678的存储形式为
进制码:0001 0110 0010 1110
读取效率很高,但是解译很麻烦,查看麻烦
3、两者的对比
文本文件 | 二进制文件 | |
---|---|---|
译码 | 编码基于字符定长,译码容易些 | 编码是变长的,译码难一些(不同的二进制文件格式,有不同的译码方式) |
空间利用率 | 任何一个符号至少需要一个字节 | 用一个比特来代表一个意思(位操作) |
可读性 | 通用的记事本工具就几乎可以浏览所有文本文件 | 需要一个具体的文件解码器 |
二、文件操作
-
C语言中不能直接操作文件,需要借助库函数对文件进行操作
-
基本流程
在使用文件前要调用打开函数将文件打开,得到文件指针fp
然后调用各种有关函数,利用fp对文件进行具体处理(读或写)
在文件用完时,及时调用关闭函数来关闭文件
-
C语言中所有的文件操作都围绕文件指针完成(只有通过文件指针,我们才能知道自己操作的是哪个文件),所以操作文件前必须定义一个文件指针指向我们将要操作的文件
(一)文件指针
-
文件指针:指向文件的指针,可以指向文件的内存地址
-
格式:FILE * 指针变量标识符
// 由于文件类型已经在stdio.h头文件中有声明了,所以我们不需要另外声明,直接使用就行了
FILE *fp = NULL;
FILE为大写,需要包含头文件<stdio.h>
FILE是系统使用typedef定义出来的有关文件信息的一种结构体类型,FILE结构体中含有文件名、文件状态和文件当前位置等信息(不需要关心)
-
c语言中有三个特殊的文件指针无需定义,打开可直接使用(在后续说明中会提到!!!)
stdin:标准输入。默认为当前终端(键盘),使用的scanf、getchar函数默认从此终端获得数据
stdout:标准输出。默认为当前终端(屏幕),使用的printf、puts函数默认输出信息到此终端
stderr:标准出错。默认为当前终端(屏幕),当我们程序出错或者使用perror函数时信息打印在此终端
(二)打开与关闭文件
-
任何文件使用之前必须打开,使用后必须关闭
1、fopen打开一个文件
-
格式:fp = fopen(文件名,文件打开方式)
-
返回值:返回的是文件的起始地址,执行不成功返回NULL(需要在打开之后判断是否打开成功)
-
文件名:要操作文件的名字,可包含路径
文件路径 + 文件名主干 + 文件后缀
文件的路径可以唯一标识文件的位置
绝对路径:从根目录(盘符)开始的文件路径 + 文件名 + 文件扩展名(文件类型)
相对路径:相对于当前目录,从当前目录开始访问到某一文件的路径
-
打开方式:读、写、二进制、文本(与文件的存储方式无关,与操作系统有关)
r:以只读方式打开文件。文件不存在返回NULL;文件存在返回文件指针,进行后续的读操作
w:以只写方式打开文件。文件不存在以指定文件名创建此文件;文件存在则清空文件内容,进行写操作;文件打不开,返回NULL
a:以追加方式打开文件。文件不存在,以指定文件名创建此文件(同w);若文件存在,从文件的结尾处进行写操作
+:可读可写
b:以二进制的方式打开文件
t:以文本的方式打开文件(省略!!!)
- 不是很重要,了解一下就行
- 在windows平台下,当读取文件的时候,系统会将所有的"\r\n"转换成"\n";当写入文件的时候,系统会将"\n"转换成"\r\n"写入
- 在Unix/Linux平台下,“文本”与“二进制”模式没有区别。\r\n作为两个字符原样输入输出
-
打开方式的组合形式
模 式 | 功 能 |
r或rb | 以只读方式打开一个文本文件(不创建文件) |
w或wb | 以写方式打开文件(使文件长度截断为0字节,创建一个文件) |
a或ab | 以添加方式打开文件,即在末尾添加内容,当文件不存在时,创建文件用于写 |
r+或rb+ | 以可读、可写的方式打开文件(不创建新文件) |
w+或wb+ | 以可读、可写的方式打开文件 (使文件长度为0字节,创建一个文件) |
a+或ab+ | 以添加方式打开文件,打开文件并在末尾更改文件(如果文件不存在,则创建文件) |
-
顺序读写文件:文件指针会从文件指针当前位置顺序读写文件,打开文件时,文件指针默认指向文件的开头
2、fclose关闭文件
-
格式:fclose(文件指针);
-
正常执行则返回0,不成功则返回-1(EOF)
3、练习
以只读、文本方式打开当前路径下一个叫test.txt文件
以只写、二进制方式打开同一个文件
以追加(a)、文本的方式打开同一个文件
同时以读/写、二进制方式打开同一个文件,要求若文件不存在,提示出错
同时以读/些、文本方式打开同一个文件,要求若文件不存在,创建此文件
若打开成功则关闭相应的文件
void test1()
{
FILE *fp = NULL;
fp = fopen("tset.txt", "r"); // 只读、文本
fp = fopen("test.txt","wb"); // 只写、二进制
fp = fopen("test.txt","a"); // 追加、文本
fp = fopen("tset.txt", "rb+"); // 可读可写、二进制、不存在报错
fp = fopen("test.txt","w+"); // 可读可写、文本、不存在创建
if(fp != NULL) // 打开成功则关闭相应的文件
{
fclose(fp);
}
}
-
对文件操作最常用的是:“读”和“写”
-
C语言提供了多种对文件读写的函数
字节读写函数: fgetc和fputc
字符串读写函数:fgets和fputs
数据块读写函数:fread和fwrite
格式化读写函数:fscanf和fprintf
-
以上函数可完成对文件内容的顺序读写
(三)文件的字节读写(单个字符的输入输出)
1、fgetc:字节读操作
-
格式:ch = fgetc(fp);
-
说明
从指定文件读取一个字节赋给ch(以“读”或“读写的方式”打开文件)
读取文本文件,读到文件结尾返回EOF(EOF是在stdio.h文件中定义的符号常量,值为-1)
读二进制文件,读到文件结尾,使用feof判断结尾(后面会讲)
2、读操作示例
如果本地没有创建相应的文件,会报错如下所示
事先在本地创建a.txt,文件内容为 hello file
文件输出:buf = hello file
void test2()
{
char buf[128] = "";
int i = 0;
FILE* fp = NULL;
// 1、使用fopen打开一个文件,获得文件指针
fp = fopen("a.txt", "r");
if (fp == NULL) // 以只读的形式打开文本,当前目录下没有该文本,输出错误信息,然后退出
{
perror("fopen");
return;
}
// 2、对文件的操作fgetc
while (1)
{
// fgetc调用一次 读取到一个字节
buf[i] = fgetc(fp);
if (buf[i] == EOF) // EOF表示已经到文件末尾
{
break;
}
i++;
}
printf("buf = %s\n", buf);
// 3、关闭文件
fclose(fp);
}
3、fputc:字节的写操作
-
格式:fputc(ch,fp);
-
说明
把一个ch变量中的值(1个字节)写到指定的文件
如果输出成功,则返回输出的字节;如果输出失败,则返回一个EOF
4、写操作示例
void test3()
{
char buf[128] = "";
int i = 0;
FILE* fp = NULL;
fp = fopen("b.txt", "w"); // 没有就创建
if (fp == NULL)
{
perror("fopen");
return;
}
// 使用fputc进行文件的数据写入
printf("请输入要写入文件的字符串:");
fgets(buf, sizeof(buf), stdin); // 会获取换行符
buf[strlen(buf) - 1] = 0; // 去掉键盘输入的换行符
// 将字符串buf中的元素 逐个写入文件中
while (buf[i] != '\0')
{
fputc(buf[i], fp);
i++;
}
fclose(fp);
}
5、练习:从a.txt读取文件内容写入到b.txt
void test4()
{
FILE *fp1 =NULL;
FILE *fp2 =NULL;
// 以只读的方式打开a.txt
fp1 = fopen("a.txt","r");
if(fp1 == NULL)
{
perror("fopen");
return;
}
// 以只写的方式打开b.txt
fp2 = fopen("b.txt","w");
if(fp2 == NULL)
{
perror("fopen");
return;
}
// 从fp1中每读取一个字节写入到fp2中
while(1)
{
char ch;
// 读
ch = fgetc(fp1);
if(ch == EOF) // 已经读到文件末尾
break;
// 写
fputc(ch,fp2);
}
fclose(fp1);
fclose(fp2);
return;
}
(四)字符串的输入输出(文件的字符串读写)
1、fputs:往文件中写入一个字符串
-
格式:fputs("china", fp);
-
说明
向指定文件写入一个字符串
第一个参数可以是字符串常量、字符数组、字符指针
字符串末尾的'\0'不会写进文件
void test5()
{
// 指针数组
char *buf[]={"窗前明月光\n","疑似地上霜\n","举头望明月\n","低头思故乡"};
int n = sizeof(buf)/sizeof(buf[0]);
FILE *fp = NULL;
int i=0;
fp = fopen("c.txt", "w");
if(fp == NULL)
{
perror("fopen");
return;
}
for(i=0;i<n; i++)
{
fputs(buf[i], fp);
}
fclose(fp);
}
2、fgets:从文件中获取字符串
-
格式:fgets(str, n, fp);
-
说明
功能:从fp指向的文件中读取n-1个字符,在读取n-1个字符之前遇到换行符或EOF,提前结束读取并在最后加一个’\0’
str:存放数据的首地址
返回值:成功读取则返回读到字符串的首元素地址,失败则返回NULL
可以读取换行符
用来获取文件一行的数据
void test6()
{
char buf[128] = "";
FILE* fp = NULL;
char* path = "c.txt"; // path指向文件名为"c.txt"的地址
fp = fopen(path, "r");
if (fp == NULL)
{
perror("fopen");
return;
}
while (fgets(buf, sizeof(buf), fp))
{
printf("%s\n", buf);
}
fclose(fp);
}
3、综合示例
void test7()
{
FILE* fp = NULL;
char str[10] = "\0";
fp = fopen("test1.txt", "w");
fputs("XYZ123*#!", fp); // 将字符串"XYZ123*#!"写入文件指针fp指向的当前文件位置
fclose(fp);
fp = fopen("test1.txt", "r+");
fgets(str, 8, fp); // 从文件指针fp指向文件的当前位置读取7个字符存入字符数组str中
// 获取字符的长度会比参数给定的长度小1个,因为需要用于存储'\0'
puts(str);
fclose(fp);
}
4、练习:从一个文件中读取一个字符串,输出到另一个文件
void test8()
{
FILE* fp_r, * fp_w; // 文件读写指针
char str[100] = "";
fp_r = fopen("src.text", "r+");
if (fp_r == NULL) // 判断文件是否正常打开
{
perror(fopen);
return;
}
fp_w = fopen("dest.text", "w+");
if (fp_w == NULL)
{
perror(fopen);
return;
}
// 文本输入输出
fgets(str, 20, fp_r);
puts(str);
fputs(str, fp_w);
// 键盘输入,屏幕输出
fgets(str, 100, stdin);
fputs(str, stdout);
fclose(fp_r);
fclose(fp_w);
return 0;
}
(五) 文件块的读写(以二进制的形式读写数据)
1、fwrite:将数据块写入到文件中
-
格式:fwrite(buffer,size,count,fp);
-
说明
buffer:指向存储数据空间的首地址的指针
size:一次性读取数据块大小
count:要读取的数据块的个数
fp:指向要进行写操作的文件指针
返回实际读写的数据块数---count(不是总数据大小哦!!!)
void test08()
{
// 定义一个结构体,英雄名、喜爱程度、玩的次数
HERO hero[] = {
{"盾山",100, 150},
{"牛魔",80, 20},
{"鲁班大师",95, 85},
{"东皇",100, 90}
};
int n = sizeof(hero) / sizeof(hero[0]);
FILE* fp = NULL;
fp = fopen("hero.txt", "w");
if (fp == NULL)
{
perror("fopen");
return;
}
// fwrite将内存的数据原样的输出到文件中
// 写入文件的数据不便于用户查看,但是不会影响程序的读
fwrite(hero, sizeof(HERO), n, fp);
fclose(fp);
}
- 查看的时候发现和自己写入的内容不一样,这就是文件块的特点保存的快,但是查看比较麻烦
2、fread:从文件中读取数据块
-
格式:fread(buffer,size,count,fp);
-
参数与fwrite一样
void test10()
{
HERO hero[4];
int i = 0;
FILE* fp = NULL;
fp = fopen("hero.txt", "r");
if (fp == NULL)
{
perror("fopen");
return;
}
fread(hero, sizeof(HERO), 4, fp); // 结构体数组中有四组数据
for (i = 0; i < 4; i++)
{
printf("英雄姓名:《%s》,喜爱程度:《%d》,玩的次数:《%d》\n", \
(hero + i) -> name, (hero + i) ->Is_Love, (hero + i) ->Play_NUm);
}
fclose(fp);
}
3、综合示例
void test11()
{
FILE* fp = NULL;
int arr[6] = { 1, 2, 3, 4, 5, 6 };
fp = fopen("test3.txt", "wb"); // 只写的形式打开二进制文本
fwrite(arr, sizeof(int), 6, fp); // 把整型数组arr中6个大小为int类型的数据写入文件指针fp所指向的文件当前位置
fclose(fp);
int arr2[6] = { 0 };
fp = fopen("test3.txt", "rb");
fread(arr2, sizeof(int), 6, fp); // 从文件指针fp所指向文件的当前位置读取6个整型大小的数据放入数组arr2中
for (int i = 0; i < 6; i++)
printf("%d\t", arr2[i]);
putchar('\n');
fclose(fp);
}
4、练习:从键盘输入一个结构体数组数据,输出到文件,再读入并显示
void test12()
{
struct stu boya[10], boyb[2];
FILE* fp = NULL;
int i = 0;
fp = fopen("test12.txt", "wb+");
if (fp == NULL)
{
perror(fopen);
return;
}
printf("请输入数据:\n");
for (; i < 2; i++)
{
scanf("%s %d %d", boya[i].name, &boya[i].num, &boya[i].age);
}
fwrite(boya, sizeof(boya), 2, fp); // 学生信息写入文件
rewind(fp); // 重新将文件指针指向开头
fread(boyb, sizeof(boyb), 2, fp);
for (i = 0; i < 2; i++)
{
printf("%s %d %d", boyb[i].name, boyb[i].num, boyb[i].age);
}
fclose(fp);
}
(六)文件的格式化操作(格式化输入和输出函数)
-
格式化输入输出:以什么格式输入的就以什么格式输出
-
针对文件块查看不便的问题,通过格式化输入输出的方法
1、fprintf:格式化写操作
-
格式:fprintf(文件指针, 格式化字符串, 输出列表);
void test13()
{
HERO hero[] = {
{"盾山",100, 150},
{"牛魔",80, 20},
{"鲁班大师",95, 85},
{"东皇",100, 90}
};
int n = sizeof(hero) / sizeof(hero[0]);
FILE* fp = NULL;
int i = 0;
fp = fopen("hero.txt", "w");
if (fp == NULL)
{
perror("fopen");
return;
}
for (i = 0; i < n; i++)
{
fprintf(fp, "英雄:%s 喜爱程度:%d 玩的次数:%d\n", hero[i].name, hero[i].Is_Love, hero[i].Play_NUm);
}
fclose(fp);
}
2、fscanf:格式化读操作
-
格式:fscanf(文件指针, 格式化字符串, 输入列表);
void test14()
{
HERO hero[4];
int i = 0;
FILE* fp = NULL;
fp = fopen("hero.txt", "r");
if (fp == NULL)
{
perror("fopen");
return;
}
for (i = 0; i < 4; i++)
{
fscanf(fp, "英雄:%s 喜爱程度:%d 玩的次数:%d\n", hero[i].name, &hero[i].Is_Love, &hero[i].Play_NUm);
}
for (i = 0; i < 4; i++)
{
printf("%s %d %d\n", hero[i].name, hero[i].Is_Love, hero[i].Play_NUm);
}
fclose(fp);
}
3、综合示例
优点:对磁盘文件读写使用方便,容易理解,直观的查看
缺点:在输入时将ASCII码转换成二进制,在输出时将二进制转为字符,花费时间较多
struct student { // 学生结构体
int id;
char name[20];
char sex[4];
int age;
float score;
};
void test15()
{
FILE* fp = NULL;
struct student S1 = { 1, "小明", "男", 18, 90.5 }, S2;
fp = fopen("test15.txt", "w");
fprintf(fp, "%d %s %s %d %f\n", S1.id, S1.name, S1.sex, S1.age, S1.score); // 以指定的格式把数据写入文件指针fp所指向的文件中
fclose(fp);
fp = fopen("test15.txt", "r");
fscanf(fp, "%d %s %s %d %f\n", &S2.id, S2.name, S2.sex, &S2.age, &S2.score); // 以指定的格式从文件指针fp所指向的文件中读取一些数据放入结构体变量S2中
fclose(fp);
}
(七)格式化输入输出与文件块对比
文件拷贝用文件块read和fwrite函数,速度快,查看不方便
文件打印输出查看用fprintf和fcsanf函数,不在乎时间,需要的是直观可看
在内存与磁盘频繁交换数据的情况下,最好不用fprintf和fcsanf函数,而使用fread和fwrite函数