文件读写#

通过 #include <fstream> 中的 ofstreamifstreamfstream 文件流, 你可以对文件进行写入、读取或读写.

它们的用法于 coutcin 相似, 因此请参考 几种 cin 输入方式的区别 等了解. 以下解释如何打开文件、读取到了文件末尾和关闭文件.

打开文件#

要使用文件流, 我们需要用它先打开一个文件:

 1#include <fstream>
 2using namespace std;
 3
 4int main() {
 5  ifstream ifile;
 6  ifile.open("C:/test.txt", std::ios_base::in);
 7
 8  // 然后就能像 cin 一样使用 ifstream
 9  int value = 0;
10  ifile >> value;
11}

这样就打开了一个文件, 但是何必呢?!

你没必要指明 std::ios_base::in, ifstream (input file stream) 始终会给文件打开方式加上它:

1int main() {
2  ifstream ifile;
3  ifile.open("C:/test.txt");
4}

你没必要分出一行写, 可以在声明 ifile 时就让它打开文件:

1int main() {
2  ifstream ifile("C:/test.txt");
3}
"可是我之前的写法也是可以的啊……"
  1. 你如果不是为了读取文件的内容, 何必用 ifstream? 既然用 ifstream 就是想读取文件内容, 那何必再加一句 std::ios_base::in? 你用 ifstream, 就是为了读取文件.

  2. 你如果不是为了现在打开这个文件, 何必去声明 ifstream ifile? 既然你声明 ifstream ifile 就是要打开文件, 何必再在额外一行去写一个 ifile.open 来打开文件? 你声明 ifstream ifile, 就是为了在此时此刻打开文件.

写代码必须注意代码的目的性, 没有明确代码各部分的目的是写不出代码的直接原因之一.

同理, ofstream 会始终在打开方式中加上 std::ios_base::out, fstream 会始终在打开方式中加上 std::ios_base::in | std::ios_base::out, 哪怕你给它其他的打开方式.

判断打开成功#

通过 ifile.is_open(), 我们可以判断上一次打开文件是否成功:

1#include <cstdlib>  // for exit, EXIT_FAILURE, EXIT_SUCCESS
2
3int main() {
4  ifstream ifile("C:/test.txt");
5  if (!ifile.is_open()) {
6    cerr << "错误信息\n";  // err 的意思是 error
7    exit(EXIT_FAILURE);
8  }
9}

读取文件到末尾#

在课上可能像下面这样写, 千万千万千万千万 不要用!!!

1int main() {
2  ifstream ifile("C:/test.txt");
3  for (int value = 0; !ifile.eof();) {  // 或者有些奇怪的老师会写 ifile.eof() == 0
4    ifile >> value;
5
6    cout << value;
7  }
8}

是, 没错, 这样是可以判断文件到没到文件尾 (eof, end of file), 但是其他问题呢? 例如, 现在循环里是在读取 int 类型, 那如果文件里有一个不是数字的字符呢? 读取将会失败, 你将永远不能达到文件尾, 这个循环将会永远继续下去.

前置内容

在往下看正确的方案之前, 请先阅读 几种 cin 输入方式的区别 直到读完 "共同部分".

数据读写有三类错误状态: eof (到达读取数据的尾部)、fail (读写操作失败)、bad (读写流本身出现不可逆转的问题). 读写操作失败 fail 才是你真正想要的, 它涵盖了 bad 和 eof 的几乎所有情况, 唯一没涵盖的某种 eof 情况也不是你真正想要的.

因此, 我们应该使用 !ifile.fail() 来表达读取有没有结束, 而 几种 cin 输入方式的区别 的共同部分已经说了怎么写:

 1int main() {
 2  ifstream ifile("C:/test.txt");
 3  if (!ifile.is_open()) {
 4    cerr << "打开文件失败!\n";
 5    exit(EXIT_FAILURE);
 6  }
 7
 8
 9  for (int value = 0; ifile >> value;) {
10    cout << value;
11  }
12
13  if (ifile.bad()) {
14    cerr << "读写流本身出现不可逆转的问题!\n";
15    exit(EXIT_FAILURE);
16  } else if (ifile.eof()) {
17    cout << "成功读取到了文件尾!\n";
18  } else if (ifile.fail()) {
19    cerr << "读写操作出现问题!\n";
20    exit(EXIT_FAILURE);
21  }
22}

备注

注意, ifile.good() != !ifile.fail(), 就像下面表里列的那样, 但别去记这玩意. 上面已经说了, 用 !ifile.fail()!

../../_images/iostate.png

关闭文件#

太长别看: 你不需要手动用 file.close() 来关闭文件, 用这种方式只会让人知道你真的不会 C++.

C++ 中的对象存在 生命期和存储周期 的概念. 简单来说, 你在大括号 {} 内声明的变量, 到 } 时就会死掉.

 1int main() {
 2  int value1 = 0;
 3  cin >> value1;
 4
 5  if (value != 0) {
 6    int value2 = value1 * value1;
 7    cout << value2;
 8  }  // value2 死掉了
 9
10  cout << value2;  // 错误: value2 死都死了, 你不可能用它
11}  // value1 死掉了

ofstreamifstreamfstream 死掉时, 它们会自动关闭文件.

1int main() {
2  std::ifstream ifile("C:/test.txt");
3
4  int value = 0;
5  ifile >> value;
6}  // ifile 死了, 它自动关闭文件!!!

参见

文件需要我们预先获取、之后释放, 因而是一种 "资源". 在 资源 (resource) 中, 我解释了资源的所有权问题和 C++ 中资源的最佳管理方案——RAII, 这就是 ofstreamifstreamfstream 采用的方案.

读写同一个文件#

  • 我们打开一个文件对它进行写入时, 是先将数据写入到缓冲区, 再在合适时候 (比如) 一次性从缓冲区写入到文件.

  • 我们打开一个文件对它进行读取时, 是先将文件读入到缓冲区, 再从缓冲区中读取数据.

这意味着, 如果我们先用 ofstream 写入文件, 在 ofstream 还没有关闭文件时, 就用 ifstream 可读取同一文件, 那么可能读取的是原来文件中的数据. (想想你用 Visual Studio 写代码忘了保存, 觉得自己改动没有效果然后发图片问问题. 这就是你没有将缓冲区 (代码编辑区) 的内容写入到文件中.)

"这总能用 file.close() 了吧?" 但你觉得下面的代码方便阅读吗?

你可以人为添加 {}; 或者往往更好地, 既然是写入和读取是在做不同的事情, 为什么不写成分别的函数?

 1int main() {
 2  {
 3    ofstream ofile("C:/test.txt");
 4    ofile << 682132891;
 5  }
 6  {
 7    ifstream ifile("C:/test.txt");
 8    int value = 0;
 9    ifile >> value;
10  }
11}

当然你可以用既能写入又能读取的 fstream, 但我认为反而过于麻烦了:

1int main() {
2  fstream file("C:/test.txt");
3  file << 682132891;
4
5  file.seekp(0);  // 你能看懂这个函数是回到文件开头吗?
6
7  int value = 0;
8  ifile >> value;
9}

二进制文件读写#

二进制文件读写见于 二进制输入输出 (binary I/O).