相关文章推荐
老实的打火机  ·  ifstream::rdbuf - C++ ...·  2 小时前    · 
非常酷的手套  ·  CVPR2020 Oral: ...·  7 月前    · 
一身肌肉的稀饭  ·  VMware Aria ...·  9 月前    · 

C++学习笔记(9) 文件的输入输出

C++ 提供以下类来执行字符的输出、输入从/到文件。

  • ofstream :写进文件的流类
  • ifstream :读取文件的流类
  • fstream :读取和写入文件的流类

这几个类都是直接或间接继承自istream和ostream的。我们之前已经使用过这个类的几个对象了:cin是类istream的一个对象,而cout是类ostream的一个对象。因此,我们已经在使用了文件流相关的类。实际上,我们也可以与使用cin和cout一样使用文件流,唯一的区别就是文件流与实际文件联系在一起。让我们一起看一个例子。

#include <iostream>
#include <fstream>
using namespace std;
int main()
    ofstream myfile;
    myfile.open("example.txt");
    myfile<<"Writing this to a file.\n";
    myfile.close();

代码建立了一个叫example.txt的文件,并插入一个语句,使用的方式就和我们使用cout一样,但文件流是myfile。

现在让我们来一步一步解析如何使用。

打开文件

首先进行的第一步操作一般就是创建一个类的对象,将它与实际文件相关联起来。这个过程就叫做打开文件。然后在程序中,这个流对象就代替了这个文件,所有对这个流对象的输入输出操作,都会作用到与之相关的实际文件上。

为了用流对象打开一个文件,我们可以使用下面的成员函数:

open(filename,mode);

其中filename是代表文件名的字符串,而mode则有以下可选参数。:

  • ios::in :以输入的形式打开文件。
  • ios:out :以输出的形式打开文件。
  • ios:binary :以二进制形式打开文件。
  • ios:ate :将起始位置设置为文件末尾。如果没有设置这个参数,那么就默认起始位置是文件开始位置。
  • ios:app :所有的输出操作都在文件的末尾,将输出的内容附加到目前内容之后。
  • ios:trunc :如果所打开的文件已经存在而且有内容,就将内容删除并且用新的输出替代掉。

所有这些参数都能通过管道符号并行启用。比如说,如果我们想要打开一个文件exam.bin,以二进制的模式,将数据添加进文件的末尾,那么我们就可以这么写:

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

类ofstream、ifstream、fstream的每一个成员函数都有着默认的模式,如果打开一个文件而不输入其他参数的话。

  • ofstream :default mode——ios::out
  • ifstream :default mode——ios::in
  • fstream :default mode——ios::out|ios::in

对于ofstream和ifstream来说,两个参数都是自动赋予的,甚至在输入参数的时候没有包含in和out,他们也会和输入的那些参数组合。

但对于fstream来说,只要函数默认调用,也就是不传入任何参数时才是in|out。输入任意参数,那么这两个参数就会被覆盖掉,而不会默认组合。

文件流以二进制的方式打开,执行输入输出与任何格式都没有关系。非二进制文件比如说text文件,可能会在一些特殊的字符上发生转义(比如说换行符或回车符)。

因为一般来说文件流执行的第一个任务就是打开文件,因此这三个类都包含着构造函数,能够自动调用open这个成员函数,它的格式与open函数是一样的。因此,我们能够在声明一个流对象的时候就调用open函数:

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

这个语句与前一个调用open函数的语句是等价的。

为了检查文件流是否打开成功,可以通过调用成员is_open。这个成员函数返回的是一个布尔类型的值。

if (myfile.is_open()){/* code */}

关闭文件

当我们完成输入输出的操作之后,我们应该关闭文件,这样操作系统才能收到通知,这部分资源就能使用了。因此,我们调用流中的成员函数close。这个成员函数能够刷新关联的缓冲区并关闭文件:

myfile.close();

一旦调用这个成员函数,流对象就能用于打开其他文件,文件也能被其他进程打开。

如果对象在与open file关联时被销毁,那么类就会自动调用析构函数close。

text文件

text文件流是当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的返回值是流对象自己的引用,在布尔表达式里面,如果流文件还有行没读完就输出为真,输出为假时,要么就是到了文件末尾,要么就是读取错误。

检查状态标志

下面的成员函数的存在就是为了检查流是否出一个特殊的状态,他们都会输出布尔类型的值。

  • bad() :读取或写入操作失败时,返回真。比如说,比如说我们要写一个还没有打开的文件或者是设备已经没有空间剩余了。
  • fail() :与bad的情况类似,不过在格式错误发生的时候也返回真。比如说当我们试图读取一个整数时却读取了一个字母字符。
  • eof() :如果打开的文件读取到了末尾则返回真。
  • good() :它是最通用的状态检查标志。上面的任意函数返回的结果为真时他返回的结果为false。请注意,good和bad并不是完全相反的,比如说good返回false的时候,bad可能也返回false,因为流可能只是由格式上的错误或者读到了文件的末尾。

成员函数clear()能够被用于重新设置状态标志。

get和put的流定位

所有的 I/O 流在内部都至少保持在某一个内部位置。

ifstream与istream一样,保持一个内部get位置,其中包含下一个输入操作中要读取的元素的位置。

ofstream与ostream一样,保持一个内部put位置,即必须写入下一个元素的位置。

最后是fstream,它保持了get和put的位置,就像iostream。

这三个内部流的位置都指向流里面的,下一个读取或者写入操作执行的位置。这些位置使用以下函数进行观察和修改。

tellg() 和 tellp()

这两个不带函数的成员函数返回一个streampos类型的值,这个类型代表了目前get或者put所在的位置。

seekg() 和 seekp()

这几个函数允许我们修改get和put的位置。两个函数都能根据不同的原型进行重载,其第一种形式如下:

seekg(position);
seekp(position);

使用这一原型,流指针指向的位置变更为值为position的绝对值位置(从文件开头开始数)。这个参数也是streampos的参数,与函数tellg、tellp返回的类型相同。

另外一种形式是:

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

使用这一原型,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;

请注意,我们给变量begin、end赋予的类型是streampos。这是一个用于缓冲区和文件位置的特殊类型,由file.tellg()所返回。这个类型的值能够被相同类型的值加减,而且能够被转化为足以包含文件大小的足够大的整型数。

这些流定位函数使用了两个特殊的类型:streampos和streamoff。这些类型同样是stream类中定义的成员的类型。

  • 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这个参数则是一个整型数,其规定了读取或写入内存块的字符数量。

// 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;