重要的事情说三遍:
C++ 提供了以下类来执行字符文件的输入和输出操作:
这些类直接或间接继承自 istream
和 ostream
类。我们已经使用过这些类的对象:cin
是 istream
类的对象,cout
是 ostream
类的对象。因此,我们已经在使用与文件流相关的类。实际上,我们可以像使用 cin
和 cout
一样使用我们的文件流,唯一的区别是我们必须将这些流与物理文件关联。让我们来看一个例子:
// basic file operations
#include <iostream>
#include <fstream>
using namespace std;
int main () {
ofstream myfile;
myfile.open ("example.txt");
myfile << "Writing this to a file.\n";
myfile.close();
return 0;
}
这个代码创建一个名为 example.txt
的文件,并向其中插入一句话,就像我们用 cout
做的那样,但使用的是文件流 myfile
。
让我们一步一步地来看:
打开文件
对这些类的对象执行的第一个操作通常是将其与一个真实的文件关联。这一过程称为“打开文件”。一个打开的文件在程序中表示为一个“流”(即这些类的对象;在前面的例子中,这是 myfile
),对该流对象执行的任何输入或输出操作都将应用于与之关联的物理文件。
为了使用流对象打开文件,我们使用它的成员函数 open
:
open(filename, mode);
其中 filename
是表示要打开文件名的字符串,mode
是一个可选参数,由以下标志的组合组成:
标志 | 含义 |
---|---|
ios::in | 打开进行输入操作。 |
ios::out | 打开进行输出操作。 |
ios::binary | 以二进制模式打开。 |
ios::ate | 设置初始位置在文件末尾。如果没有设置这个标志,初始位置是文件的开头。 |
ios::app | 所有输出操作都在文件末尾执行,将内容追加到文件的当前内容。 |
ios::trunc | 如果文件已经存在且打开进行输出操作,删除以前的内容并替换为新内容。 |
这些标志可以使用按位或运算符(|
)组合。例如,如果我们想以二进制模式打开文件 example.bin
以添加数据,可以通过以下调用成员函数 open
实现:
ofstream myfile;
myfile.open("example.bin", ios::out | ios::app | ios::binary);
ofstream
、ifstream
和 fstream
类的 open
成员函数各有一个默认模式,如果文件在没有第二个参数的情况下打开,则使用该模式:
类 | 默认模式参数 |
---|---|
ofstream | ios::out |
ifstream | ios::in |
fstream | ios::in | ios::out |
对于 ifstream
和 ofstream
类,即使传递了不包括 ios::in
和 ios::out
的模式作为 open
成员函数的第二个参数,它们也会自动分别假设 ios::in
和 ios::out
(标志是组合的)。
对于 fstream
,只有在调用函数时未指定模式参数的情况下才会应用默认值。如果函数调用时有任何模式参数值,则覆盖默认模式,而不是组合。
以二进制模式打开的文件流执行的输入和输出操作独立于任何格式考虑。非二进制文件称为文本文件,某些特殊字符(如换行符和回车符)的格式转换可能会发生。
由于对文件流执行的第一个任务通常是打开文件,这三个类包含一个自动调用 open
成员函数的构造函数,并具有与此成员函数完全相同的参数。因此,我们也可以通过编写以下代码声明前面的 myfile
对象并执行相同的打开操作:
ofstream myfile("example.bin", ios::out | ios::app | ios::binary);
在一个语句中结合对象构造和流打开。打开文件的两种形式都是有效且等效的。
要检查文件流是否成功打开文件,可以通过调用成员函数 is_open
来实现。这个成员函数返回一个 bool
值,表示流对象是否确实与一个打开的文件关联,或否则返回 false
:
if (myfile.is_open()) { /* ok, 继续输出操作 */ }
关闭文件
当我们完成对文件的输入和输出操作时,应关闭它,以便操作系统收到通知,其资源再次可用。为此,我们调用流的成员函数 close
。此成员函数刷新关联的缓冲区并关闭文件:
myfile.close();
一旦调用了这个成员函数,流对象可以重新用于打开另一个文件,并且文件再次可由其他进程打开。
如果对象在仍然与一个打开的文件关联时被销毁,则析构函数会自动调用成员函数 close
。
文本文件
文本文件流是那些在打开模式中不包含 ios::binary
标志的文件。这些文件设计用于存储文本,因此从/向它们输入或输出的所有值可能会进行一些格式转换,这些转换不一定对应于它们的字面二进制值。
在文本文件上进行写操作的方式与我们对 cout
所做的方式相同:
// 写入文本文件
#include <iostream>
#include <fstream>
using namespace std;
int main () {
ofstream myfile ("example.txt");
if (myfile.is_open())
{
myfile << "This is a line.\n";
myfile << "This is another line.\n";
myfile.close();
}
else cout << "Unable to open file";
return 0;
}
从文件读取也可以像我们对 cin
所做的方式:
// 读取文本文件
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main () {
string line;
ifstream myfile ("example.txt");
if (myfile.is_open())
{
while ( getline (myfile,line) )
{
cout << line << '\n';
}
myfile.close();
}
else cout << "Unable to open file";
return 0;
}
最后一个例子读取一个文本文件并在屏幕上打印其内容。我们创建了一个 while 循环,通过 getline 函数逐行读取文件。getline
返回的是流对象本身的引用,当作为布尔表达式评估时(如在这个 while 循环中),如果流准备好进行更多操作,则为 true
,如果已经到达文件末尾或发生其他错误,则为 false
。
检查状态标志
以下成员函数用于检查流的特定状态(它们都返回一个 bool
值):
bad()
如果读写操作失败,则返回true
。例如,在尝试向未打开写入的文件写入或尝试写入的设备没有剩余空间的情况下。fail()
在与bad()
相同的情况下返回true
,但也包括格式错误的情况,例如在尝试读取整数时提取了字母字符。eof()
如果打开读取的文件已到达文件末尾,则返回true
。good()
这是最通用的状态标志:它在调用前面的任意一个函数返回true
的情况下返回false
。注意good
和bad
不是完全相反的(good
同时检查更多的状态标志)。
成员函数clear()
可用于重置状态标志。
get 和 put 流定位
所有 I/O 流对象内部至少保留一个内部位置:
ifstream
,如同 istream
,保留一个内部获取位置,表示下一次输入操作要读取的元素位置。
ofstream
,如同 ostream
,保留一个内部放置位置,表示下一次输出操作要写入的元素位置。
最后,fstream
保留了获取位置和放置位置,如同 iostream
。
这些内部流位置指向流中的位置,下一次读取或写入操作将在此位置执行。这些位置可以使用以下成员函数进行观察和修改:
tellg() 和 tellp()
这两个无参数成员函数返回成员类型 streampos
的值,该类型表示当前获取位置(对于 tellg
)或放置位置(对于 tellp
)。
seekg() 和 seekp()
这些函数允许更改获取位置和放置位置。两个函数都重载了两种不同的原型。第一种形式是:
seekg(position);
seekp(position);
使用此原型,流指针更改为绝对位置 position
(从文件开始位置算起)。该参数的类型为 streampos
,与函数 tellg
和 tellp
返回的类型相同。
另一种形式是:
seekg(offset, direction);
seekp(offset, direction);
使用此原型,获取或放置位置设置为相对于某个特定点的偏移值 direction
。offset
的类型为 streamoff
。direction
的类型为 seekdir
,这是一个枚举类型,确定偏移值的起点,可以取以下任意值:
方向 | 含义 |
---|---|
ios::beg | 从流的起点计算偏移量 |
ios::cur | 从当前职位计算偏移量 |
ios::end | 从流的末尾计算偏移量 |
以下示例使用我们刚看到的成员函数来获取文件的大小:
// 获取文件大小
#include <iostream>
#include <fstream>
using namespace std;
int main () {
streampos begin, end;
ifstream myfile ("example.bin", ios::binary);
begin = myfile.tellg();
myfile.seekg(0, ios::end);
end = myfile.tellg();
myfile.close();
cout << "size is: " << (end - begin) << " bytes.\n";
return 0;
}
注意我们用于变量 begin
和 end
的类型:
streampos size;
streampos
是一个特定类型,用于缓冲区和文件定位,是 file.tellg()
返回的类型。此类型的值可以安全地从其他相同类型的值中减去,也可以转换为足够大的整数类型以包含文件大小。
这些流定位函数使用两种特定类型:streampos
和 streamoff
。这些类型也定义为流类的成员类型:
类型 | 成员类型 | 描述 |
---|---|---|
streampos | ios::pos_type | 定义为 fpos<mbstate_t> 。它可以转换为/从 streamoff 转换,并且可以加上或减去这些类型的值。 |
streamoff | ios::off_type | 它是基本整数类型(如 int 或 long long )的别名。 |
上面每个成员类型都是其非成员等效类型的别名(它们是完全相同的类型)。使用哪个都没关系。成员类型更通用,因为它们在所有流对象(甚至使用特殊字符类型的流)中都是相同的,但由于历史原因,现有代码广泛使用非成员类型。
二进制文件
对于二进制文件,使用提取和插入操作符(<<
和 >>
)以及像 getline
这样的函数来读取和写入数据效率不高,因为我们不需要格式化任何数据,而且数据可能没有按行格式化。
文件流包括两个专门设计用于顺序读取和写入二进制数据的成员函数:write
和 read
。第一个(write
)是 ostream
的成员函数(由 ofstream
继承)。read
是 istream
的成员函数(由 ifstream
继承)。fstream
类的对象拥有这两个函数。它们的原型是:
write(memory_block, size);
read(memory_block, size);
其中 memory_block
的类型为 char*
(指向 char
的指针),表示读取的数据元素存储的字节数组的地址或要写入的数据元素所在的字节数组地址。size
参数是一个整数值,指定要从/向内存块读取或写入的字符数。
// 读取整个二进制文件
#include <iostream>
#include <fstream>
using namespace std;
int main () {
streampos size;
char * memblock;
ifstream file ("example.bin", ios::in|ios::binary|ios::ate);
if (file.is_open())
{
size = file.tellg();
memblock = new char [size];
file.seekg (0, ios::beg);
file.read (memblock, size);
file.close();
cout << "the entire file content is in memory";
delete[] memblock;
}
else cout << "Unable to open file";
return 0;
}
在这个例子中,整个文件被读取并存储在一个内存块中。让我们来看看这是如何完成的:
首先,文件以 ios::ate
标志打开,这意味着获取指针将定位在文件的末尾。这样,当我们调用成员函数 tellg()
时,我们将直接获得文件的大小。
一旦获得文件的大小,我们请求分配一个足够大的内存块来容纳整个文件:
memblock = new char[size];
之后,我们将获取位置设置在文件的开头(记住我们打开文件时该指针在末尾),然后读取整个文件,最后关闭它:
file.seekg (0, ios::beg);
file.read (memblock, size);
file.close();
此时,我们可以操作从文件中获取的数据。但是我们的程序只是宣布文件的内容在内存中,然后结束。
缓冲区和同步
当我们操作文件流时,这些流与类型为 streambuf
的内部缓冲区对象相关联。此缓冲区对象可能表示一个内存块,作为流和物理文件之间的中介。例如,对于 ofstream
,每次调用成员函数 put
(写入单个字符)时,字符可能会插入到这个中间缓冲区中,而不是直接写入与流相关联的物理文件。
操作系统也可能定义其他层次的缓冲区用于读写文件。
当缓冲区刷新时,其中包含的所有数据都会写入物理介质(如果是输出流)。这个过程称为同步,发生在以下任何情况下: