C++官网参考链接:https://cplusplus.com/doc/tutorial/files/
输入/输出文件
C++提供了以下类来执行向文件输出和从文件中输入字符:
*ofstream(ofstream
):写入文件中的流类
*ifstream(ifstream
):从文件中读取的流类
*fstream(fstream
):从文件中读取和写入到文件中的流类。
这些类直接或间接地派生自类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 | Open for input operations. |
ios::out | Open for output operations. |
ios::binary | Open in binary mode. |
ios::ate | Set the initial position at the end of the file. (设置文件结束处的初始位置。如果未设置此标志,则初始位置为文件的开始。) |
ios::app | All output operations are performed at the end of the file, appending the content to the current content of the file. (所有输出操作都在文件结束处执行,将内容追加到文件的当前内容。) |
ios::trunc | If the file is opened for output operations and it already existed, its previous content is deleted and replaced by the new one. (如果打开该文件进行输出操作,并且该文件已经存在,则会删除其以前的内容,并用新内容替换。) |
所有这些标志都可以使用位操作符按位或(|)进行组合。例如,如果我们想以二进制模式打开example.bin文件来添加数据,我们可以通过调用成员函数open来实现:
ofstream myfile;
myfile.open ("example.bin", ios::out | ios::app | ios::binary);
类ofstream、ifstream和fstream的每个open成员函数都有一个默认模式,在没有第二个实参的情况下打开文件:
class(类) | default mode parameter(默认模式形参) |
---|---|
ofstream | ios::out |
ifstream | ios::in |
fstream | ios::in | ios::out |
对于ifstream和ofstream类,ios::in和ios::out被自动分别假定,即使一个mode不包含它们作为第二个实参传递给open成员函数(标志是组合的)。
对于fstream,只有在调用函数时没有为mode形参指定任何值时才应用默认值。如果用该形参中的任何值调用函数,则将覆盖默认的mode,而不是组合默认的mode。
以二进制模式打开的文件流执行输入和输出操作独立于任何格式考虑。非二进制文件被称为文本文件,由于某些特殊字符(如换行符和回车符)的格式化,可能会发生一些转换。
由于在文件流上执行的第一个任务通常是打开文件,因此这三个类包括一个自动调用open成员函数的构造函数,并且具有与该成员完全相同的形参。因此,我们也可以声明前面的myfile对象,并在前面的例子中执行相同的打开操作:
ofstream myfile ("example.bin", ios::out | ios::app | ios::binary);
在单个语句中组合对象构造和流打开。打开文件的两种形式都是有效且等价的。
要检查文件流是否成功打开了文件,可以通过调用成员is_open来完成。如果流对象确实与打开的文件相关联,则该成员函数返回bool值true,否则返回false:
if (myfile.is_open()) { /* ok, proceed with output */ }
关闭一个文件
当我们完成对一个文件的输入和输出操作时,我们应该关闭它,以便通知操作系统,它的资源再次可用。为此,我们调用流的成员函数close。此成员函数接受刷新相关缓冲区并关闭文件:
myfile.close();
一旦调用了这个成员函数,就可以重用流对象来打开另一个文件,而该文件又可以被其他进程打开。
如果对象在仍然与打开的文件相关联的情况下被销毁,析构函数自动调用成员函数close。
文本文件
文本文件流是那些在打开的mode中不包含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;
}
从文件中读取也可以用与cin相同的方式执行:
// 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;
}
最后一个例子读取一个文本文件并在屏幕上打印出它的内容。我们已经创建了一个while循环,它使用getline逐行读取文件。getline返回的值是流对象本身的引用,当作为布尔表达式计算时(如在这个while循环中),如果流已准备好进行更多操作,则为true;如果到达文件结束或发生其他错误,则为false。
检查状态标志
以下成员函数用于检查流的特定状态(它们都返回bool值):
bad()
如果读写操作失败,返回true。例如,当我们试图写入一个未打开的文件时,或者当我们试图写入的设备没有剩余空间时。
fail()
在与bad()相同的情况下返回true,但也在发生格式错误的情况下返回true,例如当我们试图读取整数时提取了字母字符。
eof()
如果打开用于读取的文件已到达文件结束,则返回true。
good()
它是最通用的状态标志:在调用前面任何函数都会返回true的情况下,它返回false。注意,good和bad并不是完全相反的(good一次检查更多的状态标志)。
成员函数clear()可用于重置状态标志。
获取和写入流定位
所有I/O流对象在内部至少保留一个内部位置:
与istream一样,ifstream保留了一个内部的读取位置,该位置是下一个输入操作中要读取的元素的位置。
与ostream一样,ofstream保留一个内部的写入位置,即下一个元素必须写入的位置。
最后,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)值。offset为streamoff类型。direction的类型是seekdir,这是一个枚举类型,确定从哪里开始计算偏移(offset)值的点,它可以采用以下值中的任何一个:
ios::beg | offset counted from the beginning of the stream(从流的开始位置计算偏移量) |
ios::cur | offset counted from the current position(从流的当前位置计算偏移量) |
ios::end | offset counted from the end of the stream(从流的结束位置计算偏移量) |
下面的例子使用我们刚刚看到的成员函数来获取文件的大小:
// 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;
}
注意我们为变量begin和end所使用的类型:
streampos size;
streampos是用于缓冲区和文件定位的特定类型,是file.tellg()返回的类型。此类型的值可以安全地从相同类型的其他值中减去,也可以转换为大到足以包含文件大小的整数类型。
这些流定位函数使用两种特殊类型:streampos和streamoff。这些类型也被定义为流类的成员类型:
Type(类型) | Member type(成员类型) | Description(描述) |
---|---|---|
streampos | ios::pos_type | Defined as (定义为fpos<mbstate_t>。) |
streamoff | ios::off_type | It is an alias of one of the fundamental integral types (such as (它是一种基本整型类型(例如int或long long)的别名。) |
上面的每个成员类型都是其等价非成员的别名(它们是完全相同的类型)。用哪一个并不重要。成员类型更泛型,因为它们在所有流对象上是相同的(甚至在使用奇异类型的字符的流上),但由于历史原因,非成员类型在现有代码中广泛使用。
二进制文件
对于二进制文件,使用提取和插入操作符(<<和>>)和getline之类的函数读写数据是低效的,因为我们不需要格式化任何数据,而且数据可能没有按行格式化。
文件流包括两个专门为按顺序读写二进制数据而设计的成员函数:read和write。第一个(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;
}
在本例中,整个文件被读取并存储在一个内存块中。让我们来看看这是如何做到的:
首先,打开文件时带有ios::ate标志,这意味着读取指针将定位在文件结束处。这样,当我们调用成员tellg()时,我们将直接获得文件的大小。
一旦我们获得了文件的大小,我们请求分配一个足够大的内存块来容纳整个文件:
memblock = new char[size];
在这之后,我们继续设置文件开头的读取位置(记住,我们是用这个在定位在文件的结束指针打开文件的),然后读取整个文件,最后关闭它:
file.seekg (0, ios::beg);
file.read (memblock, size);
file.close();
此时,我们可以对从文件中获得的数据进行操作。但是我们的程序只是宣布文件的内容在内存中,然后结束。
缓冲区和同步
当我们操作文件流时,它们与streambuf类型的内部缓冲区对象相关联。该缓冲区对象可以表示一个内存块,该内存块充当流和物理文件之间的中介。例如,对于ofstream,每次调用成员函数put(它写入单个字符)时,可以将该字符插入这个中间缓冲区,而不是直接写入与流相关的物理文件。
操作系统还可以定义用于文件读写的其他缓冲层。
刷新缓冲区时,将其中包含的所有数据写入物理介质(如果它是输出流)。这个过程被称为同步,在以下任何情况下发生:
*当文件关闭时:在关闭文件之前,所有尚未被刷新的缓冲区被同步,所有挂起的数据被写入或读取到物理介质。
*当缓冲区满时:缓冲区有一定的大小。当缓冲区满时,它会自动同步。
*显式地,使用操纵符:当某些操纵符在流上使用时,将发生显式同步。这些操纵符是:flush和endl。
*显式地,使用成员函数sync():调用流的成员函数sync()导致立即同步。如果流没有相关联的缓冲区或在失败的情况下,此函数返回一个等于-1的int值。否则(如果流缓冲区成功同步)它返回0。