目录
1.为什么使用文件
我们前面学习结构体时,写了通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。我们在想既然是通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
2.什么是文件
磁盘上的文件是文件但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)
2.1.程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)
2.2.数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件
注:
1.程序文件和数据文件没有严格的划分,如果一个程序文件从一个.c文件读取或写入数据,那么这个.c文件就是一个数据文件。所以我们应该观察的是此时此刻谁是程序文件谁是数据文件
2.在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
3.本章讨论的是数据文件。
2.3.文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。文件名包含 3 部分:文件路径 + 文件名主干 + 文件后缀例如: c:\code\test.txt
c:\code\ test .txt 文件路径 文件名主干 文件后缀 为了方便起见,文件标识常被称为 文件名 。
3.文件的打开和关闭
3.1.文件指针
缓冲文件系统中,关键的概念是 “ 文件类型指针 ” ,简称 “ 文件指针 ” 。每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
struct _iobuf
{
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
不同的 C 编译器的 FILE 类型包含的内容不完全相同,但是大同小异。每当打开一个文件的时候,系统会根据文件的情况自动创建一个 FILE 结构的变量,并填充其中的信息,使用者不必关心细节。一般都是通过一个 FILE 的指针来维护这个 FILE 结构的变量,这样使用起来更加方便。
FILE* pf; //文件指针变量
定义 pf 是一个指向 FILE 类型数据的指针变量。可以使 pf 指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联 的文件 。比如:
3.2.文件的打开和关闭
3.2.1.fopen函数
参数:
const char *filename:文件名
当要打开的文件在项目文件(项目文件为里面有.c的文件)中时文件名为:
文件名主干+文件后缀
当要打开的文件不在项目文件夹中时文件名为:
文件路径+文件名主干+文件后缀
const char *mode:打开方式
返回:
FILE *:返回和文件相关联的文件信息区的起始地址,如果打开失败,比如上图指定文件不存在的出错,那么返回一个空指针,"w"等使用方式也可能打开失败,比如文件没有访问权限或硬件问题等
使用方式:
3.2.2.fclose函数
参数:
FILE *stream:和文件相关联的文件信息区的起始地址
使用方式:
3.2.3.示例代码
4.文件的顺序读写
4.1.fputc函数
参数:
int c:要输出到文件的字符
FILE *stream:和文件相关联的文件信息区的起始地址
注:
当使用fputc函数输出到文件中时,也就是输出到文件流时,需要打开文件,使用fputc函数输出,然后关闭文件
当使用fputc函数输出到屏幕上时不需要打开屏幕等操作,因为c语言程序只要运行起来,就默认打开三个流:
stdin:标准输入流
stdout:标准输出流
stderr:标准错误流
打印到屏幕上就是传到标准输出流中,而标准输出流运行程序就自动打开,因为fputc函数适用于所有输出流,所以如果要打印到屏幕上,在FILE *stream参数那写成stdout即可
fputc代码1:
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写文件
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
fclose(pf);
pf = NULL;
return 0;
}
fputc代码1输出到文件的结果:
fputc代码2:
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, stdout);
}
return 0;
}
fputc代码2输出到屏幕的结果:
4.2.fgetc函数
参数:
FILE *stream:和文件相关联的文件信息区的起始地址
返回:
int:返回读取的字符,如果遇到文件结束或错误的时候返回EOF
fgetc代码:(经过前面fputc函数输出,文件里面已经有了a~z字符)
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
fclose(pf);
pf = NULL;
return 0;
}
fgetc从文件中输入数据到内存中然后打印结果:
注:
1.这里面每次用fgetc函数进行读取,结构体文件指针pf中会有成员变量记录读取的字符位置,下一次使用fgetc函数读取时,读取到的就是下一个字符的地址,如下图所示
2.因为读取错误或文件结束返回的是EOF,EOF的值是-1,所以返回类型应该是int,用int类型变量接收
4.3.fputs 函数
参数:
const char *string:要输出到文件的字符串
FILE *stream:和文件相关联的文件信息区的起始地址
fputs代码1:
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
fputs("hello world\n", pf);
fputs("hehe\n", pf);
fclose(pf);
pf = NULL;
return 0;
}
fputs代码1输出到文件的结果:
fputc代码2:
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
fputs("hello world\n", stdout);
fputs("hehe\n", stdout);
return 0;
}
fputc代码2输出到屏幕的结果:
4.4.fgets函数
参数:
char *string:存储读取到字符串的数组名
int n:读取字符的个数(真正读取n-1个)
FILE *stream:和文件相关联的文件信息区的起始地址
返回:
char *:返回的是存储字符串string的数组名(起始地址),如果遇见错误或文件结束条件的话,返回一个空指针NULL
fgets代码1:(经过前面fputs函数输出,文件里面第一行hello world第二行hehe)
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
char buf[1000] = {0};
//读文件
fgets(buf, 3, pf);
printf("%s", buf);
fgets(buf, 3, pf);
printf("%s", buf);
fclose(pf);
pf = NULL;
return 0;
}
fgets从文件中输入数据到内存中然后打印结果:
注:
这里面其实第一个fgets函数输入了两个字符,第二个fgets函数又输入两个字符
fgets代码2:(经过前面fputs函数输出,文件里面第一行hello world第二行hehe)
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
char buf[1000] = {0};
//读文件
fgets(buf, 1000, pf);
printf("%s", buf);
fgets(buf, 1000, pf);
printf("%s", buf);
fclose(pf);
pf = NULL;
return 0;
}
fgets从文件中输入数据到内存中然后打印结果:
4.5.函数实例运用
实现一个代码将data.txt 拷贝一份 生成data2.txt
代码:
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE* pr = fopen("data.txt", "r");
if (pr == NULL)
{
printf("open for reading: %s\n", strerror(errno));
return 0;
}
FILE* pw = fopen("data2.txt", "w");
if (pw == NULL)
{
printf("open for writting: %s\n", strerror(errno));
fclose(pr);
pr = NULL;
return 0;
}
//拷贝文件
int ch = 0;
while ((ch = fgetc(pr)) != EOF)
{
fputc(ch, pw);
}
fclose(pr);
pr = NULL;
fclose(pw);
pw = NULL;
return 0;
}
拷贝结果:
4.6.fprintf函数
参数:
FILE *stream:和文件相关联的文件信息区的起始地址
const char *format [, argument ]...:该参数与printf函数括号内参数完全相同
fprintf代码1:
#include<stdio.h>
#include <errno.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
double d;
};
int main()
{
struct Stu s = { "张三", 20, 95.5 };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写格式化的数据
fprintf(pf, "%s %d %lf", s.name, s.age, s.d);
fclose(pf);
pf = NULL;
return 0;
}
fprintf代码1输出到文件的结果:
fprintf代码2:
#include<stdio.h>
#include <errno.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
double d;
};
int main()
{
struct Stu s = { "张三", 20, 95.5 };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写格式化的数据
fprintf(stdout, "%s %d %lf", s.name, s.age, s.d);
fclose(pf);
pf = NULL;
return 0;
}
fprintf代码2输出到屏幕的结果:
4.7.fscanf函数
参数:
FILE *stream:和文件相关联的文件信息区的起始地址
const char *format [, argument ]...:该参数与scanf函数括号内参数完全相同
fscanf代码2:(经过前面fprintf函数输出,文件里面为:张三 20 95.500000)
#include<stdio.h>
#include <errno.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
double d;
};
int main()
{
struct Stu s = { 0 };
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读格式化的数据
fscanf(pf, "%s %d %lf", s.name, &(s.age), &(s.d));
printf("%s %d %lf\n", s.name, s.age, s.d);
fclose(pf);
pf = NULL;
return 0;
}
fscanf从文件中输入数据到内存中然后打印结果:
4.8.fwrite函数
参数:
const void *buffer:指针指向的要被写入的数据地址
size_t size:一个元素的大小,单位是字节
size_t count:要写入元素的个数
FILE *stream:和文件相关联的文件信息区的起始地址
fwrite代码:
#include<stdio.h>
#include <errno.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
double d;
};
int main()
{
struct Stu s[2] = { {"张三", 20, 95.5} , {"lisi", 16, 66.5}};
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//按照二进制的方式写文件
fwrite(s, sizeof(struct Stu), 2, pf);
fclose(pf);
pf = NULL;
return 0;
}
fwrite代码输出到文件的结果:
注:write是以二进制的方式将数据写入文件中,如果用记事本直接打开,记事本这个工具打开的时候是以文本的形式解析文件里面的内容的,所以解析出来是乱码的
4.9.fread函数
参数:
void *buffer:指针接收从文件读取的数据
size_t size:一个元素的大小,单位是字节
size_t count:要写入元素的个数
FILE *stream:和文件相关联的文件信息区的起始地址
返回:
size_t:返回读到的完整元素的个数
fread代码:(经过前面fwrite函数输出,文件里面为二进制的:张三 20 95.500000lisi 16 66.5)
#include<stdio.h>
#include <errno.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
double d;
};
int main()
{
struct Stu s[2] = {0};
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//按照二进制的方式读文件
fread(s, sizeof(struct Stu), 2, pf);
printf("%s %d %lf\n", s[0].name, s[0].age, s[0].d);
printf("%s %d %lf\n", s[1].name, s[1].age, s[1].d);
fclose(pf);
pf = NULL;
return 0;
}
fread从文件中输入数据到内存中然后打印结果:
4.10.通讯录的优化(可以将数据保存起来)
通讯录:
1.可以存放1000个人的信息
2.人的信息:名字、年龄、性别、电话、住址
功能:
1.增加联系人(完成)
2.删除联系人(完成)
3.查找联系人(先不完成)
4.修改联系人(先不完成)
5.排序(名字/年龄)(先不完成)
6.展示所有人信息(完成)
0.退出(完成)
test.c代码:
#define _CRT_SECURE_NO_WARNINGS 1
//通讯录:
//1. 可以存放1000个人的信息
//2. 人的信息:名字,年龄,电话,住址,性别
//3. 增加联系人
//4. 删除联系人
//5. 查找联系人
//6. 修改联系人
//7. 排序(名字 / 年龄)
#include "contact.h"
enum Oprion
{
EXIT,//0
ADD,
DEL,
SEARCH,
MODIFY,
SORT,
SHOW
};
void menu()
{
printf("***************************************\n");
printf("******** 1.add 2.del *****\n");
printf("******** 3.search 4.modify *****\n");
printf("******** 5.sort 6.show *****\n");
printf("******** 0.exit *****\n");
printf("***************************************\n");
}
int main()
{
int input = 0;
Contact con = {0};//通讯录
//初始化通讯录
InitContact(&con);
do
{
menu();
printf("请选选择:>");
scanf("%d", &input);
switch (input)
{
case ADD:
AddContact(&con);
break;
case DEL:
DeleteContact(&con);
break;
case SEARCH:
break;
case MODIFY:
break;
case SORT:
break;
case SHOW:
ShowContact(&con);
break;
case EXIT:
//保存信息到文件
SaveContact(&con);
DestroyContact(&con);
printf("退出通讯录\n");
break;
default:
printf("选择错误\n");
break;
}
}while(input);
return 0;
}
contact.h代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#define MAX 1000
#define NAME_MAX 20
#define SEX_MAX 5
#define ADDR_MAX 30
#define TELE_MAX 12
#define DEFAULT_SZ 3
typedef struct PeoInfo
{
char name[NAME_MAX];
int age;
char sex[SEX_MAX];
char addr[ADDR_MAX];
char tele[TELE_MAX];
}PeoInfo;
//通讯录的结构体
typedef struct Contact
{
//PeoInfo data[MAX];
PeoInfo* data;//存放数据
int sz;//通讯录中有效信息的个数
int capacity;//记录当前通讯录的最大容量
}Contact;
//初始化通讯录
void InitContact(Contact* pc);
//销毁通讯录
void DestroyContact(Contact* pc);
//增加联系人到通讯录
void AddContact(Contact* pc);
//打印通讯录中的信息
void ShowContact(const Contact* pc);
void DeleteContact(Contact* pc);
//保存信息到文件
void SaveContact(Contact* pc);
void LoadContact(Contact* pc);
contact.c代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include "contact.h"
void check_capacity(Contact* pc)
{
assert(pc);
if (pc->sz == pc->capacity)
{
//增加容量
PeoInfo* tmp = (PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(PeoInfo));
if (tmp != NULL)
{
pc->data = tmp;
pc->capacity += 2;
printf("增容成功\n");
}
else
{
printf("check_capacity()::%s\n", strerror(errno));
}
}
}
void LoadContact(Contact* pc)
{
//从文件加载数据了
FILE* pf = fopen("contact.txt", "rb");
if (pf == NULL)
{
printf("InitContact:: open for reading : %s\n", strerror(errno));
return;
}
//读取文件
PeoInfo buf = { 0 };
//pc->sz
while (fread(&buf, sizeof(PeoInfo), 1, pf))
{
check_capacity(pc);
pc->data[pc->sz] = buf;
pc->sz++;
}
//关闭文件
fclose(pf);
pf = NULL;
}
void InitContact(Contact* pc)
{
assert(pc);
pc->sz = 0;
PeoInfo* tmp = (PeoInfo*)malloc(DEFAULT_SZ *sizeof(PeoInfo));
if (tmp != NULL)
{
pc->data = tmp;
}
else
{
printf("InitContact()::%s\n", strerror(errno));
return;
}
pc->capacity = DEFAULT_SZ;
LoadContact(pc);
}
void DestroyContact(Contact* pc)
{
assert(pc);
free(pc->data);
pc->data = NULL;
pc->sz = 0;
pc->capacity = 0;
}
void AddContact(Contact* pc)
{
assert(pc);
check_capacity(pc);
//输入联系人
printf("请输入名字:>");
scanf("%s", pc->data[pc->sz].name);
printf("请输入年龄:>");
scanf("%d", &(pc->data[pc->sz].age));
printf("请输入性别:>");
scanf("%s", pc->data[pc->sz].sex);
printf("请输入电话:>");
scanf("%s", pc->data[pc->sz].tele);
printf("请输入地址:>");
scanf("%s", pc->data[pc->sz].addr);
pc->sz++;
printf("增加联系人成功\n");
}
void ShowContact(const Contact* pc)
{
assert(pc);
int i = 0;
printf("%-10s\t%-5s\t%-5s\t%-13s\t%-20s\n", "名字", "年龄", "性别", "电话","地址");
for (i = 0; i < pc->sz; i++)
{
printf("%-10s\t%-5d\t%-5s\t%-13s\t%-20s\n",
pc->data[i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
}
}
int FindByName(const Contact* pc, char name[])
{
int i = 0;
for (i = 0; i < pc->sz; i++)
{
if (strcmp(pc->data[i].name, name) == 0)
{
return i;
}
}
return -1;//找不到
}
void DeleteContact(Contact* pc)
{
char name[NAME_MAX] = {0};
if (pc->sz == 0)
{
printf("通讯录为空,无法删除\n");
return;
}
printf("请输入要删除人的名字:>");
scanf("%s", name);
//查找指定联系人
int pos = FindByName(pc, name);
if (pos == -1)
{
printf("要删除的人不存在\n");
return;
}
else
{
//删除
int j = 0;
for (j = pos; j < pc->sz-1; j++)
{
pc->data[j] = pc->data[j + 1];
}
pc->sz--;
printf("删除指定联系人成功\n");
}
}
void SaveContact(Contact* pc)
{
//打开文件
FILE* pf = fopen("contact.txt", "wb");
if (pf == NULL)
{
printf("SaveContact:: %s\n", strerror(errno));
return;
}
//写文件
int i = 0;
for (i = 0; i < pc->sz; i++)
{
fwrite(pc->data+i, sizeof(PeoInfo), 1, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
}
4.11.sprintf和sscanf函数
4.11.1sprintf和sscanf函数的介绍
参数:
char *buffer:接收格式化数据的字符串
const char *format [, argument] ... :该参数与printf函数括号内参数完全相同
功能:
写一个格式化的数据放到字符串中
参数:
const char *buffer:要读取里面有格式化数据的字符串
const char *format [, argument ] ...:该参数与scanf函数括号内参数完全相同
功能:
在字符串中将格式化的数据提取出来
代码:
#include<stdio.h>
#include <errno.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
double d;
};
int main()
{
struct Stu s = { "张三", 20, 95.5 };
struct Stu tmp = { 0 };
char buf[100] = { 0 };
sprintf(buf, "%s %d %lf", s.name, s.age, s.d);//
printf("%s\n", buf);
sscanf(buf, "%s %d %lf", tmp.name, &(tmp.age), &(tmp.d));
printf("%s %d %lf\n", tmp.name, tmp.age, tmp.d);
return 0;
}
运行结果:
4.11.2.总结
scanf:从标准输入流(stdin)上进行格式化输入的函数
printf:向标准输出流(stdout)上进行格式化的输出函数
fscanf:可以从标准输入流(stdin)/指定的文件流上读取格式化的数据
fprintf:把数据按照格式化的方式输出到标准输出流(stdout)/指定的文件流
sscanf:可以从一个字符串中提取(转化)出格式化数据
sprintf:把一个格式化的数据转换成字符串
5.文件的随机读写
5.1.fseek函数
参数:
FILE *stream:和文件相关联的文件信息区的起始地址
long offset:偏移量(偏移量往后偏移是正的,往前偏移是负的)
int origin:下面三种选择
SEEK_SET:从当前文件开始的位置开始偏移
SEEK_CUR:从当前文件指针的位置开始偏移
SEEK_END:从当前文件结束的位置开始偏移
功能:
修改当前文件指针指向文件的位置
代码1:(test.txt文件中存的是abcdef)
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE * pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
//定位文件指针
fseek(pf, 3, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);//f
fclose(pf);
pf = NULL;
return 0;
}
运行结果1:
注:
1.经过前面两次fgetc函数读取之后,当前文件指针的位置应该指向了字符c的位置,然后使用fseek函数使得文件指针的位置向后偏移3个偏移量指向了f的位置,然后fgetc函数获取并进行打印
2.上面代码的fseek(pf, 3, SEEK_CUR)可以替换成fseek(pf, 5, SEEK_SET)或fseek(pf, -1, SEEK_END)
代码2:
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写文件
int ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
//定位文件指针
fseek(pf, -2, SEEK_END);
fputc('#', pf);
fclose(pf);
pf = NULL;
return 0;
}
运行后test.txt文档中的内容:
5.2.ftell函数
参数:
FILE *stream:和文件相关联的文件信息区的起始地址
返回:
long:返回文件指针相对于起始位置的偏移量
代码:(test.txt文件中存的是abcdef)
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
int ret = ftell(pf);
printf("%d\n", ret);
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
5.3.rewind函数
参数:
FILE *stream:和文件相关联的文件信息区的起始地址
注:
1.rewind(pf)与fseek(pf, 0, SEEK_SET)功能相同
代码:(test.txt文件中存的是abcdef)
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
int ret = ftell(pf);
printf("%d\n", ret);//2
rewind(pf);
ret = ftell(pf);
printf("%d\n", ret);//0
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
6.文本文件和二进制文件
根据数据的组织形式,数据文件被称为 文本文件 或者 二进制文件 。数据在内存中以二进制的形式存储,如果不加转换的输出到外存(写到文件里面去),就是 二进制文件(打开文件我们看不懂) 。如果要求在外存上以 ASCII 码的形式存储,则需要在存储前转换。以 ASCII 字符的形式存储的文件就是 文 本文件(打开文件我们能看懂) 。
一个数据在内存中是怎么存储的呢?字符一律以 ASCII 形式存储,数值型数据既可以用 ASCII 形式存储,也可以使用二进制形式存储如有整数10000以文本的形式输出到磁盘,也就是以 ASCII 码的形式输出到磁盘,则磁盘中占用 5个字节(10000被看成是5个字符,每个字符一个字节,1的ASCII码为49二进制存储110001,0的ASCII 码为48二进制存储110000)以二进制形式输出到磁盘,则在磁盘上只占4 个字节( VS2013 测试)
代码:
#include<stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf); //二进制的形式写到文件中
fclose(pf);
pf = NULL;
return 0;
}
运行后test.txt文件:
注:
1.因为是以二进制的形式写入的,所以打开文件里面的内容是看不懂的。我们在vs编译器中源文件添加现有项并添加test.txt文件。
右击test.txt文件打开方式里面有二进制编辑器,用二进制编辑器打开它
打开后如下图所示,10000的二进制为0000 0000 0000 0000 0010 0111 0001 0000转换成十六进制就是0x 00 00 27 10,内存中是小端存储应该为10 27 00 00,如下图所示,下图中为了好展示所以显示是十六进制,其实里面放的是二进制数据
7. 文件读取结束的判定
文件读取函数:
fgetc:以字符的方式读
fgets:以文本行的方式读
fread:以二进制的方式读
7.1.被错误使用的feof
牢记:在文件读取过程中,不能用 feof 函数的返回值直接用来判断文件的是否结束。而是 应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束
1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )fgetc 判断是否为 EOFfgets 判断返回值是否为 NULL2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。fread判断返回值是否小于实际要读的个数
代码1:(文本文件例子)
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c; // 注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if(!fp)
{
perror("File opening failed");
return EXIT_FAILURE;
}
//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);
}
注:
1.ferror函数:将流传给ferror函数,如果函数返回真(非零)说明读取的时候发生了I/O错误而停下来
2.feof函数:将流传给feof函数,如果函数返回真(非零)说明是正常读取遇到文件结束而结束了;如果函数返回假说明不是正常结束,遇到其他问题结束了
代码2:(二进制文件例子)
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = {1.0,2.0,3.0,4.0,5.0};
FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组
fclose(fp);
double b[SIZE];
fp = fopen("test.bin","rb");
size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组
if(ret_code == SIZE)
{
puts("Array read successfully, contents: ");
for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
putchar('\n');
}
else
{ // error handling
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp))
{
perror("Error reading test.bin");
}
}
fclose(fp);
}
8. 文件缓冲区
ANSIC 标准采用 “ 缓冲文件系统 ” 处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“ 文件缓冲区 ” 。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C 编译系统决定的。
例如使用printf函数在屏幕上打印信息,printf函数会调用系统调用api,让操作系统在屏幕上打印信息,但是操作系统有很多事情要处理,所以有了缓冲区,缓冲区先攒着要操作系统做的事,等攒满了之后,操作系统一起处理。如果给操作系统一个指令不想等缓冲区满了执行,需要立即执行,那么刷新一下缓冲区即可,printf函数打印的时候后面加个\n,\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);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
注:
1.上面代码fputs将字符串"abcdef"输出到文件中,其实不会立马输出,而是先将"abcdef"放在输出缓冲区中,可以借助Sleep睡眠进行检验,因为fputs已经执行,在代码运行睡眠的10秒中打开文件进行查看,可以看到文件没有被写入,因此可以验证是先放入了缓冲区。
2.fflush函数是用来刷新缓冲区的,此时会将缓冲区的内容进行输出,,可以借助Sleep睡眠进行检验,因为已经刷新了缓冲区,在代码运行睡眠的10秒中打开文件进行查看,可以看到文件已经被写入了。
3.在使用fclose关闭文件的时候,其实也会先刷新缓冲区,然后再关闭文件
4.因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。
5.有些时候,比如如下代码,在vs编译器下进行,printf函数后面不加\n也会刷新缓冲区,会每隔一秒打印一次hehe;但是在gcc编译器下进行,printf函数后面不加\n不会刷新缓冲区,因此不会每隔一秒打印一次hehe,而是等到把缓冲区放满,一次性打印很多个hehe,这是因为不同的编译器对缓冲区的实现方式有所差异