文件输入/输出

C++ 提供了以下类,用于执行字符与文件之间的输入/输出操作:

  • ofstream: 用于写入文件的流类
  • ifstream: 用于从文件中读取的流类
  • fstream: 用于从/向文件读取和写入的流类。

这些类直接或间接地派生自以下类:istreamostream我们已经使用过类型为这些类的对象,例如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),对这个流对象执行的任何输入或输出操作都将应用于与之关联的物理文件。

为了使用流对象打开文件,我们使用其成员函数open():

open (filename, mode);

其中filename是一个以 null 结尾的字符序列,类型为const char *(与字符串文字的类型相同),表示要打开的文件的名称,而mode是一个可选参数,包含以下标志的组合:

ios::in打开以进行输入操作。
ios::out打开以进行输出操作。
ios::binary以二进制模式打开。
ios::ate将初始位置设置在文件末尾。
如果此标志未设置为任何值,则初始位置是文件开头。
ios::app所有输出操作都在文件末尾执行,将内容附加到文件的当前内容。 此标志只能用于为仅输出操作打开的流。
ios::trunc如果为输出操作打开的文件之前已存在,则其先前的内容将被删除并替换为新内容。

所有这些标志都可以使用按位 OR 运算符(||)组合。例如,如果我们想以二进制模式打开文件example.bin以添加数据,我们可以通过以下调用成员函数来实现:open():

1
2
ofstream myfile;
myfile.open ("example.bin", ios::out | ios::app | ios::binary);

每个open()类的成员函数、ofstream, ifstreamfstream都有一个默认模式,如果在没有第二个参数的情况下打开文件,则使用该默认模式。

默认模式参数
、ofstreamios::out
ifstreamios::in
fstreamios::in | ios::out

不写入任何字符。对于ifstream、ofstream这些类,即使传递给ios::inios::out成员函数的第二个参数中没有包含它们的模式,也会自动分别被假定。open()成员函数。

仅当在未指定模式参数的任何值的情况下调用该函数时,才会应用默认值。 如果使用该参数中的任何值调用该函数,则默认模式将被覆盖,而不是组合。

以二进制模式打开的文件流执行独立于任何格式考虑因素的输入和输出操作。 非二进制文件被称为文本文件,并且由于某些特殊字符(如换行符和回车符)的格式设置,可能会发生一些转换。

由于通常在文件流对象上执行的第一个任务是打开文件,因此这三个类都包含一个构造函数,该构造函数会自动调用open()成员函数,并且具有与此成员函数完全相同的参数。 因此,我们也可以声明先前的myfile对象,并通过编写以下代码在之前的示例中进行相同的打开操作:

1
ofstream myfile ("example.bin", ios::out | ios::app | ios::binary);

将对象构造和流打开合并到一个语句中。 两种打开文件的方式都是有效且等效的。

要检查文件流是否成功打开文件,可以通过调用成员is_open()(不带任何参数)来完成。 如果流对象确实与一个打开的文件关联,则此成员函数返回一个 bool 值 true,否则返回 false。

1
if (myfile.is_open()) { /* ok, proceed with output */ }

关闭文件

当我们完成对文件的输入和输出操作后,我们应该关闭它,以便它的资源可以再次使用。 为了做到这一点,我们必须调用流的成员函数close()。此成员函数不带任何参数,它的作用是刷新关联的缓冲区并关闭文件。

1
myfile.close();

调用此成员函数后,流对象可用于打开另一个文件,并且该文件可以再次被其他进程打开。

如果在对象仍然与打开的文件关联时被销毁,则析构函数会自动调用成员函数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.

从文件中输入数据也可以以与我们使用的方式相同的方式执行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 << endl;
    }
    myfile.close();
  }

  else cout << "Unable to open file"; 

  return 0;
}
This is a line.
This is another line.  

最后一个示例读取一个文本文件并在屏幕上打印其内容。 我们创建了一个 while 循环,使用 getline 逐行读取文件。getline 返回的值是对流对象本身的引用,当将其作为布尔表达式进行评估时(如此 while 循环中所示),如果流已准备好进行更多操作,则为 true;如果已到达文件末尾或发生其他错误,则为 false。

检查状态标志

除了good()(检查流是否已准备好进行输入/输出操作)之外,还存在其他成员函数来检查流的特定状态(所有这些函数都返回一个 bool 值):

bad()
如果读取或写入操作失败,则返回 true。 例如,如果我们尝试写入未打开以进行写入的文件,或者我们尝试写入的设备没有剩余空间。
fail()
在与 bad() 相同的情况下返回 true,但在发生格式错误的情况下也返回 true,例如,当我们尝试读取一个整数时提取一个字母字符。
eof()
如果打开以进行读取的文件已到达末尾,则返回 true。
good()
它是最通用的状态标志:在调用任何先前函数将返回 true 的相同情况下,它返回 false。

为了重置由我们刚刚看到的任何这些成员函数检查的状态标志,我们可以使用成员函数clear(),它不带任何参数。

get 和 put 流指针

所有 i/o 流对象至少有一个内部流指针

ifstream,例如istream,有一个称为 get pointer 的指针,该指针指向下一次输入操作中要读取的元素。

