第18讲:文件与字符串IO

《计算机程序设计》

苏醒

计算机学院计算机研究所编译系统研究室

课前准备

  • 在WSL Ubuntu中建立一个专门的目录用于课上练习,如~/course
$ mkdir ~/course
  • 打开VSCode并连接到WSL,打开此目录并新建文件,如~/course/main.cpp
  • 编辑main.cpp,随堂编程练习的代码请直接在此文件中编辑
main.cpp
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
  // ============= begin =============

  // =============  end  =============
  return 0;
}

温故:格式化标志位与IO操纵子

IO操纵子与相关的流状态/方法
操纵子 相关标志位/方法 描述
dec, oct, hex dec, oct, hex 设置数字进制
scientific, fixed, hexfloat, defaultfloat scientific, fixed 设置浮点数输出格式
left, right, internal left, right, internal 设置输出对齐方式
boolalpha, noboolalpha boolalpha 设置布尔数输入输出格式
showbase, noshowbase showbase 显示整数的进制前缀
showpoint, noshowpoint showpoint 显示浮点数的小数点
showpos, nonshowpos showpos 显示正数符号
skipws, noskipws skipws 跳过空白字符
uppercase, nouppercase uppercase 使用大写字母表示十六进制前缀
setw() width() 设置输出宽度
setfill() fill() 设置填充字符
setprecision() precision() 设置浮点数输出精度

温故:流状态

  • 流对象内部维护若干个状态标志,用于记录输入/输出流的状态
    • goodbit:流状态正常
    • eofbit:流到达文件尾
    • failbit:输入/输出操作失败(可通过clear()清除)
    • badbit:发射能够不可恢复错误(不可通过clear()清除)
流状态查询方法
方法 描述 方法 描述
eof() eofbit bad() badbit
good() goodbit fail() failbit || badbit
rdstate() 返回当前流状态 clear() 清除流状态错误

重审流式IO

思考

  • 从标准输入cin读取过的数据,能否再次读取?
  • 从标准输出cout输出过的数据,能否进行覆盖?
  • 能否不损失精度地将浮点数输出?
  • 能否像Python一样支持从数据生成复杂的格式化字符串?f'...'.format(...)
  • 标准输入输出流是一个顺序流,不可回溯——覆水难收
    • 已经读取的数据无法再次读取,已经输出的数据无法覆盖
  • 格式化输出的精度有限,大多数时候无法保留原数值精度
  • IO操纵子可以精准控制输出格式,但发送到标准输出后难以再次读入到字符串

学习目标

  • 学习内容
    • 文件IO
    • 字符串IO

学习目标

  • 理解不同的文件打开模式
  • 掌握非格式化输入输出与随机IO的方法
  • 掌握字符串IO的基本方法

学会以文件和字符串为数据载体的输入输出

一、文件IO

1.1 文件流

  • 文件流(file stream)是C++对于文件输入输出设备的抽象
    • 与标准输入输出流类似,数据可以从文件流中流入或流出
    • 文件流的方向分为输入流和输出流,也可以是双向流
  • 使用文件流,需包含<fstream>头文件,并根据需要构造流对象
文件流
#include <fstream> // 包含文件流头文件
using namespace std;

int main() {
  ifstream fin("input.txt");   // 构造输入文件流对象,从input.txt输入
  ofstream fout("output.txt"); // 构造输出文件流对象,向output.txt输出
  fstream fio;                 // 构造双向文件流对象,不关联任何文件
  fio.open("file.txt");        // 打开文件file.txt,支持从文件读与向文件写
  fin.close();  // 关闭文件流
  fout.close(); // 关闭文件流
  fio.close(); // 关闭文件流
  return 0;
}

1.1 文件流

  • 文件流的核心内涵:文件与程序间的数据通道
文件流
ifstream fin("input.txt");
ofstream fout("output.txt");
fstream fio;
fio.open("file.txt");
fin.close();
fout.close();
fio.close();

