目录
前言
在前面写通讯录时,我们发现:每次程序结束后通讯录内的消息就不见了,原因是:信息都是在内存中储存;如果我们要想把信息存储起来,就需要和文件打交道,那么C语言中又是怎么对文件进行操作呢?看完这篇文章或许你就懂了!
一什么是文件
磁盘上保存的数据都叫做文件;
在程序设计中,我们一般谈的文件有两种:程序文件、数据文件;
1程序文件
包括源程序文件(后缀为.c),目标文件(后缀为.obj),可执行程序(后缀为.exe)
2数据文件
文件的内容不一定是程序,而是程序运行时读写的数据; 比如程序运行需要从中读取数据的文件,或者输出内容的文件
3文件名
用来标识文件唯一性;文件名通常是一个完整的文件路径(c:\code\test.txt)
二文件的打开与关闭
1文件指针
缓冲文件系统中,有个概念叫“文件类型指针”,简称“文件指针”; 打开一个文件时都会在内存中开辟一个结构体,用来储存文件的相关信息(文件名,大小...),C语言把这种结构体定义为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结构体,要想更好地管理它们:用类似链表的方式实现
2fopen
第一个参数是文件名(可以是相对路径(.\ ..\进行表示)或者绝对路径(完整路径));
第二个参数是你要用什么方式打开文件?
文件使用方式 | 含义 | 如果指定文件不存在 |
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
3fclose
与申请内存类似:文件操作完后也要对文件进行关闭
使用:
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen error");
return 1;
}
//使用...
fclose(pf);
pf = NULL;
return 0;
}
注意:对于文件后缀名:建议把它进行开启,否则可以会出现以下错误:
明明在当前路径下有该文件,就是找不到!原来该文件的后缀名是被隐藏掉了,真正的文件名是:
三文件的读与写
1文件的顺序读写
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
这里所说的:输入输出流是怎么一回事?
流是一种抽象的概念,我们可将其看成‘水流’(数据流):当我们要接水时:我们可以用盆接,用碗接,用手接...同样,我们要接收数据(输入流)也有多种方式,fgetc,fgets...用水的情况也有多种,我们使用数据(输出流)处理具体情况的方式也有所不同,决定了程序员在不同情况下要掌握不同函数调用来使用数据的能力;常见的输入输出流的例子: 我们平时说:用scanf接收数据,把数据打印到屏幕上:本质上就是输入输出流;那么你可能会说:我在使用scanf或者printf时是不用传入FILE*的指针变量就能完成,这怎么可能是输入输出流?
其实:在程序运行时,程序默认为我们打开了三个流:stdout(标准输出流),stdin(标准输入流),stderror(标准错误流),这三个变量都是FILE*类型!
使用scanf或者printf时底层默认使用指定的标准流来处理数据的~
1.1fputc fgetc
向指定流里面写入/读取一个字符
使用:data.txt文件写入26个字母
int main()
{
FILE* pf1 = fopen("data.txt", "w");
if (pf1 == NULL)
{
perror("fopen error");
return 1;
}
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf1);
}
fclose(pf);
pf = NULL;
return 0;
}
data.txt文件读取26个字母
int main()
{
FILE* pf = fopen("data.txt", "r");//注意这个文件操作的方式是"r"
if (pf == NULL)
{
perror("fopen error");
return 1;
}
char ch = 0;
//fgetc读到文件结尾后返回EOF(-1)
while (1)
{
ch = fgetc(pf);
if (ch == EOF) break;
printf("%c ", ch);
}
fclose(pf);
pf = NULL;
return 0;
}
使用stdout则将数据输出到屏幕上,达到与printf一样的效果
1.2fputs fgets
向指定流里写入/读取一行字符串
向data.txt中写入两行字符串
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen error");
return 1;
}
fputs("hello\n", pf);
fputs("world\n", pf);
fclose(pf);
pf = NULL;
return 0;
}
从data.txt读取第一行的三个字符
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen error");
return 1;
}
char arr[10] = { 0 };
fgets(arr, 3+1, pf);//读取的字符总数包含了\n,所以决定读n个字符参数传n+1
printf("%s", arr);
fclose(pf);
pf = NULL;
return 0;
}
1.3fprintf fscanf
格式化数据(如结构体)写入/读取数据
将格式化数据写入data.txt
#include<stdio.h>
struct S
{
float f;
char c;
int i;
};
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen error");
return 1;
}
struct S s = { 3.14,'w',100 };
fprintf(pf, "%f-%c-%d", s.f, s.c, s.i);
fclose(pf);
pf = NULL;
return 0;
}
把data.txt里的格式化数据读出来(读出的格式需要与写入的格式保持一致)
#include<stdio.h>
struct S
{
float f;
char c;
int i;
};
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen error");
return 1;
}
struct S s = {0};
//fprintf(pf, "%f-%c-%d", s.f, s.c, s.i);
fscanf(pf, "%f-%c-%d", &(s.f), &(s.c), &(s.i));//格式保存一致
printf("%f %c %d", s.f, s.c, s.i);
fclose(pf);
pf = NULL;
return 0;
}
1.4fwrite fread
以二进制格式写入/读取
使用fwrite向文件写进5个数:
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror("fopen error");
return 1;
}
int arr[] = { 1,2,3,4,5 };
fwrite(arr, sizeof(int), 5, pf);
fclose(pf);
pf = NULL;
return 0;
}
发现都是乱码(二进制),那这些到底是不是我们写进去的数据呢?
用fread读取后打印出来看看:
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
perror("fopen error");
return 1;
}
int arr[5] = {0};
fread(arr, sizeof(int), 5, pf);
for (int i = 0; i < 5; i++)
{
printf("%d ", arr[i]);
}
fclose(pf);
pf = NULL;
return 0;
}
前面的三组函数都是以文本文件进行写入/读取,而fwrite/fread以二进制文件进行写入/读取,这两者要怎么理解?
1.5文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
如果用一个整数以二进制格式写到文件中,要想看到对内容进行‘翻译’:可以将二进制文件拖到VS中以二进制编译器的形式打开:内容是整数在内存中的储存形式(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;
}
2文件的任意读写
1fseek
根据文件指针的位置和偏移量来定位文件指针(想读哪个文件指针就定位到哪里)
关于origin的参数:
使用:通过三个参数找到文件的最后一个字符
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen error");
return 1;
}
//data.txt的数据:abcde
char ch=fgetc(pf);
printf("%c\n", ch);
//读出最后一个字符e
//fseek(pf, -1, SEEK_END);//SEEK_END定位到的是\n,往前偏移一位才能找到e
//fseek(pf, 4, SEEK_SET);//开始位置完后偏移4找到e(要计算)
fseek(pf, 3, SEEK_CUR);//当前位置往后偏移3找到e(要计算)
ch = fgetc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
return 0;
}
2ftell
返回文件指针相对于起始位置的偏移量
3rewind
让文件指针从起始位置开始
四文件读取结束
网上或者教材上很多说:用 feof 函数进行判断文件是否结束,其实是不严谨的; feof作用:当文件读取结束后,判断是否遇到文件结束标志而结束 与ferrof作用对比: 当文件读取结束后,判断是否遇到错误而结束
1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets );
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数 (如果正常使用write/read 写入/读取多少个,正常来说是会返回写入/读到的个数)
使用:将test1.txt的内容拷贝到test2.txt中
#include <stdio.h>
int main(void)
{
int c; // 注意:int,非char
FILE* fp1 = fopen("test1.txt", "r");
if (!fp1)
{
perror("File opening failed");
return 1;
}
FILE* fp2 = fopen("test2.txt", "w");
if (!fp2)
{
perror("File opening failed");
fclose(fp1);
fp1 = NULL;
return 2;
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp1)) != EOF) // 标准C I/O读取文件循环
{
fputc(c, fp2);
}
fclose(fp1);
fp1 = NULL;
fclose(fp2);
fp2 = NULL;
return 0;
}
五文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”;
写入文件的数据先保存在“文件缓冲区”中,等到调用相关函数把文件缓冲区的数据刷新到文件中(close()),才是真正地把数据保存在文件中
而scanf()读键盘数据也是同理:所以我们在用scanf读入字符串时要注意缓冲区的'\n'问题
//测试文件缓冲区存在的代码
#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);//fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
六改写通讯录
学习完文件操作后,是时候解决前言提出的问题:把通讯录改造成不受程序关闭而通讯录信息丢失
1.在程序退出时,把当前通讯录的信息保存在Contact.txt文件中(使用fwrite) 2.在程序刚开始初始化通讯录时,把Contact.txt文件的数据加载到内存中(使用fread)
void CheckCapacity(Contact* con)
{
if (con->sz == con->capacity)
{
//扩容
Mess* ptr = realloc(con->message, (con->capacity + Default_Size) * sizeof(Mess));
if (ptr != NULL)
{
con->message = ptr;
con->capacity += Default_Size;
printf("扩容成功\n");
}
else
{
perror("realloc error");
return;
}
}
}
void InitContact(Contact* con)
{
//文件版本:对上一次的通讯录数据进行拷贝
con->message = (Mess*)malloc(Default_Size * sizeof(Mess));
if (con->message == NULL)
{
perror("malloc error");
return 1;
}
con->sz = 0;
con->capacity = Default_Capacity;
FILE* pf = fopen("Contact.txt", "rb");
if (pf == NULL)
{
perror("fopen fail");
return 1;
}
Mess tmp = { 0 };
while (fread(&tmp, sizeof(Mess), 1, pf))
{
CheckCapacity(con);
con->message[con->sz] = tmp;
con->sz++;
}
}
void SaveContact(Contact* con)
{
FILE* pf = fopen("Contact.txt", "wb");
for (int i = 0; i < con->sz; i++)
{
fwrite(con->message + i, sizeof(Mess), 1, pf);
}
}
以上便是全部内容,有问题欢迎在评论区指正,感谢观看!