C++ 文件操作篇
文章目录
- [C++ 文件操作篇](#C++ 文件操作篇)
- [1 简介](#1 简介)
-
- [1.1 继承关系](#1.1 继承关系)
- [1.2 流](#1.2 流)
- [1.3 缓冲区](#1.3 缓冲区)
- [2 文件操作步骤](#2 文件操作步骤)
-
- [2.1 头文件](#2.1 头文件)
- [2.2 创建流对象](#2.2 创建流对象)
- [2.3 打开文件](#2.3 打开文件)
- [2.4 读取数据](#2.4 读取数据)
- [2.5 写入数据](#2.5 写入数据)
- [2.6 fstream类](#2.6 fstream类)
- [2.7 关闭文件](#2.7 关闭文件)
- [3 数据类型转换](#3 数据类型转换)
- [4 状态标志符的验证](#4 状态标志符的验证)
- [5 获得和设置流指针](#5 获得和设置流指针)
- [6 二进制文件读写](#6 二进制文件读写)
-
- [6.1 写文件](#6.1 写文件)
- [6.2 读文件](#6.2 读文件)
- [7 缓存和同步](#7 缓存和同步)
- 参考文档:
1 简介
1.1 继承关系
这里文件操作主要有:
text
ios->ostream->ofstream
ios->istream->ifstream
iostream->fstream
(还继承了两个缓冲区,一个用于输入,一个用于输出)
(此类用于文件I/O的同步,即协调地移动输入缓冲区的输入指针和输出缓冲区的输出指针)
(输入输出模式时使用fstream类)
1.2 流
在程序设计中,数据输入/输出(I/O)操作是必不可少的,C++语言的数据输入/输出操作是通过I/O流库来实现的。
C++中把数据之间的传输操作称为流,
**输出流:**数据从内存传送到某个载体或设备中
**输入流:**某个载体或设备传送到内存缓冲区变量中
- 标准I/O流:内存与标准输入输出设备之间信息的传递;
- 文件I/O流:内存与外部文件之间信息的传递;
- 字符串I/O流:内存变量与表示字符串流的字符数组之间信息的传递
流类分类 | 流类名称 | 流 类 作 用 |
---|---|---|
流基类 | ios | 所有流类的父类,保存流的状态并处理错误 |
输入流类 | istream | 输入流基类,将流缓冲区中的数据作格式化和非格式化之间的转换并输入 ifstream |
ifstream | 文件输入流类 | |
stream_withassign | cin输入流类,即操作符>>输入流 | |
istrstream | 串输入流类, 基于C类型字符串char*编写 | |
istringstream | 串输入流类, 基于std::string编写 | |
输出流类 | ostream | 输出流基类,将流缓冲区中的数据作格式化和非格式化之间的转换。并输出 |
ofstream | 文件输出流类 | |
ostream_withassign | Cout、cerr、clog的输出流类,即操作符<<输出流 | |
ostrstream | 串输入流类, 基于C类型字符串char*编写 | |
ostringstream | 串输入流类, 基于std::string编写 | |
输入/输出流类 | iostream | 多目的输入/输出流类的基类 |
fstream | 文件流输入/输出类 | |
strstream | 串流输入/输出类, 基于C类型字符串char*编写 | |
stringstream | 串流输入/输出类, 基于std::string编写 |
1.3 缓冲区
文件输出输入使用缓冲区,在声明每个ofsream or ifsream
对象时程序会为其自动分配 该对象自己的缓冲区;分为输入缓冲区 和输出缓冲区
缓冲区就是一块存储空间,它是为了匹配程序处理速度和外设处理速度;比如程序一次处理1byte,但是磁盘一次读取512bytes;又或者程序一次处理1byte,可以1byte地从磁盘读取,但是由于硬件读取一次数据复杂且操作慢,因此使用缓冲区可以加快程序处理速度。
何时清空缓存: 1.缓存区满时 2.使用文件close()方法时,为了保证关闭文件时文件被更新。
输入输出流中的缓冲streambuf
- stl(标准库)提供了缓冲区类
streambuf
,提供给输入输出流使用,每个标准的输入输出流对象均包含一个streambuf
的指针。 - 可以通过调用
rdbuf()
获取该指针,从而直接访问底层streambuf
对象 streambuf
最精彩的部分在于它支持<<,>>操作,以及迭代器操作,支持自定义。
2 文件操作步骤
2.1 头文件
c
#include <fstream>
2.2 创建流对象
c
ofstream fout("xxx.txt"); //文件写操作 内存写入存储设备
ifstream fin("xxx.txt"); //文件读操作,存储设备读区到内存中
fstream foi("xxx.txt"); //读写操作,对打开的文件可进行读写操作
2.3 打开文件
在从文件读取信息或者向文件写入信息之前,必须先打开文件。ofstream 和 fstream 对象都可以用来打开文件进行写操作,如果只需要打开文件进行读操作,则使用 ifstream 对象。
c
void open(const char *filename, ios::openmode mode); // filename : 文件路径 openmode : 打开方式
模式标志 | 描述 |
---|---|
ios::app | 追加模式。所有写入都追加到文件末尾。 |
ios::ate | 文件打开后定位到文件末尾。 |
ios::in | 打开文件用于读取。 |
ios::out | 打开文件用于写入。 |
ios::trunc | 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。 |
例如,要以写入模式打开文件,并希望截断文件,以防文件已存在
c
ofstream fout;
fout.open("file.dat", ios::out | ios::trunc );
如果想要打开一个文件用于读写
c
ifstream afile;
afile.open("file.dat", ios::out | ios::in );
*** 可以不显式的调用open()
函数 ***
c
ofstream out("xxx.txt", ios::out);
ifstream in("xxx.txt", ios::in);
fstream foi("xxx.txt", ios::in|ios::out);
/* 直接调用了其默认的打开方式,因为在stream类的构造函数中调用了open()函数,并拥有同样的构造函数,所以在这里可以直接使用流对象进行文件的操作 */
成员函数is_open()
可以对文件是否打开进行验证
2.4 读取数据
① 为了保持健壮性,读文件要有验证代码
c
ifstream fin("xxx.txt", ios::in);
if(!fin.is_open())
{
std::cerr<<"cannot open the file"
}
//或者
if(!fin)
{
std::cerr<<"cannot open the file";
}
② 读取文件
第一种:按元素直接读
c
string tmp;
ifstream fin("writein.txt");
if (!fin){
cerr << "Fail!\n";
}
else{
// 这种方法会从fin中逐个单词读取(也就是遇到空格、换行时会停止)
while (fin >> tmp)
{
cout << tmp << endl;
}
fin.close();
}
第二种:使用getline按行读
输入流成员函数getline()用法
getline是C++标准库函数;它有两种形式,一种是头文件< istream >
中输入流成员函数;一种在头文件< string >
中普通函数;
语法结构:
c
istream& getline (char* s, streamsize n );
istream& getline (char* s, streamsize n, char delim );
从istream
中读取至多n
个字符(包含结束标记符)保存在s
对应的数组中。即使还没读够n个字符,如果遇到delim
或 字数达到限制 ,则读取终止 ,delim
都不会被保存进s
对应的数组中。
- 读入了文件结束标志 EOF
- 读到一个新行
- 达到字符串的最大长度
--如果getline
没有读入字符,将返回false
,可用于判断文件是否结束
c
char buf[1021] = { 0 };
string tmp;
ifstream fin("writein.txt");
if (!fin){
cerr << "Fail!\n";
}
else{
// char *
while (fin.getline(buf, sizeof(buf)))
{
cout << buf << endl;
}
// string
while (getline(fin, tmp))
{
cout << tmp << endl;
}
fin.close();
}
第三种:使用get
c
// (不推荐,效率)
char c;
ifstream fin("writein.txt");
if (!fin){
cerr << "Fail!\n";
}
else{
while (c = fin.get() != EOF)
{
cout << c;
}
fin.close();
}
2.5 写入数据
c
string name;
ofstream outfile("writein.txt", ios::app); // 以追加模式开启文件,新数据会被加到文件尾端
cout << "Writing to the file" << endl;
cout << "Enter your name: ";
cin >> name;
outfile << name << endl;
2.6 fstream类
c
#include<fstream>
using namespace std;
int main() {
fstream file;
file.open("1.txt",ios::out); //以只写模式打开文件
char buf[] = "test";
file.write(buf,sizeof(buf));//写入文件
file.close(); //关闭文件
file.open("1.txt", ios::app); //以追加模式打开文件
char buf1[] = "test1";
file.write(buf1, sizeof(buf));//写入文件末尾
file.close(); //关闭文件
file.open("1.txt",ios::in); //以只读方式打开文件
char buf2[0xFF]; //存储读取的内容
file.read(buf2,0xFF); //读文件
file.close(); //关闭文件
}
2.7 关闭文件
当文件读写操作完成之后,我们必须将文件关闭以使文件重新变为可访问的。成员函数close()
,它负责将缓存中的数据排放出来并关闭文件 。这个函数一旦被调用,原先的流对象就可以被用来打开其它的文件了,这个文件也就可以重新被其它的进程所访问了。为防止流对象被销毁时还联系着打开的文件,析构函数将会自动调用关闭函数close
。
3 数据类型转换
一般默认从文件中读取的是字符格式或者字符串格式的数据,如果是数字要转化为float
等格式怎么办呢?
直接定义负责接受的变量数据类型,按行分后再按单词分
- 从文件中提取"行" :
fin.getline(line,sizeof(line))
- 从"行"中提取"单词":`std::stringstream word(line);
4 状态标志符的验证
- eof() :
ifstream
从类ios
中继承过来的,当到达文件末尾时返回true
。 - bad():如果在读写过程中出错,返回 true 。例如:当我们要对一个不是打开为写状态的文件进行写入时,或者我们要写入的设备没有剩余空间的时候。
- fail():除了与bad() 同样的情况下会返回 true 以外,加上格式错误时也返回true ,例如当想要读入一个整数,而获得了一个字母的时候。
- good():如果调用以上任何一个函数返回true 的话,此函数返回 false
- clear():重置以上成员函数所检查的状态标志
下面是一个eof()
的使用示例
c
#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
int main () {
char buffer[256];
ifstream in("test.txt");
if (! in.is_open())
{ cout << "Error opening file"; exit (1); }
while (!in.eof() ) // 状态标志符的验证
{
in.getline (buffer,100);
cout << buffer << endl;
}
return 0;
}
//结果 在屏幕上输出
This is a line.
This is another line
5 获得和设置流指针
所有输入/输出流对象都有至少一个流指针:
ifstream
, 类似istream
, 有一个被称为get pointer
的指针,指向下一个将被读取的元素。ofstream
, 类似ostream
, 有一个指针put pointer
,指向写入下一个元素的位置。fstream
, 类似iostream
, 同时继承了get
和put
我们可以通过使用以下成员函数来读出或配置这些指向流中读写位置的流指针:
-
tellg() 和 tellp()
这两个成员函数不用传入参数,返回pos_type 类型的值(根据ANSI-C++ 标准) ,就是一个整数,代表当前get 流指针的位置 (用tellg) 或 put 流指针的位置(用tellp)
-
seekg() 和seekp()
这对函数分别用来改变流指针get 和put的位置。两个函数都被重载为两种不同的原型:
seekg ( pos_type position );
seekp ( pos_type position );
使用这个原型,流指针被改变为指向从文件开始计算的一个绝对位置。要求传入的参数类型与函数 tellg 和tellp 的返回值类型相同。
seekg ( off_type offset, seekdir direction );
seekp ( off_type offset, seekdir direction );
使用这个原型可以指定由参数direction决定的一个具体的指针开始计算的一个位移(offset)。它可以是:
ios::beg 从流开始位置计算的位移 ios::cur 从流指针当前位置开始计算的位移 ios::end 从流末尾处开始计算的位移
流指针 get 和 put 的值对文本文件(text file)和二进制文件(binary file)的计算方法都是不同的,因为文本模式的文件中某些特殊字符可能被修改。由于这个原因,建议对以文本文件模式打开的文件总是使用seekg 和 seekp的第一种原型,而且不要对tellg 或 tellp 的返回值进行修改。对二进制文件,你可以任意使用这些函数,应该不会有任何意外的行为产生。
以下例子使用这些函数来获得一个二进制文件的大小:
c
// obtaining file size
#include <iostream.h>
#include <fstream.h>
const char * filename = "test.txt";
int main () {
long l,m;
ifstream in(filename, ios::in|ios::binary);
l = in.tellg();
in.seekg (0, ios::end);
m = in.tellg();
in.close();
cout << "size of " << filename;
cout << " is " << (m-l) << " bytes.\n";
return 0;
}
//结果:
size of example.txt is 40 bytes.
6 二进制文件读写
以二进制形式读写文件有哪些好处?
举个例子,现在要做一个学籍管理程序,其中一个重要的工作就是记录学生的学号、姓名、年龄等信息。这意味着,我们需要用一个类来表示学生,如下所示:
c
class CStudent
{
char szName[20]; //假设学生姓名不超过19个字符,以 '\0' 结尾
char szId[l0]; //假设学号为9位,以 '\0' 结尾
int age; //年龄
};
如果以文本形式存储学生的信息,则最终的文件中存储的学生信息可能是这个样子:
c
Micheal Jackson 110923412 17
Tom Hanks 110923413 18
......
要知道,这种存储学生信息的方式不但浪费空间,而且后期不利于查找指定学生的信息(查找效率低下),因为每个学生的信息所占用的字节数不同。
这种情况下,以二进制形式将学生信息存储到文件中,是非常不错的选择,因为以此形式存储学生信息,可以直接把 CStudent 对象写入文件中,这意味着每个学生的信息都只占用 sizeof(CStudent) 个字节。
- 二进制方式对文件进行读写操作时,打开方式 要指定为
ios::binary
- 如果存储类,只占用
sizeof(class)
个字节。 - 以二进制形式读写文件,<< 和 >> 将不再适用,需要使用 C++ 标准库专门提供的 read() 和 write() 成员方法。其中
- 二进制文件后缀:
.bat
6.1 写文件
流对象调用成员函数 write
:函数原型:ostream & write(const char * buffer, int len);
字符指针buffer
指向要写入文件的二进制数据的起始位置,len
是读写的字节数,返回一个调用该方法的对象的引用
下面的程序演示了如何将学生信息以二进制形式写入文件:
c
#include <iostream>
#include <fstream>
using namespace std;
struct CStudent
{
char szName[20];
int age;
};
int main()
{
CStudent s;
ofstream fout("students.dat", ios::out | ios::binary);
while (cin >> s.szName >> s.age) // ^z 结束键入
{
fout.write((char*)&s, sizeof(s));
}
fout.close();
return 0;
}
6.2 读文件
流对象调用成员函数 read
:函数原型:istream & read(char * buffer, int len);
字符指针buffer
指向读取字节的起始位置,len
是读写的字节数,返回一个调用该方法的对象的引用
下面程序演示了如何使用 read()
方法将二进制文件数据读取出来:
c
#include <iostream>
#include <fstream>
using namespace std;
struct CStudent
{
char szName[20];
int age;
};
int main()
{
CStudent s;
ifstream fin("students.dat", ios::in | ios::binary); //二进制读方式打开
if (!fin)
{
cout << "error" << endl;
return;
}
while (fin.read((char*)&s,sizeof(s)))//一直读到文件结束
{
cout << s.szName << " " << s.age << endl;
}
fin.close();
return 0;
}
7 缓存和同步
当我们对文件流进行操作的时候,它们与一个streambuf 类型的缓存(buffer)联系在一起。这个缓存(buffer)实际是一块内存空间,作为流(stream)和物理文件的媒介。例如,对于一个输出流, 每次成员函数put (写一个单个字符)被调用,这个字符不是直接被写入该输出流所对应的物理文件中的,而是首先被插入到该流的缓存(buffer)中。
当缓存被排放出来(flush)时,它里面的所有数据或者被写入物理媒质中(如果是一个输出流的话),或者简单的被抹掉(如果是一个输入流的话)。这个过程称为同步(synchronization),它会在以下任一情况下发生:
- 当文件被关闭时: 在文件被关闭之前,所有还没有被完全写出或读取的缓存都将被同步。
- **当缓存buffer 满时:**缓存Buffers 有一定的空间限制。当缓存满时,它会被自动同步。
- **控制符明确指明:**当遇到流中某些特定的控制符时,同步会发生。这些控制符包括:flush 和endl。
- 明确调用函数sync(): 调用成员函数sync() (无参数)可以引发立即同步。这个函数返回一个int 值,等于-1 表示流没有联系的缓存或操作失败。
参考文档:
成员函数详解:C++输入文件流ifstream用法详解
流的一些总结:c++输入输出流
一文读懂缓冲区
c++输入文件流ifstream用法详解
c++输出文件流ofstream用法详解