明察秋毫

  • 文件流是读写文件的通道,其本身不是持久化的,而是临时的
    • 给定文件名构造一个文件流对象(或调用open方法打开文件),表示建立程序与文件之间的数据通道
    • 文件流对象销毁(或调用close方法),表示关闭此数据通道
    • 在文件流打开期间,可以通过流对象对文件进行读写操作

1.2 打开与关闭文件

  • 打开方式1:构造文件流对象的同时打开文件
构造文件流对象的同时打开文件
ifstream fin("input.txt");   // 打开文件input.txt,默认为读模式
ofstream fout("output.txt"); // 打开文件output.txt,默认为写模式
  • 打开方式2:构造文件流对象后,调用open方法打开文件
构造文件流对象后打开文件
fstream fio;
if (not fio.is_open())   // 判断流对象是否打开了文件
  fio.open("/home/xsu/file.txt");  // 打开文件file.txt,默认为读写模式

明察秋毫

文件名可以是绝对路径相对路径,若为相对路径,则以当前工作目录为起点

  • 调用close方法可以关闭打开的文件(文件流对象销毁时也会自动关闭文件)
关闭文件
fio.close();

1.3 读写文件

  • 当文件流打开文件后,即可对文件进行读写操作
  • 对于格式化输入输出,文件流与标准输入输出流相同
    • 插入<<和提取运算符>>
    • 格式化标志位与IO操纵子
formattedoutput.cpp
#include <fstream>
#include <iomanip>
using namespace std;
int main() {
  const double PI = 3.141592653;
  ofstream fout("out.txt");
  fout << "PI is " << PI << endl;
  fout << "More precisely " << setprecision(10)
       << scientific << PI << endl;
  fout.close();
  return 0;
}
$ g++ -o code/formattedoutput code/formattedoutput.cpp
$ ./code/formattedoutput && cat out.txt
PI is 3.14159
More precisely 3.1415926530e+00

1.3 读写文件

  • 当文件流打开文件后,即可对文件进行读写操作
  • 对于格式化输入输出,文件流与标准输入输出流相同
    • 插入<<和提取运算符>>
    • 格式化标志位与IO操纵子
formattedinput.cpp
#include <fstream>
#include <iostream>
int main() {
  std::ifstream fin("out.txt");
  char text[128];
  while (true) {
    fin >> text;
    if (fin.eof()) break;
    std::cout << text << std::endl;
  }
  fin.close();
  return 0;
}
$ g++ -o code/formattedinput code/formattedinput.cpp
$ ./code/formattedinput
PI
is
3.14159
More
precisely
3.1415926530e+00

明察秋毫

文件流与标准输出输出流均基于“流”的抽象概念,提供一致的接口!

1.4 文件打开模式

  • 文件流打开文件时,可以指定打开模式
文件打开模式
模式 描述
ios::in 读模式
ios::out 写模式
ios::app 每次写都追加到文件尾部,仅与ios::out组合使用
ios::trunc 打开文件时清空文件内容,仅与ios::out组合使用
ios::ate 打开文件后将文件位置置于尾部,与ios::inios::out均可组合
ios::binary 打开文件为二进制模式,与ios::inios::out均可组合

明察秋毫

打开模式可组合使用,如ios::in | ios::out表示打开文件用于输入和输出

1.4 文件打开模式

  • 文件流打开文件时,可以指定打开模式
openmode.cpp
#include <fstream>
#include <iomanip>
using namespace std;
int main(int argc, char *argv[]) {
  const double PI = 3.141592653;
  ofstream fout("out.txt", ios::out | ios::app); // 打开一个文件用于输出,指定每次输出到文件末尾
  fout << "Actually, PI cannot be precisely presented" << endl;
  fout.close();
  return 0;
}
$ g++ -o code/openmode code/openmode.cpp
$ ./code/openmode && cat out.txt
PI is 3.14159
More precisely 3.1415926530e+00
Actually, PI cannot be precisely presented

Try it!

试试,如果不使用ios::app模式打开文件,运行程序,观察out.txt的内容

