文件操作
文章目录
1文件操作 - 创建与写入数据
文件是什么
文件用于持久地存储数据。
何为持久:断电后,数据不消失。下次加电,可以读取。
文件的属性:
- 文件名 main.cpp
- 路径 D:\Cpp\Hello
- 长度, 以字节计算
- 内容
- 只读 / 读写
文件是什么
新建一个文本文件 hello.txt
写入
“hello”
(1)观察文件属性 | 大小
(2)用UltraEdit查看 , 文本方式 | 十六进制方式
结论:文件中的存储内容和内存一样,也是数字。
使用标准C函数操作文件
ANSI C中定义了文件操作的库函数:
#include <stdio.h>
fopen: 打开一个文件
fclose: 关闭文件
fwrite: 写入数据
fread: 读出数据
保存数据到文件
保存数据到文件的步骤:
(1) fopen: 打开文件
(2) fwrite: 写入数据
(3) fclose: 关闭文件
保存数据到文件
(1) fopen: 打开文件
FILE* fopen(const char filename, const char mode);
mode: 使用"wb" (w表示write, b表示binary)
c:\abc.txt : 全路径
c:\0\abc.txt
返回值: 文件指针 FILE
其中,不用关心FILE的具体类型,只需要知道它是一个指 针就行了。
保存数据到文件
(1) fopen: 打开文件
示例:
const char* filename = "c:/aaa.txt";
FILE* fp = fopen(filename, "wb" );
if(fp == NULL)
{
printf("failed to open file!\n");
return -1;
}
保存数据到文件
(2) fclose: 关闭文件
int fclose(FILE* stream);
参数: stream就是前面fopen的返回值
示例:
fclose(fp);
保存数据到文件
(3) fwrite: 写入数据
size_t fwrite(const void *buf,
size_t size, size_t count,
FILE *stream);
参数:
stream就是前面fopen的返回值
buf, 要写入的数据
size,总是传1
count, 字节数
返回值 :字节数
示例:
char buf[] = "hello";
fwrite(buf, 1, 5, fp);
保存数据到文件
(3) fwrite: 写入数据
size_t fwrite(const void *buf, size_t size, size_t count, FILE *stream);
参数:
stream就是前面fopen的返回值
buf, 要写入的数据
size,总是传1
count, 字节数
返回值 :字节数
示例:
char buf[] = “hello”;
fwrite(buf, 1, 5, fp);
保存数据到文件
#include <stdio.h>
int main()
{
const char* filename = “c:/aaa.txt”;
FILE* fp = fopen (filename, “wb” );
if(fp == NULL)
{
printf(“failed to open file!\n”);
return -1;
}
char buf[] = “hello”;
int n = fwrite (buf, 1, 5, fp);
fclose (fp);
return 0;
}
在UltraEdit中观察文件中的数据。。。
2文件操作 - 写入数据
内容提要
- 如何写入数字
- 如何写入字符串
- 如何写入结构体
- 常见错误与问题
a) 文件路径
b) 文件后缀
c) 什么时候fopen失败
d) 如何实现追加写入
写入数字
有一个int数组,在存储到文件时,一般有两种方式:
int buf[4] = { 0xA001098, 0xB002 ,0xC003, 0xD004 };
方式1 :
直接按字节写入 fwrite(buf, 1, sizeof(buf), fp);
(对照VC的内存窗口来理解)
方式2 :格式化为字符串写入
for(int i=0; i<4; i++)
{
char text[16];
sprintf(text, “%d,” , buf[i]);
fwrite(text,1,strlen(text), fp);
}
写入数字
对于浮点数(float,double),也可以使用这两种写法
double a = 3 / 4.0;
fwrite(&a, 1, sizeof(a), fp);
或
char text[16];
sprintf(text, “%.2lf”, a);
fwrite(text, 1, strlen(text), fp);
比较:后者在保存数据时,丢失了精度,无法还原
写入字符串
字符串的两种存储方式
char buf[16] = “hello”;
方式1:按实际有效长度写入
fwrite(buf, 1, strlen(buf), fp);
方式2:按固定长度写入
fwrite(buf, 1, 16, fp);
(占用了较多的空间)
结构体的写入
结构体数据的写入:
struct Student
{
int id; // id
char gender; // 性别
char name[16]; // 年龄
};
Student someone = { 20150101, ‘M’ , “Noname” };
方式一:整体写入
fwrite(&someone, 1, sizeof(someone), fp);
(对照VC的内存窗口来理解)
方式二:将各字段分开写入
fwrite(&someone.id, 1, 4, fp);
fwrite(&someone.gender, 1, 1, fp);
fwrite(&someone.name, 1, 16, fp);
结构体的写入
两种方式的比较:后者省空间
常见错误与问题
- 路径问题
可以用斜杠,也可以用反斜杠,
比如
filename = “c:/test/abc.txt”;
filename = “c:\test\abc.txt”;
(注意:使用转义字符\)
常见错误与问题
2. 文件名后缀问题
后缀可以随意写,例如
abc.dat
abc.data
abc.mydata123
甚至没有后缀名也是可以的
c:/abc
c:/readme
只要不与该目录下的其他目录或文件重名就行。
常见错误与问题
3. 什么时候fopen返回NULL ?
当fopen返回NULL时,表示文件打开失败
FILE* fp = fopen(filename, “wb” );
if(fp == NULL)
{
printf(“failed to open file!\n”);
}
(1)目录不存在
例如,filename = “c:/test/abc.txt”;
但是 C:\test\这个目录并不存在
(2) 该文件已经存在,但是“只读”的
只读的文件以"wb"方式打开时也会失败
常见错误与问题
4. 如何实现“追加写入”?append
使用"wb"方式写入时,每次写得的东西都把原有的东西覆盖 了。。。
解决办法:使用 “ab” 作为参数 (append binary)
FILE* fp = fopen(filename, “ab” );
if(fp == NULL)
{
printf(“failed to open file!\n”);
}
① 使用"wb"模式时,每次打开文件都先清空原有的内容。(即 使你不写入新东西,原有的数据也被清空了) ② 使用"ab"模式时,每次打开文件不清空原有内容,新内容追 加在末尾。
3文件操作 - 读取数据
从文件中读取数据
也分为三步
(1)打开文件 : fopen, 得到FILE*
(2)读取数据 : fread …
(3)关闭文件 : fclose
从文件中读取数据
打开文件
const char* filename = “c:/test/aaa.xyz”;
FILE* fp = fopen(filename, “rb” );
if(fp == NULL)
{
printf(“failed to open file!\n”);
return -1;
}
注意:模式为 “rb” (read binary)
从文件中读取数据
读取数据
size_t fread(void *buf, // 存储到目标内存地址
size_t size, // 设为1
size_t nelem, // 最多读取多个字节
FILE *stream);
返回值: 实际读取到的字节的个数
从文件中读取数据
比如,定义一个128字节的缓冲区
char buf[128];
int n = fread(buf, 1, 128, fp);
观察返回值n的值
观察buf里的数据
顺序读取
如果文件很大,则无法一次读完。可以采用顺序读写,每次读取 一定长度,直到读取。
char buf[4];
while(! feof (fp))
{
int n = fread (buf, 1, 4, fp);
if( n > 0)
{
printf(“read %d bytes \n”, n);
}
}
注:使用feof函数检测文件是否已经到达末尾(EOF, End Of File)
顺序读写
顺序读取的特点是:从头开始读,依次读取。已经读过的内 容,无法回头重新读取。
(有的地方称之为“文件流”stream)
问题与错误
(1)当目标文件不存在时,fopen返回 NULL
(2)读文件时,必须以 "rb"模式打开。 如果以"wb"方式打开,是无法读到东西的
4文件操作 - 数据的存储格式
数据存储的基本原则
原则:能够写入,也能够读出并还原。
(读出指的是数据的解析和还原) 如果写到文件里,却没有办法读出,那就是一个失败的设计
例如,你2个int变量,值为:12345和6790
失败的存储方法:
char buf[128];
sprintf("%d%d", a, b);
fwrite(buf, 1, strlen(buf), fp);
可行的存储方法:
char buf[128];
sprintf("%d,%d", a, b); // 以逗号分隔
fwrite(buf, 1, strlen(buf), fp);
如果不以逗号分隔,则无法对数据进行解析。数据白存了!
数据存储的基本原则
常见的文件类型都有自己的存储格式。
bmp
jpg
mp4
mp3
数据的存储格式可以自己定义。只要能满足自己的应用需要 就可以了。
存储格式
数据的存储格式是不限的,只要你能满足“能写入、能读出 并还原”的原则,哪种方案都可以。
(不考虑方案的优劣)
下面介绍一种最简单的方案:按字节存储
存储格式
按字节存储:所有数据,在内存里的表现都是一串字节,因 此,只要将这些字节存入即可。
char / short / int : 占1,2,4个字节
float / double: 4,8个字节
数组:
字符数组:
结构体:
指针:另行讨论。
按字节存储
(1) 基本类型的变量
char/short/int/float/double型变量的存储
只需要知道变量地址和大小
// 写入
int a = 0x12345678;
int b = 0x0A0A0A0A;
fwrite(&a, 1, 4, fp); // 4个字节
fwrite(&b, 1, 4, fp); // 4个字节
// 读取
int a ,b;
fread(&a, 1, 4, fp);
fread(&b, 1, 4, fp);
按字节存储
(2) 数组的存储
地址:即数组名
字节数:手工计算
float arr[4];
fwrite(arr, 1, 4*4 , fp);
// 读取:由于不知道一共存了多个少float,需要循环读取
while(! feof(fp))
{
float a;
if(fread(&a, 1, 4, fp) <= 0)
{
break;
}
}
按字节存储
(3) 字符数组的存储 (注:这里只是描述一种比较简单的存取方式)
定长方式存取:不论有效长度是多少,统一存储32个字节
char text[32];
fwrite(text, 1, 32, fp);
fread(text, 1, 32, fp);
按字节存储
(4) 结构体的存储
有两种办法,都比较简单。
第一种办法:直接存取整个结构体。
第二种办法:把每个成员变量依次存储。
struct Student
{
int id;
char name[16];
int scores[3];
};
按字节存储
整体存取 Student s = {201501, “shaofa”, {90,90, 90} };
// 写入
fwrite(&s, 1, sizeof(s), fp);
// 读出
fread(&s, 1, sizeof(s), fp);
按字节存储
把每个成员变量依次分别存取 Student s = {201501, “shaofa”, {90,90, 90} };
// 写入
fwrite(&s.id, 1, sizeof(s.id), fp); // int
fwrite(s.name, 1, sizeof(s.name), fp);// char[]
fwrite(s.score, 1, sizeof(s.score), fp); // int[]
// 读出
fread(&s.id, 1, sizeof(s.id), fp);
fread(s.name, 1, sizeof(s.name), fp);
fread(s.score, 1, sizeof(s.score), fp);
按字节存储
(5) 指针的存储
指针要么不存储,要么存储它指向的对象的内容
(指针本身没必要存储,它只是一个地址)
struct Car
{
char maker[32]; // 制造商
int price; // 价格
};
struct Citizen
{
char name[32]; // 名字
int deposite; // 存款
Car* car; // NULL时表示没车
};
按字节存储
Car* car = (Car*) malloc(sizeof(Car));
strcpy(car->maker, “Chevrolet”);
car->price = 10;
Citizen who = { “shaofa”, 100};
who.car = car;
如何存储car的信息??显示不能存储指针,因为指针只是 一个地址。重启程序之后,要能够从文件中还原出Car的信 息才行。
按字节存储
写入文件: 存储car信息
if(who.car != NULL)
{
fwrite(“Y”, 1, 1, fp); // 存入一个字节’Y’
fwrite(who.car->maker, 1, 32, fp);
fwrite(&who.car->price,1, 4, fp);
}
else
{
fwrite(“N”, 1, 1, fp); // 存入一个字节’N’
}
按字节存储
从读文件中读出
char has = ‘N’;
fread(&has, 1, 1, fp);
if(has == ‘Y’) // 先看有没有car的信息
{
Car* car = (Car*) malloc(sizeof(Car));
fread(car->maker, 1, 32, fp);
fread(&car->price, 1, 4, fp);
}
小结
介绍一种最简单的存储方式,直接按字节存储。
大家不一定要使用这种方式,但是一定要遵守设计原则: “能写入,也要能读出并且还原!”
5文件操作 - 文件的随机访问fseek
文件的随机访问
计算机领域的两个术语:
顺序访问 : sequential access, 按顺序访问,不能跳跃
随机访问 : random access,随意跳到一个位置访问
举例,当在看一个MP4电影时,你可以拖放进度条到任意位
置访问。你在拖放(seek)的时候,实际上播放器就是用了随 机访问。
文件是不是支持“随机访问”?是由物理存储和系统驱动决 定的。一般来说,我们使用的硬盘都是支持“随机访问”的
fseek
ANSI C函数:使用fseek可以实现文件FILE*的随机访问
int fseek(FILE *stream, long offset, int mode);
注:本篇只讨论在“读”文件时候的随机访问技术
参数
stream: 文件指针
offset: 一个整数,表示偏移值
mode : 相对位置
返回值
0,操作成功;-1,操作失败
fseek
跳到第100个字节的位置
fseek(fp, 100, SEEK_SET);
跳到倒数100字节的位置
fseek(fp, 100, SEEK_END);
跳到当前位置往前100个字节
fseek(fp, -100, SEEK_CUR);
跳到当前位置往后100个字节
fseek(fp, 100, SEEK_CUR);
什么叫“当前位置”??
文件位置指示器
文件位置指示器 file-offset indicator
每个被打开的文件对象FILE*,其数据结构里都有一个位置指 示器
,表示当前的读/写位置。(当前位置到文件头的距离)
当fopen打开文件时,位置指示器的值为0
当fread读取字节时,位置指示器的值会增加相应的字节数
例如,读取128个字节,则位置指示器的值就增加 128,继续 fread,则继续增加
当fseek时,会调整位置指示器的值
例如,fseek(fp, 100, SEEK_SET)则位置 指示器的值被设定 为100。fseek(fp, 100, SEEK_END)则指示器的值为filesize100。
文件位置指示器
举例:
文件中按字节保存了100个struct对象的数据,每个对象使 用了字节数是一样的。
读第80个对象的数据
Student stu;
fseek(fp, 79* sizeof(Student), SEEK_SET);
fread(&stu, 1, sizeof(Student), fp);
fseek不宜频繁调用
在物理上,硬盘、U盘等外部存储器属于“慢速存储设备”, 不能频繁的读写。
(1)速度慢、效率低
(2)降低设备的使用寿命
在fseek的时候,每一次fseek都要移动物理“磁头”,因而 不能频繁的fseek,以免损坏物理设备。
(不必关心“磁头”的物理机制,只需要知道有一个机械的 读写器就可以了)
fseek不宜频繁调用
通常的做法,一次性读取一批数据到内存,然后在内存中处 理数据。
注:一般来说,读取4个字节和读取4096个字节,对于硬盘 来说所需要的操作是相同的。因为硬盘一般单次读写的最小 单位是几K,比如4096字节。
常见问题
同时打开一个相同的文件,有什么影响?
例如,fp1和fp2操作的是同一样文件,
FILE* fp1 = fopen(“abc.xyz”, “rb”);
FILE* fp2 = fopen(“abc.xyz”, “rb”);
答:
(1)同时读一个文件是允许的,但同时写一个文件是不可 以的。 (2)fp1和fp2各自己记录一个文件位置,逻辑上互不影响 。
(3)但是在物理上,同时读一个文件时,速度会变慢,效 率会变低。由于fp1和fp2的“当前位置”可能不同,可以想像一下“磁头”来回移动的情形。
常见问题
注意,这里全部使用的rb
模式来打开文件。
如果你坚持使用从其他旧的教程里学到的rt模式,所有遇到 的问题请自行解决。(text)
读者请永不使用wt,rt,at模式,具体原因后面略有讲述。
6文件操作 - 以文本形式存储
当数据量比较小时,可以用文本形式存储。就是把所有数据 均格式化成字符串来存储。
通常配置文件是以文本形式存储的,例如*.xml *.cfg *.ini。
比如,在xml文件中存储ip和端口号:
<Root> <ip> 192.168.10.60 </ip> <port> 8080 </port> </Root>
以文本形式存储
存储的原则仍然不变,以能够读出并解析为准。(允许自己定义 格式,只要能写入能读出就可以)
下面介绍一种按行存储的实现方法。
按行存储
把每个单元的数据格式化为一行(末尾加上\n),写入文件。
例如,保存ip和port
char ip[]=“192.168.1.100”;
int port = 8080;
char line[256];
sprintf(line, “ip=%s\n”, ip);
fwrite(line, 1, strlen(line), fp);
sprintf(line, “port=%d\n”, port);
fwrite(line, 1, strlen(line), fp);
按行存储
也可以直接使用fprintf函数
char ip[]=“192.168.1.100”;
int port = 8080;
fprintf (fp, “ip=%s\n”, ip);
fprintf (fp, “port=%d\n”, port);
fprintf:第一个参数是文件指针,后面的参数和printf,直 接将数据格式化成字符串并写入文件。
按行解析
解析时复杂度相对较高
(1) 按行读取,每次读取一行。 由于不知道每行是多长,所以用fread读取时,需要检测是 否已经读到了 \n这个分隔符。(有点复杂)
推荐使用fgets函数,这个函数已经把上面的逻辑给封装好 了。fgets内部会检查,当读到字符\n时,停止读取。返回 实际读取的字节长度。
按行解析
(2) 对读取到的每一行进行解析
char buf[512];
while(! feof(fp))
{
char* line = fgets(buf, 512, fp);
if(line)
{
printf(“got: %s”, line);
}
}
注: line末尾有一个\n字符
例
保存结构体时,可以把结构体的数据格式化为一行。例如,
struct Student
{
int id; // id char gender; // 性别
char name[16]; // 年龄
};
Student someone = { 20150101, ‘M’ , “Noname” };
fprintf(“id=%d,gender=%c,name=%s\n”,
someone.id, someone.gender, someone.name);
注意
按行解析一行,一般用sscanf是无法胜任的。
比如,
char line[] = “id=123,name=shaofa,hometown=anhui”;
int id;
char name[32];
char hometown[32];
sscanf(line, “id=%d,name=%s, hometown=%s”, &id, name, hometown);
sscanf只适合提取数字,不能提取字符串!
小结
1.可以用fprintf,将数据格式化成文本保存。每行内容后 面加一个\n作为分隔符。
2 用fgets函数读取每一行。接着对每一行的内容解析。 (文本解析是个难点)
3 文本方式存储数据时,数据量不能太大。(因为处理速 度慢)。 当数据量太大时,使用“按字节方式存储”