1、资料清单

资料 内容 入口
教材 《C++语言程序设计(第4版)》Chap11,原书页码 497-527 抽取 Markdown 下载
课件 Chap11 课件,位于本地郑莉 C++ 资料目录 本地资料夹
例题源代码 C++V5 源代码 下载
VS2019 工程 C++V5 VS2019 solution 下载

Chap11 的主线是流的抽象 → 标准输入输出 → 格式控制 → 文件流 → 字符串流 → 对象持久化。前面章节主要在内存里处理对象和容器,这章开始把数据从程序送到屏幕、文件和字符串缓冲区。

2、颜色标注

颜色 含义 使用位置
蓝色 核心术语、定义 流、插入、提取、缓冲、文件流
绿色 推荐写法、主线 RAII 自动关闭文件、std::getline、显式检查打开状态
黄色 易错点、边界条件 >>getline 混用、格式状态遗留、文本/二进制差异
红色 不建议做法 不检查文件是否打开、直接把复杂对象裸写入文件

3、学习目标

目标 要会到什么程度
流的概念 理解输入、输出、提取、插入和缓冲
标准流 会区分 cincoutcerrclog
格式控制 会使用 setwsetprecisionfixedhexoct
文件流 会读写文本文件,知道 ios::app 追加模式
二进制文件 会使用 read / write 读写字节
字符串流 会用 stringstream 做字符串解析和格式化
对象串行化 知道直接写对象内存的限制,优先做显式字段保存

4、章节目录

小节 内容 本节关键词
11.1 I/O 流的概念及流类库结构 抽象、流缓冲、输入输出
11.2 输出流 ostream、插入运算符、格式控制
11.3 输入流 istream、提取运算符、getline
11.4 输入输出流 iostream、文件读写定位
11.5 综合实例 银行账户程序改进
11.6 深度探索 宽字符、宽流、对象串行化
11.7 小结 文件、格式、状态

5、I/O 流的基本概念

是一种抽象:它把数据的生产者和消费者连接起来,并负责数据的传输过程。键盘、屏幕、文件、字符串缓冲区都可以被统一看成流。

操作 C++ 写法 含义
插入 cout << value 把数据写入输出流
提取 cin >> value 从输入流读取数据
读字符 in.get(ch) 从输入流取一个字符
读一行 std::getline(in, line) 从输入流读取整行
写字节 out.write(...) 向输出流写原始字节
读字节 in.read(...) 从输入流读原始字节

标准流对象如下:

对象 方向 缓冲 用途
std::cin 输入 有缓冲 从键盘或标准输入读取
std::cout 输出 有缓冲 普通输出
std::cerr 输出 通常无缓冲 立刻输出错误信息
std::clog 输出 有缓冲 日志类错误信息

std::cerr 适合马上看到的错误;std::clog 适合日志式输出。两者默认都写到标准错误流,但缓冲策略不同。

6、输出流与格式控制

输出流最常见的接口是插入运算符 <<。格式控制可以通过 <iomanip> 中的操纵符完成。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iomanip>
#include <iostream>

int main() {
double pi = 3.1415926;

std::cout << std::setw(10) << 812 << "|\n";
std::cout << std::fixed << std::setprecision(2) << pi << "\n";
std::cout << std::hex << 255 << "\n";

return 0;
}

常用格式控制:

操纵符/接口 作用 示例
setw(n) 设置下一个输出域宽度 setw(10) << x
setprecision(n) 设置有效数字或小数位 fixed << setprecision(2)
fixed 使用定点小数格式 3.14
scientific 使用科学计数法 8.31E+02
left / right 左对齐 / 右对齐 表格输出
dec / oct / hex 十进制 / 八进制 / 十六进制 进制转换输出

格式状态会保留在流对象中,临时设置后如果后续输出不想受影响,可以保存并恢复:

1
2
3
std::ios_base::fmtflags oldFlags = std::cout.flags();
std::cout << std::hex << 255 << "\n";
std::cout.flags(oldFlags);

