IO和文件
# 第 13 章 C++ IO和文件
# 目录介绍
# 13.1 输入输出介绍
C++ 中的 I/O 流 和 文件操作 是处理输入输出的重要工具。C++ 标准库提供了丰富的类和函数,用于从控制台、文件和其他设备读取和写入数据。
# 13.1.1 输入输出流
流是字节序列。如果字节流是从设备流向内存,这叫做输入操作。如果字节流是从内存流向设备,这叫做输出操作。
学过 C 语言的读者应该知道,它有一整套完成数据读写(I/O)的解决方案:
- 使用 scanf()、gets() 等函数从键盘读取数据,使用 printf()、puts() 等函数向屏幕上输出数据;
- 使用 fscanf()、fgets() 等函数读取文件中的数据,使用 fprintf()、fputs() 等函数向文件中写入数据。
C 语言的这套 I/O 解决方案也适用于 C++ 程序,但 C++ 并没有“偷懒”,它自己独立开发了一套全新的 I/O 解决方案,其中就包含大家一直使用的 cin 和 cout。
- 用 cin 接收从键盘输入的数据,用 cout 向屏幕上输出数据(这 2 个过程又统称为“标准 I/O”)。
- 除此之外,C++ 也对从文件中读取数据和向文件中写入数据做了支持(统称为“文件 I/O”)。
标准输入输出
std::cin:标准输入流(键盘输入)。std::cout:标准输出流(控制台输出)。std::cerr:标准错误流(无缓冲)。std::clog:标准日志流(有缓冲)。
# 13.1.2 I/O库头文件
ios 是所有流类的基类,它派生出 istream 和 ostream。
iostream头文件:从标准流中读写数据,istream、ostream等。分别对应于标准输入流、标准输出流。 fstream头文件:从文件中读写数据,ifstream、ofstream等。为用户控制的文件处理声明服务。 sstream头文件:从字符串中读写数据,istringstream、ostringstream。
特别需要指出的是,为了避免多继承的二义性,从 ios 派生出 istream 和 ostream 时,均使用了 virtual 关键字(虚继承)。
这些流类各自的功能分别为:
C++ 的 I/O 流基于流类库,主要包含以下类:
std::istream:输入流基类。常用于接收从键盘输入的数据;std::ostream:输出流基类。常用于将数据输出到屏幕上;std::iostream:输入输出流基类。继承自 istream 和 ostream 类,因为该类的功能兼两者于一身,既能用于输入,也能用于输出;std::ifstream:文件输入流。用于读取文件中的数据;std::ofstream:文件输出流。用于向文件中写入数据;std::fstream:文件输入输出流。兼 ifstream 和 ofstream 类功能于一身,既能读取文件中的数据,又能向文件中写入数据。
# 13.1.3 标准输出流cout
在C++中,cout是标准输出流对象,用于向控制台或终端输出数据。它是iostream库中的一部分,可以通过包含头文件
使用cout进行输出非常简单,只需使用<<运算符将要输出的数据插入到cout对象中即可。以下是一些示例用法:
#include <iostream>
int main() {
int number = 42;
double pi = 3.14159;
std::string message = "Hello, world!";
std::cout << "This is a number: " << number << std::endl;
std::cout << "This is pi: " << pi << std::endl;
std::cout << "This is a message: " << message << std::endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
cout对象可以与各种数据类型一起使用,包括基本数据类型(如整数、浮点数、字符等)和自定义类型(如字符串、数组、对象等)。
需要注意的是,cout是一个全局对象,属于std命名空间。因此,在使用cout之前,我们需要使用std::前缀来指定命名空间,或者使用using namespace std;语句来引入整个std命名空间。
# 13.1.4 标准输入流cin
在C++中,cin是标准输入流对象,用于从控制台或终端读取用户输入的数据。它也是iostream库的一部分,可以通过包含头文件
使用cin进行输入非常简单,只需使用>>运算符将输入的数据存储到相应的变量中。以下是一些示例用法:
#include <iostream>
int main() {
int number;
double pi;
std::string message;
std::cout << "Enter a number: ";
std::cin >> number;
std::cout << "Enter the value of pi: ";
std::cin >> pi;
std::cout << "Enter a message: ";
std::cin.ignore(); // 忽略之前的换行符
std::getline(std::cin, message);
std::cout << "You entered: " << number << ", " << pi << ", " << message << std::endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
使用cin对象从用户那里读取了一个整数、一个浮点数和一行字符串。每个输入都使用>>运算符将输入的数据存储到相应的变量中。std::getline函数用于读取一行字符串,而std::cin.ignore()函数用于忽略之前的换行符。
# 13.1.5 标准错误流cerr
cerr是标准错误流对象,用于向控制台或终端输出错误信息。它也是iostream库的一部分,可以通过包含头文件
使用cerr进行输出也非常简单,只需使用<<运算符将要输出的错误信息插入到cerr对象中即可。以下是一个示例用法:
#include <iostream>
int main() {
int dividend = 10;
int divisor = 0;
if (divisor == 0) {
std::cerr << "Error: Division by zero!" << std::endl;
} else {
int result = dividend / divisor;
std::cout << "Result: " << result << std::endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
进行了一个除法运算,但是当除数为零时,会输出一个错误信息到cerr对象中。这样可以将错误信息与正常输出分开,使其更易于识别和处理。
与cout不同的是,cerr对象的输出通常会直接显示在控制台或终端的错误输出流中,而不会被重定向或缓冲。这使得cerr适用于输出紧急或重要的错误信息,以便及时通知用户。
# 13.1.6 标准日志流clog
clog是标准日志流对象,用于向控制台或终端输出日志信息。它也是iostream库的一部分,可以通过包含头文件
#include <iostream>
int main() {
int value = 42;
std::clog << "This is a log message: " << value << std::endl;
return 0;
}
2
3
4
5
6
7
使用clog对象输出了一个日志信息,其中包含一个整数值。与cout和cerr不同,clog对象的输出通常会被缓冲,这意味着日志信息可能不会立即显示在控制台或终端上,而是在适当的时机进行刷新。
clog对象通常用于输出一般的日志信息,既不像cout那样用于正常输出,也不像cerr那样用于错误信息。它可以用于记录程序的运行状态、调试信息或其他重要的日志记录。
# 13.1.7 综合案例与思考
#include <iostream>
#include <iomanip>
#include <sstream>
#include <string>
using namespace std;
int main() {
// 1. cout 格式化输出
cout << "=== cout 格式化 ===" << endl;
double pi = 3.14159265358979;
cout << "默认: " << pi << endl;
cout << "精度3: " << fixed << setprecision(3) << pi << endl;
cout << "科学: " << scientific << pi << endl;
cout << "十六进制: " << hex << 255 << " 八进制: " << oct << 255 << dec << endl;
cout << "对齐: [" << setw(15) << left << "左对齐" << "]" << endl;
cout << "对齐: [" << setw(15) << right << "右对齐" << "]" << endl;
// 2. cin 输入处理
cout << "\n=== cin 输入 ===" << endl;
cout << "请输入姓名和年龄: ";
string name;
int age;
// 模拟输入处理
// cin >> name >> age;
// 3. cerr vs clog
cout << "\n=== cerr vs clog ===" << endl;
cerr << "错误: 无缓冲,立即输出" << endl;
clog << "日志: 有缓冲,适合日志记录" << endl;
// 4. stringstream(字符串流)
cout << "\n=== stringstream ===" << endl;
stringstream ss;
ss << "商品:" << "苹果" << " 价格:" << fixed << setprecision(2) << 5.5;
string result = ss.str();
cout << result << endl;
// 字符串解析
string data = "张三 25 90.5";
istringstream iss(data);
string parseName;
int parseAge;
double parseScore;
iss >> parseName >> parseAge >> parseScore;
cout << "解析: " << parseName << " " << parseAge << "岁 " << parseScore << "分" << endl;
return 0;
}
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
案例知识融合:本案例综合了C++ I/O流的核心知识:cout的格式化输出(精度、进制、对齐)、cin的输入处理、cerr(无缓冲错误流)和clog(有缓冲日志流)的区别、以及stringstream的字符串序列化与解析。这些是I/O库中最常用的功能。
思考题:
cerr和clog都输出到标准错误流,它们的主要区别(缓冲)在什么场景下有实际影响?cin >> name遇到空格就停止了,如何读取带空格的整行输入?stringstream在类型转换(如字符串转数字)中非常方便,但C++11后有更好的替代方案吗?
# 13.2 文件操作实践
# 13.2.1 文件是什么
内存中存放的数据在计算机关机后就会消失。要长久保存数据,就要使用硬盘、光盘、U 盘等设备。为了便于数据的管理和检索,引入了“文件”的概念。
成千上万个文件如果不加分类放在一起,用户使用起来显然非常不便,因此又引入了树形目录(目录也叫文件夹)的机制,可以把文件放在不同的文件夹中,文件夹中还可以嵌套文件夹,这就便于用户对文件进行管理和使用。
所谓“格式”,就是关于文件中每一部分的内容代表什么含义的一种约定。 例如,常见的纯文本文件(也叫文本文件,扩展名通常是“.txt”),指的是能够在 Windows 的“记事本”程序中打开,并且能看出是一段有意义的文字的文件。文本文件的格式可以用一句话来描述:文件中的每个字节都是一个可见字符的 ASCII 码。
所谓“文本文件”和“二进制文件”,只是约定俗成的、从计算机用户角度出发进行的分类,并不是计算机科学的分类。因为从计算机科学的角度来看,所有的文件都是由二进制位组成的,都是二进制文件。文本文件和其他二进制文件只是格式不同而已。
# 13.2.2 文件类型分类
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放。
通过文件可以将数据持久化
C++中对文件操作需要包含头文件 ==< fstream >==
文件类型分为两种:
- 文本文件 —— 文件以文本的ASCII码形式存储在计算机中
- 二进制文件 —— 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
# 13.2.3 操作文件类
C++ 标准库中还专门提供了 3 个类用于实现文件操作,它们统称为文件流类,这 3 个类分别为:
- ifstream:专用于从文件中读取数据;
- ofstream:专用于向文件中写入数据;
- fstream:既可用于从文件中读取数据,又可用于向文件中写入数据。
# 13.2.4 文本文件写
写文件步骤如下:
- 包含头文件 :#include
- 创建流对象 :ofstream ofs;
- 打开文件 :ofs.open("文件路径",打开方式);
- 写数据 :ofs << "写入的数据";
- 关闭文件 :ofs.close();
文件打开方式可以配合使用,利用|操作符。文件打开方式:
| 打开方式 | 解释 |
|---|---|
| ios::in | 为读文件而打开文件 |
| ios::out | 为写文件而打开文件 |
| ios::ate | 初始位置:文件尾 |
| ios::app | 追加方式写文件 |
| ios::trunc | 如果文件存在先删除,再创建 |
| ios::binary | 二进制方式 |
例如: 用二进制方式写文件 ios::binary | ios:: out
#include <fstream>
void test1() {
ofstream ofs;
ofs.open("2_4.txt" ,ios::out);
ofs << "姓名:张三" << endl;
ofs << "性别:男" << endl;
ofs << "年龄:18" << endl;
ofs.close();
}
int main() {
test1();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
总结:
- 文件操作必须包含头文件 fstream
- 读文件可以利用 ofstream ,或者fstream类
- 打开文件时候需要指定操作文件的路径,以及打开方式
- 利用<<可以向文件中写数据
- 操作完毕,要关闭文件
# 13.2.5 文本文件读
读文件与写文件步骤相似,但是读取方式相对于比较多。读文件步骤如下:
- 包含头文件 :#include
- 创建流对象 :ifstream ifs;
- 打开文件并判断文件是否打开成功 :ifs.open("文件路径",打开方式);
- 读数据 :四种方式读取
- 关闭文件 :ifs.close();
示例:
#include "fstream"
#include "string"
#include "iostream"
using namespace std;
void test() {
ifstream ifs;
ifs.open("text.txt", ios::in);
if (!ifs.is_open()) {
cout << "文件打开失败" << endl;
return;
}
//第一种方式
char buf[1024] = {0};
while (ifs >> buf) {
cout << buf << endl;
}
//第二种
// char buf[1024] = {0};
// while (ifs.getline(buf, sizeof(buf))) {
// cout << buf << endl;
// }
//第三种
// string buf;
// while (getline(ifs, buf)) {
// cout << buf << endl;
// }
// char c;
// while ((c = ifs.get()) != EOF) {
// cout << c;
// }
ifs.close();
}
int main() {
test();
return 0;
}
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
总结:
- 读文件可以利用 ifstream ,或者fstream类
- 利用is_open函数可以判断文件是否打开成功
- close 关闭文件
# 13.2.6 文本文件读和写
fstream 是 C++ 标准库中用于文件输入输出的类,它结合了 ifstream(输入文件流)和 ofstream(输出文件流)的功能,既可以读取文件,也可以写入文件。
#include <iostream>
#include <fstream>
#include <string>
int main() {
std::fstream file("example.txt", std::ios::in | std::ios::out | std::ios::app);
if (!file.is_open()) {
std::cerr << "Failed to open file!" << std::endl;
return 1;
}
// 写入数据
file << "Appending a new line to the file." << std::endl;
// 将文件指针移动到文件开头
file.seekg(0, std::ios::beg);
// 读取文件内容
std::string line;
while (std::getline(file, line)) {
std::cout << line << std::endl;
}
file.close();
return 0;
}
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
# 13.2.7 二进制文件写
以二进制的方式对文件进行读写操作。打开方式要指定为 ==ios::binary==
二进制方式写文件主要利用流对象调用成员函数write
函数原型 :ostream& write(const char * buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
#include "fstream"
#include "string"
using namespace std;
class Person {
public:
char name[64];
int age;
};
void test() {
//1、包含头文件
//2、创建输出流对象
ofstream ofs("person.txt", ios::out | ios::binary);
//3、打开文件
//ofs.open("person.txt", ios::out | ios::binary);
Person p = {"张三" , 18};
//4、写文件
ofs.write((const char *)&p, sizeof(p));
//5、关闭文件
ofs.close();
}
int main() {
test();
return 0;
}
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
总结: 文件输出流对象 可以通过write函数,以二进制方式写数据
# 13.2.8 二进制文件读
二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char *buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
示例:
#include "fstream"
#include "string"
#include "iostream"
using namespace std;
class Person {
public:
char name[64];
int age;
};
void test() {
//2、创建输入流对象
ifstream ifs("person.txt", ios::in | ios::binary);
if (!ifs.is_open()) {
cout << "文件打开失败" << endl;
return;
}
//3、打开文件
//ofs.open("person.txt", ios::in | ios::binary);
Person p;
//4、读文件
ifs.read((char *)&p, sizeof(p));
//5、关闭文件
ifs.close();
cout << "姓名: " << p.name << " 年龄: " << p.age << endl;
}
int main() {
test();
return 0;
}
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
文件输入流对象 可以通过read函数,以二进制方式读数据
# 13.2.9 综合案例与思考
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
struct StudentRecord {
char name[32];
int age;
double score;
};
int main() {
// 1. 文本文件写入
cout << "=== 文本写入 ===" << endl;
{
ofstream ofs("students.txt", ios::out);
if (!ofs) { cerr << "无法创建文件" << endl; return 1; }
ofs << "姓名 年龄 成绩" << endl;
ofs << "张三 20 92.5" << endl;
ofs << "李四 21 88.0" << endl;
ofs << "王五 19 95.5" << endl;
ofs.close();
cout << "文本文件写入完成" << endl;
}
// 2. 文本文件读取
cout << "\n=== 文本读取 ===" << endl;
{
ifstream ifs("students.txt", ios::in);
if (!ifs.is_open()) { cerr << "文件打开失败" << endl; return 1; }
string line;
while (getline(ifs, line)) {
cout << " " << line << endl;
}
ifs.close();
}
// 3. 二进制写入
cout << "\n=== 二进制写入 ===" << endl;
{
StudentRecord records[] = {
{"张三", 20, 92.5},
{"李四", 21, 88.0}
};
ofstream ofs("students.bin", ios::out | ios::binary);
ofs.write(reinterpret_cast<const char*>(records), sizeof(records));
ofs.close();
cout << "二进制写入完成,大小: " << sizeof(records) << "字节" << endl;
}
// 4. 二进制读取
cout << "\n=== 二进制读取 ===" << endl;
{
ifstream ifs("students.bin", ios::in | ios::binary);
StudentRecord r;
while (ifs.read(reinterpret_cast<char*>(&r), sizeof(r))) {
cout << " " << r.name << " " << r.age << "岁 " << r.score << "分" << endl;
}
ifs.close();
}
// 5. 追加写入
cout << "\n=== 追加写入 ===" << endl;
{
ofstream ofs("students.txt", ios::app);
ofs << "赵六 22 91.0" << endl;
ofs.close();
cout << "追加成功" << endl;
}
return 0;
}
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
64
65
66
67
68
69
70
71
72
案例知识融合:本案例覆盖了文件操作的完整流程:文本文件的写入(ofstream+ios::out)和读取(ifstream+getline)、二进制文件的写入(write)和读取(read)、文件追加(ios::app)。二进制模式直接读写内存数据,效率更高但不可读;文本模式人可读但需要解析。
思考题:
- 二进制写入的文件在不同平台(Windows/Mac/Linux)间传输可能有什么兼容性问题?(提示:字节序、结构体对齐)
- 如果文件打开失败,程序应该如何优雅地处理?除了检查
is_open(),还有什么方式? ios::out和ios::trunc的关系是什么?直接用ios::out打开已有文件会发生什么?
# 13.3 文件读写指针
# 13.3.1 文件错误与状态
std::fstream类中定义了一些成员函数和状态标志,用于检测和处理文件错误和状态。以下是一些常用的状态标志和相关的成员函数:
fail():检测文件操作是否失败。返回true表示失败,返回false表示成功。 bad():检测文件流是否处于错误状态。返回true表示错误,返回false表示正常。 eof():检测文件流是否到达文件末尾。返回true表示到达末尾,返回false表示未到达末尾。 good():检测文件流是否处于正常状态。返回true表示正常,返回false表示出现错误。 clear():清除文件流的错误状态标志。 rdstate():返回当前文件流的状态标志。 setstate():设置文件流的状态标志。
通过使用这些成员函数和状态标志,可以检测和处理文件操作中的错误和状态。例如,可以使用fail()函数来检测文件读取操作是否失败,然后使用clear()函数来清除错误状态标志。
void test() {
cout << "文件错误与状态" << endl;
std::ifstream file("example.txt");
//std::ifstream file("yc.txt");
if (!file) {
//我们检测文件是否成功打开,如果打开失败,则输出错误信息并返回。
std::cerr << "Failed to open the file." << std::endl;
return;
}
int num;
//然后,我们使用while循环从文件中读取整数,并检测读取操作是否失败。如果失败,则输出错误信息并清除错误状态标志。
while (file >> num) {
if (file.fail()) {
std::cerr << "Error reading the file." << std::endl;
file.clear(); // 清除错误状态标志
break;
}
std::cout << num << " ";
}
if (file.eof()) {
//最后,我们检测是否到达文件末尾,并关闭文件。
std::cout << std::endl << "End of file reached." << std::endl;
}
file.close();
std::cerr << "file to close" << std::endl;
//在上述示例中,我们打开了一个名为example.txt的文件,并使用std::ifstream对象file进行读取操作。
}
int main() {
test();
return 0;
}
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
# 13.3.2 文件的追加
在C++中,可以使用文件流对象的std::ofstream类来实现文件的追加操作。可以创建新文件或打开已存在的文件进行写入。
要实现文件的追加,需要在打开文件时指定追加模式。可以使用std::ios::app标志来指定追加模式,它会将数据追加到文件的末尾而不是覆盖原有内容。
void test() {
cout << "文件的追加" << endl;
std::ofstream file("yc.txt" , std::ios::app);
if (!file) {
std::cerr << "Failed to open the file." << std::endl;
return;
}
file << "This is a new line." << std::endl;
file << "This is another line." << std::endl;
file.close();
std::ifstream inFile("yc.txt" , std::ios::binary | std::ios::in);
if (inFile.is_open()) {
int data[5];
inFile.read(reinterpret_cast<char*>(data), sizeof(data));
for (int i = 0; i < 5; i++) {
std::cout << data[i] << " " <<endl;
}
inFile.close();
std::cout << std::endl << "Binary file read successfully." << std::endl;
} else {
std::cout << "Failed to open the file for reading." << std::endl;
}
}
int main() {
test();
return 0;
}
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
# 13.3.3 文件结尾的判断
在C++中,可以使用文件流对象的eof()函数来判断文件是否已经到达结尾。eof()函数是std::istream和std::ostream类的成员函数,用于检测文件流是否已经到达文件末尾。
eof()函数返回一个bool值,如果文件流已经到达文件末尾,则返回true,否则返回false。
以下是一个示例,演示了如何使用eof()函数来判断文件是否已经到达结尾:
void test() {
cout << "文件结尾的判断" << endl;
std::ifstream file("yc.txt");
if (!file) {
//首先,我们检测文件是否成功打开,如果打开失败,则输出错误信息并返回。
std::cerr << "Failed to open the file." << std::endl;
return;
}
std::string line;
//使用std::getline()函数从文件中逐行读取数据,并将每行数据输出到控制台。
while (std::getline(file,line)) {
std::cout << line << std::endl;
}
if (file.eof()) {
//使用eof()函数检测文件是否已经到达结尾。如果到达结尾,则输出相应的提示信息。
std::cout << "End of file reached." << std::endl;
}
file.close();
}
int main() {
test();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 13.3.4 在指定位置读/写文件
在C++中,可以使用文件流对象的seekg()和seekp()函数来在指定位置进行文件的读取和写入操作。这两个函数用于设置文件流的读取和写入位置。
seekg()函数用于设置输入文件流的读取位置,而seekp()函数用于设置输出文件流的写入位置。这两个函数都接受一个参数,表示要设置的位置。
以下是一个示例,演示了如何在指定位置读取和写入文件:
void test() {
cout << "在指定位置读/写文件" << endl;
std::fstream file("yc.txt",std::ios::in | std::ios::out);
if (!file) {
std::cerr << "Failed to open the file." << std::endl;
}
cout << "修改前的文件内容" << endl;
string buf;
while (getline(file, buf)) {
cout << buf << endl;
}
// 在指定位置读取文件
file.seekg(5, std::ios::beg); // 从文件开头偏移5个字节
char ch;
file >> ch;
std::cout << "Character at position 5: " << ch << std::endl;
// 在指定位置写入文件
file.seekp(10, std::ios::beg); // 从文件开头偏移10个字节
file << "XYZ";
file.close();
}
int main() {
test();
return 0;
}
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
# 13.3.5 综合案例与思考
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
// 创建测试文件
{
ofstream ofs("seektest.txt");
ofs << "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
ofs.close();
}
fstream file("seektest.txt", ios::in | ios::out);
if (!file) { cerr << "打开失败" << endl; return 1; }
// 1. seekg:设置读位置
cout << "=== seekg 读取位置 ===" << endl;
file.seekg(10, ios::beg); // 从头偏移10
char ch;
file.get(ch);
cout << "位置10的字符: " << ch << endl; // K
file.seekg(-5, ios::end); // 从尾往前5
file.get(ch);
cout << "倒数第5个: " << ch << endl; // V
// 2. tellg:获取当前读位置
cout << "\n当前读位置: " << file.tellg() << endl;
// 3. seekp:设置写位置
cout << "\n=== seekp 写入位置 ===" << endl;
file.seekp(0, ios::beg); // 回到开头
file << "abc"; // 覆盖前3个字符
// 4. 验证修改
file.seekg(0, ios::beg);
string content;
getline(file, content);
cout << "修改后: " << content << endl; // abcDEFGHI...
// 5. 判断文件结尾
file.clear(); // 清除 eof 标志
file.seekg(0, ios::beg);
int charCount = 0;
while (file.get(ch)) charCount++;
cout << "\n文件总字符数: " << charCount << endl;
cout << "是否到达末尾: " << (file.eof() ? "是" : "否") << endl;
file.close();
return 0;
}
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
案例知识融合:本案例综合了文件读写指针的操作:seekg设置读位置、seekp设置写位置、tellg获取当前位置、不同基准点(ios::beg/ios::end/ios::cur)的使用,以及eof判断文件结尾。特别注意读完文件后需要 clear() 清除eof标志才能继续操作。
思考题:
seekg和seekp分别控制读指针和写指针,在fstream中这两个指针是独立的还是共享的?- 用
while(!file.eof())读文件有什么陷阱?为什么推荐while(file >> data)或while(getline(file, line))? - 如果要在文件的中间插入内容(而非覆盖),标准文件流能直接做到吗?需要怎样的策略?
# 13.4 错误处理
文件操作时,需要检查文件是否成功打开或读取。
# 13.4.1 检查文件状态
#include <iostream>
#include <fstream>
int main() {
std::ifstream inFile("nonexistent.txt");
if (!inFile) {
std::cerr << "Failed to open file!\n";
return 1;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
# 13.4.2 异常处理
可以通过 exceptions() 方法启用文件流的异常处理。
#include <iostream>
#include <fstream>
int main() {
std::ifstream inFile;
inFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try {
inFile.open("nonexistent.txt");
} catch (const std::ifstream::failure &e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
# 13.4.3 综合案例与思考
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
void robustFileRead(const string& filename) {
ifstream file;
// 方式1:手动检查状态
file.open(filename);
if (!file) {
cerr << "[手动] 无法打开: " << filename << endl;
return;
}
string line;
while (getline(file, line)) {
if (file.bad()) {
cerr << "严重IO错误!" << endl;
break;
}
cout << line << endl;
}
// 检查结束原因
if (file.eof()) cout << "-- 正常读完 --" << endl;
else if (file.fail()) cout << "-- 读取失败 --" << endl;
file.close();
}
void exceptionFileRead(const string& filename) {
ifstream file;
// 方式2:启用异常
file.exceptions(ifstream::failbit | ifstream::badbit);
try {
file.open(filename);
string line;
// 注意:eof 不会触发异常,所以先关闭 failbit
file.exceptions(ifstream::badbit);
while (getline(file, line)) {
cout << line << endl;
}
cout << "-- 正常读完 --" << endl;
} catch (const ifstream::failure& e) {
cerr << "[异常] " << e.what() << endl;
}
}
int main() {
cout << "=== 手动状态检查 ===" << endl;
robustFileRead("不存在的文件.txt");
cout << "\n=== 异常处理方式 ===" << endl;
exceptionFileRead("不存在的文件.txt");
return 0;
}
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
案例知识融合:本案例对比了文件错误处理的两种方式:手动状态检查(fail/bad/eof/good)和异常机制(exceptions+try-catch)。手动方式更灵活,异常方式代码更简洁但需注意eof也会触发failbit。实际开发中两种方式各有场景。
思考题:
fail()和bad()有什么区别?什么错误会导致bad()为 true?- 为什么
exceptions设置了failbit后,正常读到文件末尾也会抛异常? - RAII 思想如何应用于文件操作?
ifstream析构时会自动关闭文件吗?
# 13.5 IO综合案例
# 13.5.1 键盘输入写文本文件
从控制面板输入信息,把这些内容写到指定的文本文件。第一步:获取键盘输入内容;第二步:将内容写入到对应文本文件。
int main() {
ofstream ofs;
ofs.open("yc.txt",ios::out);
//第一步,从键盘获取数据
//string data;
char data[64];
cout << "Writing to the file" << endl;
cout << "Enter your name: ";
//键盘输入数据
cin.getline(data, 100);
ofs << data << endl;
cout << "Enter your age: ";
cin >> data;
cin.ignore();
// 再次向文件写入用户输入的数据
ofs << data << endl;
cout << "Enter your book: ";
cin >> data;
cin.ignore();
ofs << data << endl;
ofs.close();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 13.5.2 实现文件复制
需求:读取一个文件的内容,然后复制到另一个文件中。
void test() {
//使用std::ifstream类打开源文件,并使用std::ofstream类创建目标文件。
//我们使用std::ios::binary标志来以二进制模式打开文件,以确保正确复制文件的内容。
std::ifstream source_file("yc.txt",std::ios::binary);
std::ofstream destination_file("ycdoubi.txt",std::ios::binary);
if (!source_file) {
std::cout << "无法打开源文件" << std::endl;
return;
}
if (!destination_file) {
std::cout << "无法创建目标文件" << std::endl;
return;
}
char ch;
//使用get函数从源文件逐个字节读取内容,并使用put函数将字节写入目标文件,从而实现文件的复制。这个过程会一直进行,直到源文件的末尾。
while (source_file.get(ch)) {
destination_file.put(ch);
}
//请注意,这个示例是逐个字节复制文件的简单实现。对于大型文件,逐个字节的复制可能效率较低。
//在实际应用中,您可以使用更高效的方法,如缓冲区复制或使用std::copy函数。
source_file.close();
destination_file.close();
std::cout << "文件复制完成" << std::endl;
}
int main() {
test();
return 0;
}
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
# 13.5.3 合并两个文件信息
# 13.5.4 文件加密
# 13.5.5 在文件中查找关键词
# 13.6 IO流底层原理
# 13.6.1 流缓冲区机制
C++ IO流使用缓冲区(streambuf)减少系统调用次数。
当你写cout << "Hello"时,数据并不会立刻到达屏幕,而是先写入一个内存缓冲区。缓冲区满了或遇到刷新条件时,才真正调用系统调用输出。
三种缓冲模式:
| 模式 | 说明 | 典型对象 |
|---|---|---|
| 全缓冲 | 缓冲区满才刷新 | 文件流ofstream |
| 行缓冲 | 遇到换行符\n刷新 | cout(连接终端时) |
| 无缓冲 | 每次输出都立刻刷新 | cerr |
缓冲区的底层:
// cout的缓冲区工作流程(简化)
cout << "Hello";
// 1. 数据写入streambuf内部的char数组(通常8KB)
// 2. 遇到以下条件之一时刷新:
// a. 缓冲区满
// b. 遇到endl(endl = '\n' + flush)
// c. 手动调用flush()
// d. 程序正常结束
// e. cin和cout绑定,cin读取前自动flush cout
// 3. 刷新时调用write()系统调用,数据到达内核缓冲区
// 4. 内核将数据发送到终端/文件
2
3
4
5
6
7
8
9
10
11
endl vs '\n'的性能差异:
// 慢:每次都刷新缓冲区
for (int i = 0; i < 10000; ++i)
cout << i << endl; // 10000次write()系统调用
// 快:只在缓冲区满时刷新
for (int i = 0; i < 10000; ++i)
cout << i << '\n'; // 可能只有几十次write()系统调用
2
3
4
5
6
7
高性能IO技巧:
// 1. 关闭同步(不与C的stdio混用时)
ios::sync_with_stdio(false);
cin.tie(nullptr); // 解除cin和cout的绑定
// 2. 用'\n'代替endl
// 3. 文件IO时用大缓冲区
char buf[1 << 16]; // 64KB
ofstream fout("data.txt");
fout.rdbuf()->pubsetbuf(buf, sizeof(buf));
2
3
4
5
6
7
8
9
# 13.6.2 文件系统调用
C++文件操作最终调用操作系统API:
C++ 层: ofstream.write() → streambuf → overflow()
C 层: fwrite() → FILE缓冲区 → fflush()
系统调用层:write(fd, buf, count) → 内核
内核层: 页缓存(Page Cache) → 磁盘驱动 → 物理写入
2
3
4
open()系统调用返回一个文件描述符(fd),它是一个整数索引。所有后续操作(read/write/close)都通过这个fd进行。
文件定位的底层:seekg/seekp最终调用lseek()系统调用,它修改内核为该fd维护的文件偏移量。下次read/write就从新位置开始。
# 13.6.3 格式化输出的底层
cout << 42实际发生了什么:
- 编译器选择
operator<<(ostream&, int)重载 - 将整数42转换为字符串
"42"(itoa算法) - 将字符
'4'和'2'写入streambuf - 如果需要刷新,调用write()系统调用
printf vs cout性能:
printf使用格式化字符串,运行时解析%d等格式符cout使用运算符重载,编译期确定类型,但链式调用产生多次虚函数调用(streambuf::overflow是虚函数)- 在大量IO场景下,
printf通常比cout快10-30%(sync_with_stdio关闭后差距缩小)
# 13.7 IO流训练题
训练题1:实现CSV文件读写器
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
using namespace std;
class CsvWriter {
ofstream file_;
char delimiter_;
public:
CsvWriter(const string& path, char delim = ',')
: file_(path), delimiter_(delim) {
if (!file_) throw runtime_error("无法创建文件: " + path);
}
void writeRow(const vector<string>& row) {
for (size_t i = 0; i < row.size(); ++i) {
if (i > 0) file_ << delimiter_;
// 如果包含分隔符或引号,用引号包裹
if (row[i].find(delimiter_) != string::npos || row[i].find('"') != string::npos) {
file_ << '"';
for (char c : row[i]) {
if (c == '"') file_ << '"'; // 转义引号
file_ << c;
}
file_ << '"';
} else {
file_ << row[i];
}
}
file_ << '\n';
}
};
class CsvReader {
ifstream file_;
char delimiter_;
public:
CsvReader(const string& path, char delim = ',')
: file_(path), delimiter_(delim) {
if (!file_) throw runtime_error("无法打开文件: " + path);
}
vector<vector<string>> readAll() {
vector<vector<string>> data;
string line;
while (getline(file_, line)) {
vector<string> row;
stringstream ss(line);
string cell;
while (getline(ss, cell, delimiter_)) {
row.push_back(cell);
}
data.push_back(row);
}
return data;
}
};
int main() {
// 写CSV
CsvWriter writer("test.csv");
writer.writeRow({"姓名", "年龄", "城市"});
writer.writeRow({"张三", "25", "北京"});
writer.writeRow({"李四", "30", "上海"});
writer.writeRow({"王五", "28", "深圳"});
cout << "CSV写入完成" << endl;
// 读CSV
CsvReader reader("test.csv");
auto data = reader.readAll();
for (const auto& row : data) {
for (const auto& cell : row) cout << cell << "\t";
cout << endl;
}
return 0;
}
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
练习重点:ofstream/ifstream的RAII管理、stringstream解析、文件异常处理。
训练题2:高性能文件复制
#include <iostream>
#include <fstream>
#include <chrono>
using namespace std;
void copyFile(const string& src, const string& dst, size_t bufSize = 8192) {
ifstream in(src, ios::binary);
ofstream out(dst, ios::binary);
if (!in) throw runtime_error("无法打开: " + src);
if (!out) throw runtime_error("无法创建: " + dst);
// 设置自定义缓冲区
vector<char> buf(bufSize);
while (in.read(buf.data(), bufSize) || in.gcount() > 0) {
out.write(buf.data(), in.gcount());
}
}
int main() {
// 创建测试文件
{
ofstream f("big_file.dat", ios::binary);
vector<char> data(1024 * 1024, 'A'); // 1MB
for (int i = 0; i < 10; ++i) f.write(data.data(), data.size());
}
cout << "测试文件已创建(10MB)" << endl;
// 不同缓冲区大小对比
for (size_t bs : {1024, 8192, 65536, 1048576}) {
auto start = chrono::high_resolution_clock::now();
copyFile("big_file.dat", "copy.dat", bs);
auto end = chrono::high_resolution_clock::now();
auto ms = chrono::duration_cast<chrono::milliseconds>(end - start).count();
cout << "缓冲区" << bs << "字节: " << ms << "ms" << endl;
}
return 0;
}
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
练习重点:二进制文件读写、缓冲区大小对性能的影响、gcount()处理最后不满的块。
# 13.8 综合思考题
为什么要关闭sync_with_stdio:
ios::sync_with_stdio(false)关闭了C++ IO流与C stdio的同步。同步保证了printf和cout交替使用时输出顺序正确。关闭后cout性能提升数倍,但混用printf和cout会导致输出乱序。请分析同步的底层实现原理。mmap文件IO:传统read/write需要数据在内核缓冲区和用户缓冲区之间拷贝。
mmap()将文件直接映射到进程地址空间,读写文件如同访问数组。请思考mmap的优缺点,以及它为什么在大文件随机访问场景下更有优势。零拷贝技术:
sendfile()系统调用可以将文件数据直接从内核缓冲区发送到网络套接字,不需要拷贝到用户空间。请思考为什么传统的"read+write"需要4次上下文切换和2次数据拷贝,而sendfile只需要2次上下文切换和0次拷贝。
# 11.X 卷一改造增补:现代 IO —— <filesystem> / std::format / std::print
本节为卷一新增。新代码处理路径、格式化输出,请放弃
printf与字符串拼接,全面拥抱标准库新工具。
# 11.X.1 std::filesystem(C++17):跨平台路径操作
#include <filesystem>
namespace fs = std::filesystem;
fs::path p = "data/log.txt";
if (fs::exists(p) && fs::is_regular_file(p)) {
auto sz = fs::file_size(p);
std::cout << p.stem() << " size=" << sz;
}
// 遍历目录(递归)
for (auto& entry : fs::recursive_directory_iterator("/var/log")) {
if (entry.is_regular_file() && entry.path().extension() == ".log") {
// process(entry.path());
}
}
// 创建目录、复制、删除
fs::create_directories("a/b/c");
fs::copy("src.txt", "dst.txt", fs::copy_options::overwrite_existing);
fs::remove_all("a");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
告别:Windows 用 _mkdir、Linux 用 mkdir(... mode) 的平台分裂代码。
# 11.X.2 std::format(C++20):类型安全的格式化
#include <format>
std::string s = std::format("user={} age={:>3} pi={:.2f}", "alice", 25, 3.14159);
// 输出 "user=alice age= 25 pi=3.14"
2
3
相比 printf:
- 类型安全(编译期检查格式串)
- 支持自定义类型(特化
std::formatter<T>) - 不会因
%d误用long而 UB
# 11.X.3 std::print(C++23):直接打印
#include <print>
std::print("hello {}\n", name); // 比 cout 快、比 printf 安全
std::println("count = {}", n);
2
3
# 11.X.4 文件打开的 RAII 习惯
{
std::ifstream f("a.txt");
if (!f) { /* 处理错误 */ }
std::string line;
while (std::getline(f, line)) { /* ... */ }
} // f 离开作用域自动 close —— 不需要手动 close()
2
3
4
5
6
# 11.X.5 推荐阅读
- 卷一
18.特性图谱§C++17 / C++20 / C++23 - 卷一第 12 章
12.动态内存.md§12.11 RAII —— 文件流的 RAII 本质 - cppreference:
<filesystem>(opens new window)
# 11.Y 新手陷阱 Top 5
| # | 陷阱 | 说明 |
|---|---|---|
| 1 | 不检查 open() 是否成功 | 后续读写默默失败;必须 if (!f) return; |
| 2 | 文本/二进制模式混用 | Windows 文本模式会处理 \r\n,二进制必须 std::ios::binary |
| 3 | >> 跳过空白后读字符串 | 只读到第一个空白;用 std::getline |
| 4 | endl 滥用 | 强制 flush,循环输出慢;用 '\n' |
| 5 | 中文路径编码 | Windows 下 wstring 路径或用 <filesystem> |