1、资料清单
| 资料 |
内容 |
入口 |
| 教材 |
《C++语言程序设计(第4版)》Chap11,原书页码 497-527 |
抽取 Markdown 下载 |
| 课件 |
Chap11 课件,位于本地郑莉 C++ 资料目录 |
本地资料夹 |
| 例题源代码 |
C++V5 源代码 |
下载 |
| VS2019 工程 |
C++V5 VS2019 solution |
下载 |
Chap11 的主线是流的抽象 → 标准输入输出 → 格式控制 → 文件流 → 字符串流 → 对象持久化。前面章节主要在内存里处理对象和容器,这章开始把数据从程序送到屏幕、文件和字符串缓冲区。
2、颜色标注
| 颜色 |
含义 |
使用位置 |
| 蓝色 |
核心术语、定义 |
流、插入、提取、缓冲、文件流 |
| 绿色 |
推荐写法、主线 |
RAII 自动关闭文件、std::getline、显式检查打开状态 |
| 黄色 |
易错点、边界条件 |
>> 与 getline 混用、格式状态遗留、文本/二进制差异 |
| 红色 |
不建议做法 |
不检查文件是否打开、直接把复杂对象裸写入文件 |
3、学习目标
| 目标 |
要会到什么程度 |
| 流的概念 |
理解输入、输出、提取、插入和缓冲 |
| 标准流 |
会区分 cin、cout、cerr、clog |
| 格式控制 |
会使用 setw、setprecision、fixed、hex、oct |
| 文件流 |
会读写文本文件,知道 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 cerr 和 clog 有何区别?
| 对象 |
区别 |
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 内容为:
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; }
|
输出:
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; }
|
输出:
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();
std::cout << 812 << '|';
std::cout.setf(std::ios_base::left, std::ios_base::adjustfield); std::cout.width(10); std::cout << 813 << 815 << '\n';
std::cout.unsetf(std::ios_base::adjustfield); std::cout.precision(2); std::cout.setf(std::ios_base::uppercase | std::ios_base::scientific); std::cout << 831.0 << '\n';
std::cout.flags(originalFlags);
return 0; }
|
语句说明:
| 编号 |
作用 |
| 1 |
保存当前格式标志,便于最后恢复 |
| 2 |
把对齐方式设置为左对齐 |
| 3 |
设置下一个输出项宽度为 10 |
| 4 |
清除对齐字段设置 |
| 5 |
设置大写科学计数法输出 |
| 6 |
恢复原来的格式标志 |
输出大致为:
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、本章复盘清单
| 检查项 |
是否掌握 |
| 能解释流、插入、提取的含义 |
□ |
能区分 cout、cerr、clog 的用途 |
□ |
能用 <iomanip> 控制宽度、精度、进制和对齐 |
□ |
| 能写出文本文件的创建、读取和追加程序 |
□ |
| 能说明文本文件和二进制文件的区别 |
□ |
能用 read / write 读写简单二进制数据 |
□ |
能用 stringstream 解析字符串 |
□ |
| 能说明为什么复杂对象不应直接裸写入文件 |
□ |
这章复盘时可以把重点放在“流对象 + 状态检查 + 格式控制 + 文件生命周期”上。后续做项目时,日志、配置、实验数据保存、用户输入解析,都会回到这一章的知识。