7、输入流

输入流最常见的接口是提取运算符 >>,它会跳过空白字符,适合读单个数字、单个单词。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string>

int main() {
int age = 0;
std::string name;

std::cin >> name >> age;
std::cout << name << " " << age << "\n";

return 0;
}

如果要读取包含空格的一整行,使用 std::getline

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <string>

int main() {
std::string line;
std::getline(std::cin, line);
std::cout << line << "\n";

return 0;
}

>>getline 混用时,>> 读完数据后可能留下换行符。后面立刻 getline 时,常常需要先用 std::cin.ignore() 清掉残留换行。

8、文件流

文件流定义在 <fstream> 中:

类型 方向 用途
std::ifstream 输入 读文件
std::ofstream 输出 写文件
std::fstream 输入 + 输出 同时读写

文本文件写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <fstream>
#include <iostream>

int main() {
std::ofstream out("test1.txt");

if (!out) {
std::cerr << "文件打开失败\n";
return 1;
}

out << "已成功写入文件!\n";

return 0;
}

文本文件读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <fstream>
#include <iostream>
#include <string>

int main() {
std::ifstream in("test1.txt");

if (!in) {
std::cerr << "文件打开失败\n";
return 1;
}

std::string line;
while (std::getline(in, line)) {
std::cout << line << "\n";
}

return 0;
}

追加写入使用 std::ios::app

1
2
std::ofstream out("test1.txt", std::ios::app);
out << "已成功添加字符!\n";

局部文件流对象离开作用域时会自动关闭文件,这就是 RAII。实际写代码时仍然要检查打开是否成功。

9、字符串流

字符串流定义在 <sstream> 中,适合把字符串当作输入输出流处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <sstream>
#include <string>

int main() {
std::string text = "Alice 20 95.5";
std::istringstream in(text);

std::string name;
int age = 0;
double score = 0.0;

in >> name >> age >> score;

std::ostringstream out;
out << name << " age=" << age << " score=" << score;

std::cout << out.str() << "\n";

return 0;
}

stringstream 常用于:

场景 说明
字符串解析 把一行文本拆成多个字段
格式化文本 拼接带格式的输出结果
类型转换 数字和字符串之间转换
临时缓冲 先组织文本,再一次性写入文件

10、文本文件与二进制文件

文本文件保存的是字符,便于人直接阅读;二进制文件保存的是内存字节,更紧凑,但不适合直接打开阅读。

对比 文本文件 二进制文件
可读性 人可以直接看 通常不可直接阅读
体积 可能更大 通常更紧凑
移植性 更好 受类型大小、字节序、平台影响
换行处理 Windows 文本模式可能转换换行 不做换行转换
适合场景 配置、日志、CSV 图片、音频、结构化字节数据

二进制写入基本形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <fstream>
#include <iostream>

int main() {
int value = 10;

std::ofstream out("value.bin", std::ios::binary);
if (!out) {
std::cerr << "文件打开失败\n";
return 1;
}

out.write(reinterpret_cast<const char*>(&value), sizeof(value));

return 0;
}

二进制读取基本形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <fstream>
#include <iostream>

int main() {
int value = 0;

std::ifstream in("value.bin", std::ios::binary);
if (!in) {
std::cerr << "文件打开失败\n";
return 1;
}

in.read(reinterpret_cast<char*>(&value), sizeof(value));
std::cout << value << "\n";

return 0;
}

直接把对象内存写入文件只适合非常简单、布局稳定的对象。包含指针、虚函数、动态内存、字符串、容器的对象,应该显式保存字段,而不是裸写内存。

11、课后习题答案

11-1 什么叫做流?流的提取和插入是指什么?

流是一种数据传输抽象,它负责在数据生产者和数据消费者之间建立联系,并管理数据流动。一般意义上的读操作,在流模型中叫做提取,对应 >>;写操作叫做插入,对应 <<

