在c++中引入了stream,一开始接触这个的时候感觉无法正确的理解stream的用法,在写项目的时候要用到读写文件,慢慢理解了stream的一些基本的用法。
-
overview
首先我们来看一下fstream的基本信息:
可以看到
fstream
是从
iostream
中继承来的,那么
iostream
的方法
fstream
,我们再来看看c++中提供的I/O类:
图中表明了各个类的关系,我们主要来看看
fstream
中的内容,
fstream
有4个类:
ifstream
fstream
ofstream
filebuf
其中前三个类中ifstream
和ofstream
的意思从字面上很好理解:
ifstream
将文件作为输入的源ofstream
将文件作为输出destinationfstream
在定义文件流对象是可以自己设置为in
或者out
定义了文件流对象后,每个文件流对象维护自己的filebuf
,filebuf
是与文件关联的缓冲区,filebuf
对象在其内部是通过操作一个中间的输入/输出缓冲区,该文件内容的改变会与其同步(在对文件操作完后显示的调用sync或者文件关闭之后)
-
不同I/O类型的关系
- 从概念上来说,不同的设备和不同的
char
(char
、wchar
)的size
对I/O操作没有影响。例如:
我们可以用
cin
从控制台、文件、string
中获取输入;
同理也可以用cout
向不同控制台、文件、string
中输出
c++抹平了这些是通过继承实现的,ifstream
和istringstream
是从istream
中继承来的,那么继承保证我们在使用ifstream
和istringstream
时好像在使用istream
(cin
)一样。
I/O类不允许拷贝和赋值(将拷贝构造和赋值构造声明为私有方法并不予以实现)
-
fstream
我们来看一个简单的示例程序:
#include
#include
int main(int argc, char **argv)
{
std::ofstream my_file;
my_file.open("example.txt");
my_file << "Contents from ofstream.\n"
my_file.close()
return 0;
}
以上的代码会往example.txt
文件(若当前路径下存在则直接写入,不存在则会创建对应的文件)中写入我们要写的内容,和cout
向屏幕输出一样,只是这里屏幕变成了文件而已。
从以上的代码入手,我们来一步一步看看文件流的用法。
-
打开文件
开文件就是我们需要将文件与文件对象关联,一个打开的文件在程序中的呈现方式是以文件流对象存在的,对流对象的任何操作都会应用到物理文件上,打开一个文件的动作是:
open(filename, mode)
其中
filename
是文件名,类型是字符串,mode
是一个可选的参数,可以从以下组合中选择:
以上的每个可选参数可以通过位操作的|
来组合,例如我们想将文件以二进制的形式打开,并向其尾巴部添加内容:
std::ofstream my_file("example.txt", ios::out | ios::app | ios::binary);
每一个fstream
都有自己的默认模式:
通过以上的表格,ifstream
和ofstream
即使在定义的时候,其mode
参数默认为in
或者out
,对于fstream
,默认的mode
被应用只在fstream
的mode
参数没有传入任何值的时候才会组合ios::in
和ios::out
,但是只要mode
中有一个参数被选择,那么默认的参数就会被重写,不考虑先前的组合的mode
'fstream'分别定义了三个构造函数会自动的调用open()
函数,我们可以这样来定义一个fstream
对象:
std::fstream my_file("example.txt", ios::out | ios::app | ios::binary);
但是打开文件可能会出现打不开的异常情况,标准库提供了一个函数is_open()
去检测文件是否打开成功:
if(my_file.is_open()){
/*open is successful, proceed with output*/
}
-
关闭文件
就像我们开水龙头必然要关水龙头一样,我们打开文件必须要关闭文件,文件一直打开会消耗系统资源。在我们完成了对我文件的输入或输出操作后关闭文件后,对应的文件资源变得可用。
my_file.close();
以上的close()
调用将会刷新与之相关的缓冲区并将文件关闭。在关闭之后其原先的文件流对象可以绑定其他文件了,并且该文件可以被其他进程打开。值得注意的是,在实际的编写代码的过程中我们经常会忘记调用close()
,但是在文件流对象被析构的时候与之关联的文件也会被关闭,但是我认为自己主动去关闭文件是个好习惯,最好不要把这个任务交给编译器。
-
文本文件
在打开文件的的模式中不加入ios::binary
的打开方式是以文本的方式去打开文件,在这里存在一个问题是,对于格式化输出(\n
、\t
等转移字符)的文本,会有一个转换的过程,这里的行为就像cout
向屏幕输出一样。
#include
#include
int main(int argc, char **argv)
{
std::ofstream my_file("example.txt");
if(my_file.is_open()){
my_file << "This is a line.\n";
my_file << "This is another line.\n";
}else{
std::cout <<"Unable to open file";
}
return 0;
}
文件的内容如下:
example.txt:
This is a line.
This is another line.
从文件中输入的行为类似与我们从cin
中获取输入的行为类似,这里不举例。
-
检测打开文件的文件流对象的状态
标准库提供了一系列成员函数去检测文件流对象的状态。
bad()
对文件的读写操作失败时返回true,例如,当我们试图去向一个未打开的文件执行写入操作或者向文件写入时文件空间不足的时候会返回true
fail()
行为和
bad()
类似,但是在在读写文件内容,内容格式的错误的时候也会返回true,例如我们去读一个整数的时候,但是一个字母被读入的时候,这时候'fail()'会返回true
eof()
读文件到文件尾的时候返回true
good()
在以上介绍的所有状态中只要有一个返回true
good()
就返回false,注意good()
和bad()
不是相反的操作,相对于bad()
,good()
检测的状态更多。
成员函数clear()
用于重置以上的状态。
-
获取和设置文件流对象的位置
所有的流对象都会在其内部至少维护一个指示读写位置的动作。
ifstream
和istream
一样维护一个内部的get
,来获取当前文件的读入的位置,ofstream
反之亦然,而fstream
维护了一个get
和put
的动作。
对于当前文件流读写的位置可以通过以下的成员函数获取或者修改。
tellg() && tellp()
这两个函数返回一个类型为
streampos
的值,这个类型的值表示当前的位置,其中get position
(tellg()返回)或表示put position
(tellp()返回)
seekg() && seekp()
以上函数允许改变当前的
get
或put position
,其函数调用被重载:
seekg(position)
seekp(position)
以上两个函数改变了stream pointer
的position
的绝对位置(从文件的开始位置算起),其参数的类型是streampos
另外两个重载函数的原型为;
seekg(offset, direction)
seekp(offset, direction)
以上是改变stream position
的相对位置,以direction
为参考对象作偏移,其中direction
的类型是seekdir
,是一个枚举类型,其值有以下的值可选:
| ios::beg
| offset counted from the beginning of the stream
|
| ios::cur
| offset counted from the curent position
|
| ios::end
| offset counted from the end of the stream
|
/**********************************
* @Brief: Obtaining the file size
* @CreatedTime:29/5/16
**********************************/
#include
#include
int main(int argc, char **argv)
{
std::streampos begin, end;
std::ifstream my_file("example.bin", ios::binary);
begin = my_file.tellg() // get the beginning position
my_file.seekg(0, ios::end); // set the stream pointer to end position
end = my_file.tellg(); // get the end position
my_file.close(); // close the opening file
std::cout << "Size is: " << (end - begin) << "bytes.\n"; // calculate the file size
return 0;
}
在以上代码中我们注意到有streampos
类型的值,streampos
类型是专门为缓冲区和stream pointer
的position
使用的,具有相同类型的值可以做减法,也可以转换为整型。
-
二进制文件
二进制的文件不同于文本文件,文本文件读写其内容时有其格式,例如我们可以利用getline
获取每一行的内容,但是首先对于二进制文件来说getline()
来读取二进制文件效率不高,还有就是二进制的数据可能不是按照行的格式存储。
file stream
为二进制的数据设计了两个成员函数读写二进制文件,read()
和write()
是读写二进制文件的操作,其中write()
是ostream
的成员函数,fstream
从ostream
中继承得到这个函数;read()
函数是istream
的成员函数,ifstream
从其继承这个函数; fstream
的对象同时拥有这两个函数,其中read
和write
函数的原型如下:
write(memory_block, size)
read(memory_block, size)
read()
和write()
中的memory_block
是一个字节数组(char*),表示要读入或者写入的字节块,其中size
是一个整型表示读取或者要写入的字节数。
#include
#include
int main(int argc, char **argv)
{
std::streampos size;
char *memory_block;
std::ifstream file("example.txt", ios::in | ios::bnary | ios::ate);
if(file.is_open()){
size = file.tellg();
memory_block = new char[size];
file.seekg(0, ios::beg);
file.read(memory_block, size);
file.close();
std::cout << "the entire file is in memory";
delete[] memory_block;
}else{
std::cout << "unable to open the file";
}
return 0;
}
通过以上的程序我们将文件的内容放入内存中(char数组),由于无法事先确定文件的大小,所以我们需要动态分配这个数组的大小,一开始我们打开文件的时候我们将position
指针放在了文件内容的末尾,当调用tellg
的时候我们可以可以得到文件内容的大小。在分配了整个数组后,我们将positon
指针放回到文件开始,将文件读入。
-
缓冲和同步
当我们使用流对象时,在file stream
和物理文件之间存在一个类型为streambuf
的缓冲对象,例如,我们有一个ofstream
的对象,我们调用put
的时候每次会向这个中间的buffer
插入一个字节,而不是直接插入到与之关联的文件中。
当缓冲区刷新的时候,缓冲区中的内容会写入到与之关联的文件中,当以下事件发生时,缓冲区会刷新:
**文件关闭: **在文件关闭之前,缓冲区中还没有刷新的数据会同步,待处理的数据写入文件或先从文件中读取。
**缓冲区溢出: **当缓冲区溢出的时候会自动同步。
**手动同步: **显式的执行flush
或者endl
会刷新缓冲区。
**调用成员函数同步: ** 对应的文件流对象调用sync()
的方法就可以同步缓存区,当同步失败的时候返回-1,同步成功返回0.
相关资料:
Input/output with files
C++ Primer 5th
Keep focus and have fun