1.4 文件打开模式

  • 文件流打开文件时,不指定打开模式将使用缺省模式
    • 对于输入文件流ifstream,缺省模式为ios::in
    • 对于输出文件流ofstream,缺省模式为ios::out
    • 对于双向文件流fstream,缺省模式为ios::in | ios::out

明察秋毫

  • 打开文件时,ios::inios::out至少有一个被指定,称为基本模式
  • ios::appios::trunc仅与ios::out组合使用有意义
  • ios::ate指定文件打开后的读写位置为文件末尾(稍后介绍)
  • ios::binary指定文件以二进制模式打开(稍后介绍)

1.4 文件打开模式

考考你

当试图打开一个不存在的文件,会发生什么?提示:和基本模式相关

nonexistfile.cpp
#include <fstream>
#include <iostream>
#include <iomanip>
int main() {
  std::ifstream fin("1.txt", std::ios::in);   // 1.txt不存在
  std::cout << std::setw(8) << "in " << fin.is_open() << ' ' << fin.fail() << ' ' << fin.bad() << '\n';
  std::ofstream fout("2.txt", std::ios::out); // 2.txt不存在
  std::cout << std::setw(8) << "out " << fout.is_open() << ' ' << fout.fail() << ' ' << fout.bad() << '\n';
  std::fstream fio("3.txt", std::ios::in | std::ios::out); // 3.txt不存在
  std::cout << std::setw(8) << "in|out " << fio.is_open() << ' ' << fio.fail() << ' ' << fio.bad() << '\n';
  return 0;
}
$ g++ -o code/nonexistfile code/nonexistfile.cpp
$ ./code/nonexistfile
     in 0 1 0
    out 1 0 0
 in|out 0 1 0

明察秋毫

打开不存在的文件时,若指定了ios::in,则打开失败,failbit被置位,否则创建新文件

1.4 文本模式与二进制模式

  • 文件流支持两种模式读写文件
    • 文本模式:默认模式,以文本格式读写,使用格式化IO
    • 二进制模式:使用ios::binary模式,以二进制格式读写,使用非格式化IO

考考你

  • 当使用插入操作符向文件流写入一个双精度浮点数3.1415923653时,一共写入了几个字节?
  • 当使用提取操作符从文件流中读取一个双精度浮点数3.1415923653时,一共读取了几个字节?

1.5 文本模式与二进制模式

  • 在文本模式下,文件被视为文本文件
    • 格式化IO过程隐含了数据与字符串的转换
    • 写文件时,数据\(\rightarrow\)字符串
    • 读文件时,字符串\(\rightarrow\)数据
  • 在二进制模式下,文件被视为字节流
    • 非格式化IO仅执行文件与内存之间的数据传输
    • 写文件时,文件中的字节被传输到内存中
    • 读文件时,内存中的字节被传输到文件中

文本模式/格式化IO

二进制模式/非格式化IO

1.5 文本模式与二进制模式

考考你

文本模式与二进制模式下的输入输出,各有什么优缺点?

  • 文本模式
    • 输出文件具有可读性
    • IO效率较低
    • 文件体积较大
  • 二进制模式
    • 输出文件不具有可读性,容易产生移植性问题
    • IO效率较高
    • 文件体积较小

1.6 非格式化IO

  • 流对象提供专门方法用于支持非格式化IO
basic_istream& read(char* s, std::streamsize count);        // 从输入流中读取指定数量的字符到缓冲区
basic_ostream& write(const char* s, std::streamsize count); // 从缓冲区输出指定数量的字符到输出流中
unformattedio.cpp
#include <fstream>
using namespace std;
int main() {
  fstream fio("data.txt", ios::in | ios::out | ios::binary);
  char buf[100];
  streamsize count = 100;
  fio.read(buf, count);   // 读入100个字符到buf
  if (fio.fail()) {       // 读到文件尾部仍未达到count个字符,fail标志被置位
    count = fio.gcount(); // gount返回上一次非格式化读操作实际读取的字节数
    fio.clear();          // 清除错误标志
  }
  fio.write(buf, count);  // 将buf中的count个字符写入文件
  fio.close();
  return 0;
}
$ g++ -o code/unformattedio code/unformattedio.cpp
$ echo 'hello world' > data.txt && ./code/unformattedio && cat data.txt
hello world
hello world

1.6 非格式化IO

考考你

如何将一个双精度数输出到文件中,并且完美保留其精度?

输出一个双精度数并保留精度
#include <fstream>
int main() {
  double pi = 3.141592653;
  std::ofstream fout("data.txt", std::ios::out | std::ios::binary);

  fout.close();
  return 0;
}
输出一个双精度数并保留精度
#include <fstream>
int main() {
  double pi = 3.141592653;
  std::ofstream fout("data.txt", std::ios::binary);
  fout.write((char *)(&pi), sizeof(pi)); // 输出一个双精度数
  fout.close();
  return 0;
}

考考你

如何把一个二进制保存的双精度数从文件中读取出来?

1.6 非格式化IO

  • readwrite方法外,流对象还提供了其他非格式化输入方法
其他非格式化IO方法
方法 描述
put() 向输出流中写入一个字符
get() 从输入流中提取一个字符
getline() 从输入流中提取一行字符串
ignore() 跳过指定数量的字符
gcount() 返回上次输入操作提取的字符数

1.7 文件位置

  • 文件流使用一个内部状态记录当前读写操作的位置,称为文件位置(FP)
    • FP是一个整数(basic_ios::pos_type),表示从文件头部开始的偏移量
    • FP在文件打开时初始化,指定ios::ate模式时置于文件尾,否则置于文件头
    • 读写操作从FP标定位置执行,执行后FP会向后移动,移动量等于读写的字节数
filepos.cpp
#include <fstream>
#include <iostream>
using namespace std;
int main() {
  ofstream fout("data.txt");      // 默认为 ios::out
  cout << fout.tellp() << endl;   // tellp() 返回输出流的FP,初始化为0
  fout << "Hello World!" << endl; // 输出13个字符
  cout << fout.tellp() << endl;   // 当前位置13
  fout.seekp(0, ios::beg);        // 将文件指针移动到文件开头
  cout << fout.tellp() << endl;   // 当前位置0
  fout << "Hello World!" << endl; // 输出13个字符
  cout << fout.tellp() << endl;   // 当前位置13
  return 0;
}
$ g++ -o code/filepos code/filepos.cpp
$ rm -f data.txt && ./code/filepos && cat data.txt
0
13
0
13
Hello World!

1.7 文件位置

  • 文件流允许用户设置读取和写入文件的位置,实现随机IO
    • 使用tellg()/tellp()方法查询输入/输出流的FP
    • 使用seekg()/seekp()方法设置输入/输出流的FP
    • 对于双向文件流,读与写的文件位置相同
seekpos.cpp
#include <fstream>
#include <iostream>
int main() {
  int numbers[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, value = 0;
  std::ofstream fout("data.txt", std::ios::binary); // 一次性输出整个数组
  fout.write((char *)numbers, sizeof(numbers));
  fout.close();
  std::ifstream fin("data.txt", std::ios::in | std::ios::binary);
  fin.seekg(3 * sizeof(int), std::ios::beg); // 设置文件位置到第3个整数后
  fin.read((char *)&value, sizeof(int)); // 读取一个整数
  std::cout << value << std::endl;       // 输出4
  return 0;
}
$ g++ -o code/seekpos code/seekpos.cpp
$ rm -f data.txt && ./code/seekpos && cat data.txt
4
    

1.7 文件位置

  • 文件位置设置函数seekg()/seekp()有两种使用方式
    • 第一种:指定绝对位置
    • 第二种:指定相对位置
设置文件位置
#include <fstream>
int main() {
  std::ifstream fin("data.txt", std::ios::in);
  fin.seekg(10); // 设置文件位置为10
  fin.seekg(10, std::ios::beg);  // 设置文件位置为文件头部+10
  fin.seekg(-10, std::ios::cur); // 设置文件位置为当前位置-10
  fin.seekg(-10, std::ios::end); // 设置文件位置为文件尾部-10
  fin.close();
  return 0;
}

1.7 文件尾部判定

考考你

请编写程序,以二进制方式不断从文件读入整数,直到把整个文件读完,输出所有整数

eof.cpp
#include <fstream>
#include <iostream>
using namespace std;
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  ofstream fout("data.txt", ios::binary);
  fout.write((char *)arr, sizeof(arr));
  fout.close();
  ifstream fin("data.txt", ios::binary);
  int value;
  while (not fin.eof()) {
    fin.read((char *)&value, sizeof(value));
    cout << value << ' ';
  }
  return 0;
}
$ g++ -o code/eof code/eof.cpp
$ rm -f data.txt && ./code/eof
1 2 3 4 5 5 