C++ I/O 流的作用是把键盘、屏幕、文件、字符串缓冲区等输入输出设备统一成对象接口,程序可以用相似的方式读写不同目标。

11-2 cerrclog 有何区别?

对象 区别
cerr 标准错误输出,通常无缓冲,适合马上显示的错误信息
clog 标准日志输出,有缓冲,适合日志式错误信息

两者默认都输出到标准错误流,区别主要在缓冲策略。

11-3 以文本方式建立 test1.txt 并写入字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <fstream>
#include <iostream>

int main() {
std::ofstream file("test1.txt");

if (!file) {
std::cerr << "无法打开 test1.txt\n";
return 1;
}

file << "已成功写入文件!";

return 0;
}

运行后,test1.txt 内容为:

1
已成功写入文件!

11-4 打开 test1.txt 并读出内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <fstream>
#include <iostream>

int main() {
std::ifstream file("test1.txt");

if (!file) {
std::cerr << "无法打开 test1.txt\n";
return 1;
}

char ch = '\0';
while (file.get(ch)) {
std::cout << ch;
}

return 0;
}

输出:

1
已成功写入文件!

11-5 在 test1.txt 后追加字符并读出整个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <fstream>
#include <iostream>

int main() {
{
std::ofstream out("test1.txt", std::ios::app);

if (!out) {
std::cerr << "无法打开 test1.txt 进行追加\n";
return 1;
}

out << "已成功添加字符!";
}

std::ifstream in("test1.txt");

if (!in) {
std::cerr << "无法打开 test1.txt 进行读取\n";
return 1;
}

char ch = '\0';
while (in.get(ch)) {
std::cout << ch;
}

return 0;
}

输出:

1
已成功写入文件!已成功添加字符!

11-6 定义 Dog 类并用文本方式和二进制方式保存状态

题目要求把 dog1 的状态写入磁盘文件,再读回到 dog2。这里分别给出文本方式和二进制方式。

文本方式更可读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <fstream>
#include <iostream>

class Dog {
public:
Dog(int weight = 0, int age = 0) : weight_(weight), age_(age) {}

int weight() const {
return weight_;
}

int age() const {
return age_;
}

void setWeight(int weight) {
weight_ = weight;
}

void setAge(int age) {
age_ = age;
}

private:
int weight_;
int age_;
};

int main() {
Dog dog1(5, 10);

{
std::ofstream out("dog.txt");
if (!out) {
std::cerr << "无法写入 dog.txt\n";
return 1;
}

out << dog1.weight() << " " << dog1.age() << "\n";
}

Dog dog2;

{
std::ifstream in("dog.txt");
if (!in) {
std::cerr << "无法读取 dog.txt\n";
return 1;
}

int weight = 0;
int age = 0;
in >> weight >> age;

dog2.setWeight(weight);
dog2.setAge(age);
}

std::cout << "Dog2 weight: " << dog2.weight() << "\n";
std::cout << "Dog2 age: " << dog2.age() << "\n";

return 0;
}

二进制方式更接近原题答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <fstream>
#include <iostream>

class Dog {
public:
Dog(int weight = 0, int age = 0) : weight_(weight), age_(age) {}

int weight() const {
return weight_;
}

int age() const {
return age_;
}

private:
int weight_;
int age_;
};

int main() {
Dog dog1(5, 10);

{
std::ofstream out("dog.bin", std::ios::binary);
if (!out) {
std::cerr << "无法写入 dog.bin\n";
return 1;
}

out.write(reinterpret_cast<const char*>(&dog1), sizeof(dog1));
}

Dog dog2;

{
std::ifstream in("dog.bin", std::ios::binary);
if (!in) {
std::cerr << "无法读取 dog.bin\n";
return 1;
}

in.read(reinterpret_cast<char*>(&dog2), sizeof(dog2));
}

std::cout << "Dog2 weight: " << dog2.weight() << "\n";
std::cout << "Dog2 age: " << dog2.age() << "\n";

return 0;
}