、ofstream,例如ostream,有一个称为 put pointer 的指针,该指针指向要写入下一个元素的位置。

最后,fstreamiostream继承了 get 和 put 指针(它本身是从istreamostream).

派生的)。可以使用以下成员函数来操作这些指向流中读取或写入位置的内部流指针:

tellg() 和 tellp()

这两个成员函数都没有参数,并返回成员类型的值pos_type,它是一个整数数据类型,表示 get 流指针的当前位置(在tellg的情况下)或 put 流指针的当前位置(在tellp).

seekg() 和 seekp()

这些函数允许我们更改 get 和 put 流指针的位置。 两个函数都使用两种不同的原型进行重载。 第一个原型是

seekg ( position );
seekp ( position );

使用此原型,流指针将更改为绝对位置position(从文件开头开始计数)。 此参数的类型与函数返回的类型相同tellgtellp:成员类型pos_typepos_type

,它是一个整数值。

这些函数的另一个原型是
seekp ( offset, direction );

使用此原型,get 或 put 指针的位置将设置为相对于参数direction. 确定的某个特定点的偏移值。offset的成员类型为,这也是一个整数类型。 并且directiondirection的类型为seekdir,它是一个枚举类型(),用于确定从哪个点开始计数偏移量,并且可以采用以下任何值:

ios::beg从流的开头计算的偏移量
ios::cur从流指针的当前位置计算的偏移量
ios::end从流的末尾计算的偏移量

以下示例使用我们刚刚看到的成员函数来获取文件的大小:

// obtaining file size
#include <iostream>
#include <fstream>
using namespace std;

int main () {
  long begin,end;
  ifstream myfile ("example.txt");
  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.

二进制文件

在二进制文件中,使用提取和插入运算符(<<>>>> 和 <<)以及像getline这样的函数输入和输出数据效率不高,因为我们不需要格式化任何数据,并且数据可能不使用文本文件用于分隔元素的分隔符(如空格、换行符等...)。

文件流包含两个专门设计用于按顺序输入和输出二进制数据的成员函数:writeread。第一个(writewriteostream继承的、ofstream)。并且readreadistream继承的ifstream。类fstream的对象同时具有这两个成员。他们的原型是

write ( memory_block, size );
read ( memory_block, size );

其中memory_block的类型为“指向 char 的指针”(char*),表示存储读取的数据元素的字节数组的地址,或者从中获取要写入的数据元素的字节数组的地址。size参数是一个整数值,指定要从/向内存块读取或写入的字符数。

// reading a complete binary file
#include <iostream>
#include <fstream>
using namespace std;

ifstream::pos_type size;
char * memblock;

int main () {
  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 complete file content is in memory";

    delete[] memblock;
  }
  else cout << "Unable to open file";
  return 0;
}
the complete file content is in memory

在此示例中,将读取整个文件并将其存储在内存块中。 让我们检查一下这是如何完成的:

首先,使用ios::ate标志打开文件,这意味着 get 指针将位于文件末尾。 这样,当我们调用成员tellg()时,我们将直接获得文件的大小。 请注意我们用于声明变量的类型size:

1
ifstream::pos_type size;

ifstream::pos_type是用于缓冲区和文件定位的特定类型,并且是file.tellg()返回的类型。 此类型定义为整数类型,因此我们可以对其进行与对任何其他整数值进行的操作相同的操作,并且可以安全地将其转换为足够大的另一种整数类型以包含文件的大小。 对于大小小于 2GB 的文件,我们可以使用int:

1
2
int size;
size = (int) file.tellg();

获得文件的大小后,我们请求分配一个足够大的内存块以容纳整个文件。

1
memblock = new char[size];

之后,我们继续将 get 指针设置在文件的开头(记住我们打开文件时此指针在末尾),然后读取整个文件,最后将其关闭。

1
2
3
file.seekg (0, ios::beg);
file.read (memblock, size);
file.close();

此时,我们可以使用从文件中获得的数据进行操作。 我们的程序只是声明文件的内容在内存中,然后终止。

缓冲区和同步


当我们使用文件流进行操作时,这些流与类型为streambuf的内部缓冲区相关联。 此缓冲区是一个内存块,它充当流和物理文件之间的中介。 例如,对于、ofstream,每次调用成员函数put(它写入单个字符)时,该字符不会直接写入与该流关联的物理文件。 相反,该字符将插入到该流的中间缓冲区中。

刷新缓冲区时,其中包含的所有数据都将写入物理介质(如果是输出流)或仅释放(如果是输入流)。 此过程称为同步,并在以下任何情况下发生:

  • 文件关闭时:在关闭文件之前,所有尚未刷新的缓冲区都会被同步,并且所有待处理的数据都会被写入或读取到物理介质。
  • 缓冲区满时:缓冲区具有一定的大小。当缓冲区满时,它会自动同步。
  • 显式地,使用操纵符:当在流上使用某些操纵符时,会发生显式同步。这些操纵符是:flushendl.
  • 显式地,使用成员函数 sync():调用流的成员函数sync(),它不接受任何参数,会立即进行同步。 此函数返回一个int等于-1的值,如果流没有关联的缓冲区或发生故障。 否则(如果流缓冲区已成功同步),它将返回0.
Index
目录