1.7 文件尾部判定

考考你

请编写程序,以二进制方式不断从文件读入整数,直到把整个文件读完,输出所有整数

eof2.cpp
#include <fstream>
#include <iostream>
using namespace std;
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  ofstream fout("data.txt", ios::binary);
  fout.write((char *)arr, sizeof(arr));
  fout.close();
  ifstream fin("data.txt", ios::binary);
  int value;
  while (fin.read((char *)&value, sizeof(value))) {
    cout << value << ' ';
  }
  return 0;
}
$ g++ -o code/eof2 code/eof2.cpp
$ rm -f data.txt && ./code/eof2
1 2 3 4 5 

文件IO小结

  • 文件流是C++对文件输入输出设备的抽象
  • 文件的打开模式
    • 读/写
    • 文本/二进制
    • 追加与清空
  • 非格式化IO方法readwrite
  • 文件位置与文件随机访问

二、字符串IO

2.1 字符串流

  • C++支持以字符串作为输入的来源与输出的目标
    • 应用:利用IO方法灵活地构造字符串、分析字符串中的信息
  • C++字符串流(string stream)是C++对字符串输入输出设备的抽象
    • 数据可以从字符串流中流入或流出
    • 字符串流可以是输入流、输出流或双向流
字符串输入流
#include <iostream>
#include <sstream> // 包含字符串流头文件
int main() {
  std::string str = "PI is 3.1415926";
  std::istringstream iss(str); // 从字符串构造字符串输入流
  char buf[100];
  double pi = 0;
  iss >> buf >> buf >> pi; // 从字符串流中读取数据
  std::cout << pi << std::endl;
  return 0;
}
字符串输出流
#include <iostream>
#include <sstream> // 包含字符串流头文件
#include <iomanip>
int main() {
  std::string str;
  std::ostringstream oss(str); // 从字符串构造字符串输出流
  // 向字符串流中写入数据
  oss << "PI is " << setprecision(8) << 3.1415926 << '\n';
  std::cout << oss.str() << std::endl; // 获取输出的字符串
  return 0;
}

2.2 字符串流的方法

  • 字符串流提供与文件流一致的接口
    • 格式化IO、非格式化IO、文件位置、状态标志与IO操纵子
  • 字符串流的特殊方法——str()方法
字符串流的str方法
#include <iostream>
#include <sstream>
#include <iomanip>
int main() {
  std::string str;
  std::ostringstream oss; // 构造空字符串流
  oss.str(str);           // 设置字符串流的输出目标
  oss << "PI is " << setprecision(8) << 3.1415926 << '\n';
  std::cout << oss.str() << std::endl; // 获取输出的字符串
  return 0;
}

总结

本节内容

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 24px
---
mindmap
  文件IO与字符串IO
    文件IO
      文件流
      打开与关闭文件
      文件打开模式
      二进制模式
      非格式化IO方法
      文件位置
    字符串IO
      字符串流
      str方法

学习目标

  • 理解不同的文件打开模式
  • 掌握非格式化输入输出与随机IO的方法
  • 掌握字符串IO的基本方法

课后作业

  • 自学
    • 教材9.3,9.6节 C风格文件IO
  • 实训(截止时间2025.05.05
    • C&C++文件实训(第1–2关需自学9.3节后完成)
  • 预习
    • 教材5.5,5.8 函数重载与递归函数

计算机程序设计