运行输出:

1
2
Dog2 weight: 5
Dog2 age: 10

文本文件中的内容类似 5 10,二进制文件中保存的是对象的原始字节。Windows 文本模式可能把 \n 转换成 \r\n,二进制模式不会做这种转换。

11-7 说明格式控制程序每条语句作用和运行结果

原程序的核心是保存格式状态、设置左对齐和宽度、设置科学计数法,最后恢复原状态。整理成现代写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

int main() {
std::ios_base::fmtflags originalFlags = std::cout.flags(); // 1

std::cout << 812 << '|';

std::cout.setf(std::ios_base::left, std::ios_base::adjustfield); // 2
std::cout.width(10); // 3
std::cout << 813 << 815 << '\n';

std::cout.unsetf(std::ios_base::adjustfield); // 4
std::cout.precision(2);
std::cout.setf(std::ios_base::uppercase | std::ios_base::scientific); // 5
std::cout << 831.0 << '\n';

std::cout.flags(originalFlags); // 6

return 0;
}

语句说明:

编号 作用
1 保存当前格式标志,便于最后恢复
2 把对齐方式设置为左对齐
3 设置下一个输出项宽度为 10
4 清除对齐字段设置
5 设置大写科学计数法输出
6 恢复原来的格式标志

输出大致为:

1
2
812|813       815
8.31E+02

width(10) 只影响紧跟着的下一个输出项,所以宽度只作用于 813

11-8 输入一个十进制整数,分别用十进制、八进制和十六进制输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

int main() {
int n = 0;

std::cout << "请输入一个十进制整数:";
std::cin >> n;

std::cout << "这个数的十进制形式为:" << std::dec << n << "\n";
std::cout << "这个数的八进制形式为:" << std::oct << n << "\n";
std::cout << "这个数的十六进制形式为:" << std::hex << n << "\n";

return 0;
}

输入 15 时输出:

1
2
3
这个数的十进制形式为:15
这个数的八进制形式为:17
这个数的十六进制形式为:f

11-9 打开一个文本文件,在每一行前加行号

原答案使用了已经过时的 strstream。下面用 std::vector<std::string>std::getline 改写,逻辑更直接:先读完整个文件,再重新写回带行号的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <fstream>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "用法: add_line_number <filename>\n";
return 1;
}

const std::string filename = argv[1];

std::ifstream in(filename);
if (!in) {
std::cerr << "无法打开输入文件: " << filename << "\n";
return 1;
}

std::vector<std::string> lines;
std::string line;

while (std::getline(in, line)) {
lines.push_back(line);
}

in.close();

std::ofstream out(filename);
if (!out) {
std::cerr << "无法打开输出文件: " << filename << "\n";
return 1;
}

for (std::size_t i = 0; i < lines.size(); ++i) {
out << std::setw(2) << i + 1 << ". " << lines[i] << "\n";
}

return 0;
}

运行方式:

1
./add_line_number text1.txt

运行前:

1
2
3
aaaaaaaaaaaa
bbbbbbbbbbbb
cccccccccccc

运行后:

1
2
3
1. aaaaaaaaaaaa
2. bbbbbbbbbbbb
3. cccccccccccc

12、本章复盘清单

检查项 是否掌握
能解释流、插入、提取的含义
能区分 coutcerrclog 的用途
能用 <iomanip> 控制宽度、精度、进制和对齐
能写出文本文件的创建、读取和追加程序
能说明文本文件和二进制文件的区别
能用 read / write 读写简单二进制数据
能用 stringstream 解析字符串
能说明为什么复杂对象不应直接裸写入文件

这章复盘时可以把重点放在“流对象 + 状态检查 + 格式控制 + 文件生命周期”上。后续做项目时,日志、配置、实验数据保存、用户输入解析,都会回到这一章的知识。