这些类直接或间接地派生自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; } | [file example.txt] Writing this to a file. |
这段代码创建了一个名为example.txt的文件,并以与使用cout相同的方式将句子插入其中, 但使用的是文件流myfile。
但让我们一步一步来:通常对这些类中的一个对象执行的第一个操作是将它与实际文件关联起来。 这个过程称为打开文件。在程序中,打开的文件由流(即其中一个类的对象;在前面的示例中, 这是myfile),并且在此流对象上执行的任何输入或输出操作都将应用于与其关联的物理文件。
为了用stream对象打开文件,我们使用它的成员函数open:其中filename是一个表示要打开的文件名的字符串,mode是一个可选参数,由以下标志组合而成:
ios::in | 为输入操作打开。 |
ios::out | 为输出操作打开。 |
ios::binary | 以二进制模式打开。 |
ios::ate | 设置初始位置在文件末尾。
如果未设置此标志,则初始位置为文件的开头。 |
ios::app | 将内容附加到文件的当前内容末尾,所有输出操作都在文件的末尾执行。 |
ios::trunc | 如果为输出操作而打开文件,并且它已经存在, 那么之前的内容将被删除并被新的内容替换。 |
所有这些标志都可以使用按位运算符OR(|)进行组合。例如,如果我们想以二进制模式打开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成员函数(组合标志)。
对于fstream,只有在调用函数时没有为模式参数指定任何值时才应用默认值。 如果函数是用该参数中的任意值调用的,则默认模式将被覆盖,而不是组合。
以二进制模式打开的文件流执行输入和输出操作,与任何格式无关。 非二进制文件被称为文本文件,有些编译可能是由于某些特殊字符(如换行符和回车符) 的格式化而产生的。
由于在文件流上执行的第一个任务通常是打开文件,所以这三个类包括一个构造函数, 该构造函数自动调用open成员函数,并具有与该函数完全相同的参数。 因此,我们也可以声明前面的myfile对象,并在前面的例子中执行相同的打开操作,方法如下:
ofstream myfile ("example.bin", ios::out | ios::app | ios::binary); |
在一条语句中,组合对象构造和流打开文件的两种形式都是有效的和等效的。
要检查文件流是否成功打开了文件,可以通过调用成员is_open来实现。 如果该stream对象确实与打开的文件相关联,则该成员函数返回bool值true,否则返回false:
if (myfile.is_open()) { /* ok, proceed with output */ } |
当我们完成对一个文件的输入和输出操作时,我们应该关闭它,以便操作系统得到通知, 其资源再次可用。为此,我们调用流的成员函数close。 该成员函数刷新相关缓冲区并关闭文件:
myfile.close(); |
一旦调用了这个成员函数,就可以重用stream对象来打开另一个文件,并且该文件可以再次被其他进程打开。
如果一个对象在与打开的文件关联时被销毁,析构函数会自动调用成员函数close。
文本文件流是那些在其打开模式中不包含ios::binary标志的流。这些文件被用来存储文本, 因此从它们输入或输出的所有值都可能被一些格式化转换, 这些转换不一定对应于它们的文本二进制值。
文本文件的写入操作与cout的操作方式相同:
// writing on a text file #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; } | [file example.txt] This is a line. This is another line. |
// reading a text file #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; } | This is a line. This is another line. |
最后一个示例读取文本文件并将其内容打印到屏幕上。我们已经创建了一个while循环, 使用getline逐行读取文件。getline返回的值是对stream对象本身的引用, 当作为布尔表达式(如在这个while循环中)进行计算时,如果流准备好执行更多操作, 则为true;如果到达文件结尾或发生其他错误,则为false。
与istream一样,Ifstream保留了一个内部的get位置, 该位置是下一个输入操作中要读取的元素的位置。
Ofstream和ostream一样,保持一个内部放置位置,其中包含必须写入下一个元素的位置。
最后,fstream保留了get和put的位置,就像iostream一样。
这些内部流位置指向流中执行下一个读写操作的位置。 这些位置可以通过以下成员函数进行查看和修改:
tellg() 和 tellp()这两个不带参数的成员函数返回一个成员类型streampos的值, 该类型表示当前的get位置(在tellg中)或put位置(在tellp中)。
seekg() 和 seekp()这些函数允许更改获取和放置位置的位置。两个函数都被两个不同的原型重载。第一种形式是:
seekg ( position );使用这个原型,流指针被更改为绝对位置位置(从文件开始计数)。 该参数的类型是streampos,与函数tellg和tellp返回的类型相同。
这些函数的另一种形式是:使用这个原型,get或put位置被设置为相对于由参数 direction确定的特定点的偏移值。 Offset是streamoff.类型。而direction的类型是seekdir,这是一种枚举类型, 确定从何处计算偏移量,它可以取以下任意值:
ios::beg | 从流开始位置计数的偏移量 |
ios::cur | 从当前位置计算的偏移量 |
ios::end | 从流的末端开始计数的偏移量 |
下面的例子使用我们刚刚看到的成员函数来获取文件的大小:
// obtaining file size #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; } | size is: 40 bytes. |
streampos size; |
Streampos是用于缓冲区和文件定位的特定类型,是file.tellg()返回的类型。 这种类型的值可以安全地从相同类型的其他值中减去, 也可以转换为大到足以包含文件大小的整数类型。
这些流定位函数使用两种特殊类型:streampos和streamoff。这些类型也被定义为流类的成员类型:
类型 | 成员类型 | 说明 |
streampos | ios::pos_type | 定义为 fpos < mbstate_t >。
它可以转换to/from 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参数是一个整型值,指定从内存块读取或写入的字符数。
// reading an entire binary file #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; } | the entire file content is in memory |
在本例中,整个文件被读取并存储在内存块中。让我们来看看这是如何做到的:
首先,使用ios::ate标志打开文件,这意味着get指针将被定位在文件的末尾。 这样,当调用成员tellg()时,我们将直接获得文件的大小。
一旦我们获得了文件的大小,我们就请求分配一个足够容纳整个文件的内存块:
memblock = new char[size]; |
在那之后,我们继续在文件的开始处设置get位置(记住, 我们在文件的末尾用这个指针打开了文件),然后我们读取整个文件,最后关闭它:
file.seekg (0, ios::beg); file.read (memblock, size); file.close(); |
此时,我们可以使用从文件中获得的数据进行操作。但我们的程序只是宣布文件的内容在内存中, 然后结束。
当我们操作文件流时,它们与类型为streambuf的内部缓冲区对象相关联。 这个缓冲区对象可能代表一个充当流和物理文件之间的中介的内存块。 例如,对于ofstream,每次调用成员函数put(写入单个字符)时, 字符可能会被插入这个中间缓冲区,而不是直接写入与流相关联的物理文件。
操作系统还可以定义用于读写文件的其他缓冲层。
当缓冲区被刷新时,其中包含的所有数据都被写入物理介质(如果它是一个输出流)。 这个过程被称为同步,并在下列任何一种情况下发生: