文件输入/输出

C++ 提供以下类来执行从文件输入或向文件输出字符的操作:

  • ofstream: 用于写入文件的流类
  • ifstream: 用于从文件读取的流类
  • fstream: 用于对文件进行读写操作的流类。

这些类直接或间接地派生自 istreamostream 类。我们已经使用过这些类的对象:cinistream 类的对象,coutostream 类的对象。因此,我们实际上已经在使用与文件流相关的类了。事实上,我们可以像使用 cincout 一样使用文件流,唯一的区别是我们必须将这些流与物理文件关联起来。让我们来看一个例子:

// 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 是一个表示要打开的文件名的字符串,mode 是一个可选参数,它是以下标志的组合:

ios::in为输入操作(读取)而打开。
ios::out为输出操作(写入)而打开。
ios::binary以二进制模式打开。
ios::ate将初始位置设置在文件末尾。
如果不设置此标志,初始位置是文件的开头。
ios::app所有输出操作都在文件末尾进行,将内容追加到文件当前内容的后面。
ios::trunc如果文件是为输出操作而打开的,并且它已经存在,那么它之前的内容将被删除,并被新内容替换。

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

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

ofstreamifstreamfstream 类的每个 open 成员函数都有一个默认模式,如果在打开文件时没有提供第二个参数,就会使用这个默认模式:

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

对于 ifstreamofstream 类,即使传递给 open 成员函数的第二个参数不包含 ios::inios::out,它们也会被自动分别假定(这些标志会被组合)。

对于 fstream,默认值仅在调用函数时未指定任何模式参数值的情况下应用。如果调用该函数时为该参数指定了任何值,则默认模式将被覆盖,而不是组合。

二进制模式打开的文件流执行输入和输出操作时,不考虑任何格式问题。非二进制文件被称为文本文件,由于某些特殊字符(如换行符和回车符)的格式化,可能会发生一些转换。

由于对文件流执行的第一个任务通常是打开文件,这三个类都包含一个构造函数,它会自动调用 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 << '\n';
    }
    myfile.close();
  }

  else cout << "Unable to open file"; 

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

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

检查状态标志

存在以下成员函数来检查流的特定状态(它们都返回一个 bool 值):

bad()
如果读写操作失败,则返回 true。例如,当我们试图向一个未以写入模式打开的文件写入,或者我们试图写入的设备没有剩余空间时。
fail()
在与 bad() 相同的情况下返回 true,但当发生格式错误时也返回 true,例如当我们试图读取一个整数时却提取了一个字母字符。
eof()
如果为读取而打开的文件已到达末尾,则返回 true
good()
这是最通用的状态标志:在调用任何前面函数会返回 true 的情况下,它都返回 false。注意,goodbad 并非完全相反(good 一次性检查更多的状态标志)。

成员函数 clear() 可以用来重置状态标志。

get 和 put 流定位

所有 I/O 流对象内部都至少保持一个内部位置:

ifstream,像 istream 一样,保持一个内部的获取位置 (get position),即下一次输入操作要读取的元素的位置。

ofstream,像 ostream 一样,保持一个内部的放置位置 (put position),即下一个元素必须被写入的位置。

最后,fstream,像 iostream 一样,同时保持获取位置放置位置

这些内部流位置指向流中下一次读取或写入操作要执行的位置。这些位置可以使用以下成员函数来观察和修改:

tellg() 和 tellp()

这两个没有参数的成员函数返回一个成员类型为 streampos 的值,该类型表示当前的获取位置(对于 tellg)或放置位置(对于 tellp)。

seekg() 和 seekp()

这些函数允许改变获取位置放置位置。这两个函数都有两个不同原型的重载。第一种形式是:

seekg ( position );
seekp ( position );

使用这个原型,流指针被更改为绝对位置 position(从文件开头算起)。此参数的类型是 streampos,与 tellgtellp 函数返回的类型相同。

这些函数的另一种形式是:

seekg ( offset, direction );
seekp ( offset, direction );

使用这个原型,获取放置位置被设置为一个相对于由参数 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.

注意我们为变量 beginend 使用的类型:

1
streampos size;

streampos 是用于缓冲区和文件定位的特定类型,也是 file.tellg() 返回的类型。这种类型的值可以安全地与同类型的其他值相减,也可以转换为足以容纳文件大小的整数类型。

这些流定位函数使用两种特殊的类型:streamposstreamoff。这些类型也被定义为流类的成员类型:

类型成员类型描述
streamposios::pos_type定义为 fpos<mbstate_t>
它可以与 streamoff相互转换,并且可以与这些类型的值进行加减运算。
streamoffios::off_type它是基本整数类型之一(如 intlong long)的别名。

上述每个成员类型都是其非成员等价类型的别名(它们是完全相同的类型)。使用哪一个都无所谓。成员类型更通用,因为它们在所有流对象上都是相同的(即使是使用特殊字符类型的流),但由于历史原因,非成员类型在现有代码中被广泛使用。

二进制文件

对于二进制文件,使用提取和插入运算符(<<>>)以及像 getline 这样的函数来读写数据是低效的,因为我们不需要格式化任何数据,而且数据很可能不是按行格式化的。

文件流包含两个专门设计用于顺序读写二进制数据的成员函数:writeread。第一个(write)是 ostream 的成员函数(由 ofstream 继承)。而 readistream 的成员函数(由 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 标志打开,这意味着获取指针将被定位在文件的末尾。这样,当我们调用成员函数 tellg() 时,我们将直接获得文件的大小。

一旦我们获得了文件的大小,我们就请求分配一个足够大的内存块来容纳整个文件:

1
memblock = new char[size];

紧接着,我们着手将获取位置设置在文件的开头(记住我们打开文件时这个指针在末尾),然后我们读取整个文件,最后关闭它:

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

此时,我们可以对从文件中获取的数据进行操作。但我们的程序只是宣布文件内容已在内存中,然后就结束了。

缓冲区和同步

当我们操作文件流时,它们与一个类型为 streambuf 的内部缓冲区对象相关联。这个缓冲区对象可以代表一个内存块,作为流和物理文件之间的中介。例如,对于一个 ofstream,每次调用成员函数 put(写入单个字符)时,该字符可能会被插入到这个中间缓冲区,而不是直接写入到与流关联的物理文件中。

操作系统也可能为文件的读写定义了其他层次的缓冲。

当缓冲区被刷新时,其中包含的所有数据都会被写入到物理介质中(如果它是一个输出流)。这个过程被称为同步,并在以下任何一种情况下发生:

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