C++ 通过 <fstream> 头文件提供强大的文件输入/输出功能。它将文件视为一种“流”(stream),允许你像使用 cincout 一样方便地读写文件。

核心头文件:

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
#include <fstream>
````

---

## 1. 核心类 (文件流对象)

在 `<fstream>` 中,你主要会用到三个类,它们是文件操作的“变量”:

- **`std::ifstream`** (Input File Stream)

- **用途:** 用于从文件中**读取**数据。

- **类比:** 就像 `cin`,但数据源是文件。

- **`std::ofstream`** (Output File Stream)

- **用途:** 用于向文件中**写入**数据。

- **类比:** 就像 `cout`,但目的地是文件。

- **`std::fstream`** (File Stream)

- **用途:** 可以同时**读取和写入**同一个文件。

- **类比:** `cin` 和 `cout` 的结合体。


---

## 2. 核心函数与操作

### A. 打开文件

有两种方式打开文件:

**方式一:在构造时打开 (推荐)**
```cpp
// 1. 打开文件用于写入 (默认会清空文件内容)
std::ofstream outFile("my_file.txt");

// 2. 打开文件用于读取
std::ifstream inFile("my_file.txt");

方式二:使用 open() 成员函数

1
2
3
4
5
std::ofstream outFile;
outFile.open("my_file.txt");

std::ifstream inFile;
inFile.open("my_file.txt");

B. 检查文件是否打开成功 (必须)

在读写之前,必须检查文件是否成功打开。如果文件不存在(读取时)或没有权限(写入时),操作会失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
std::ifstream inFile("my_file.txt");

// 方式一:使用 .is_open()
if (!inFile.is_open()) {
std::cerr << "错误:无法打开文件 my_file.txt" << std::endl;
// 在此 return 或 exit
}

// 方式二:直接在 if 语句中检查 (更简洁)
// 文件流对象在布尔上下文中会自动转换为 true (如果成功) 或 false (如果失败)
if (!inFile) {
std::cerr << "错误:无法打开文件 my_file.txt" << std::endl;
}

C. 文件打开模式 (File Modes)

在打开文件时,你可以指定“模式”来控制如何操作文件。模式通过 std::ios 命名空间访问,并使用 | (按位或) 组合。

  • std::ios::in模式(ifstream 默认)。

  • std::ios::out模式(ofstream 默认)。

  • std::ios::trunc截断模式。在打开文件时清空所有内容(ofstream 默认)。

  • std::ios::app追加模式。从文件末尾开始写入,不清空内容。

  • std::ios::binary二进制模式(默认为文本模式)。

常见组合示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 写入文件 (清空原内容) - 默认模式
std::ofstream outFile1("log.txt");
// 等同于: std::ofstream outFile1("log.txt", std::ios::out | std::ios::trunc);

// 2. 向文件末尾追加内容
std::ofstream outFile2("log.txt", std::ios::app);
// 等同于: std::ofstream outFile2("log.txt", std::ios::out | std::ios::app);

// 3. 打开文件用于读和写
std::fstream file("data.dat", std::ios::in | std::ios::out);

// 4. 打开二进制文件用于写入
std::ofstream binFile("data.bin", std::ios::out | std::ios::binary);

D. 关闭文件

1
2
outFile.close();
inFile.close();

注意:ifstreamofstream 对象离开其作用域时(例如函数结束),其析构函数会自动调用 close()。因此,显式调用 close() 不是必须的,但在长函数中或需要立即释放文件句柄时是好习惯。


3. 常见用法 1: 文本文件 I/O (Text I/O)

文本 I/O 用于读写人类可读的字符数据(如 .txt, .csv, .md 文件)。

A. 写入文本文件 (使用 <<)

使用流插入运算符 <<,就像 cout 一样。

实例:

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

int main() {
std::ofstream outFile("config.txt"); // 打开文件 (默认清空)

if (!outFile) {
std::cerr << "无法创建 config.txt" << std::endl;
return 1;
}

// 像 cout 一样写入
outFile << "AppName=SalarySystem" << std::endl;
outFile << "Version=1.0" << std::endl;
int userCount = 100;
outFile << "UserCount=" << userCount << std::endl;

outFile.close(); // 关闭文件
std::cout << "配置文件写入成功。" << std::endl;
return 0;
}

config.txt 的内容:

1
2
3
AppName=SalarySystem
Version=1.0
UserCount=100

B. 读取文本文件 (使用 >>getline)

方式一:使用 >> (按空格/换行分隔)

  • 流提取运算符 >> 会自动以空白字符(空格、制表符、换行符)为分隔符来读取数据。

  • 这正是你项目中 loadFromFile 使用的方法:ifs >> type >> name >> data;

方式二:使用 std::getline() (按行读取)

  • 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
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <fstream>
#include <string>
#include <sstream> // 之后会用到

int main() {
std::ifstream inFile("employees.txt");
if (!inFile) {
std::cerr << "无法打开 employees.txt" << std::endl;
return 1;
}

// 假设 employees.txt 内容如下:
// Manager Alice 4
// Technican Bob 3
// Saleman Charlie 1

std::string type, name;
int rank;

std::cout << "--- 方式一: 使用 >> 读取 ---" << std::endl;
// 最佳实践:使用 (inFile >> var) 作为循环条件
// 当读取失败或到达文件末尾时,循环会自动停止。
while (inFile >> type >> name >> rank) {
std::cout << "类型: " << type << ", 姓名: " << name << ", 级别: " << rank << std::endl;
}

// 陷阱:不要使用 while (!inFile.eof())
// eof() 标志只在 *尝试读取* 文件末尾 *之后* 才会为 true,
// 这会导致循环多执行一次。

inFile.close(); // 关闭并重置

// --- 演示 getline ---
std::cout << "\n--- 方式二: 使用 getline 读取 ---" << std::endl;
std::ifstream inFile2("employees.txt");
std::string line;

// 每次读取一行
while (std::getline(inFile2, line)) {
std::cout << "读取到整行: " << line << std::endl;

// 如果需要解析行内数据,需配合 stringstream
std::stringstream ss(line);
std::string line_type, line_name;
int line_rank;
if (ss >> line_type >> line_name >> line_rank) {
std::cout << " (解析) 类型: " << line_type << ", 姓名: " << line_name << std::endl;
}
}

inFile2.close();
return 0;
}

4. 常见用法 2: 二进制文件 I/O (Binary I/O)

二进制 I/O 用于读写原始的字节数据。它速度更快、文件更小,但内容不是人类可读的。常用于保存 structclass 的原始内存映像。

A. 关键函数 (write / read)

  • write(const char* buffer, streamsize count):将 buffer 指向的内存中的 count 个字节写入文件。
  • read(char* buffer, streamsize count):从文件中读取 count 个字节到 buffer 指向的内存。

注意: 你需要使用 reinterpret_cast 来将你的数据指针转换为 char*

B. 实例:保存和读取结构体

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
#include <iostream>
#include <fstream>
#include <string>

// 假设有一个简单的数据结构 (POD - Plain Old Data)
struct Player {
int id;
char name[50]; // 二进制读写时,用 C 风格字符串更简单
double score;
};

int main() {
Player p1 = {101, "TestUser", 95.5};
Player p2_load; // 用于加载数据

// --- 1. 写入二进制文件 ---
// 必须指定 std::ios::binary
std::ofstream outFile("player.dat", std::ios::binary);
if (!outFile) {
std::cerr << "无法创建 player.dat" << std::endl;
return 1;
}

// 将 p1 对象的内存数据,原封不动地写入文件
outFile.write(reinterpret_cast<const char*>(&p1), sizeof(Player));
outFile.close();
std::cout << "二进制数据写入成功。" << std::endl;

// --- 2. 读取二进制文件 ---
std::ifstream inFile("player.dat", std::ios::binary);
if (!inFile) {
std::cerr << "无法读取 player.dat" << std::endl;
return 1;
}

// 从文件读取数据,原封不动地填充到 p2_load 对象的内存中
inFile.read(reinterpret_cast<char*>(&p2_load), sizeof(Player));
inFile.close();

std::cout << "读取数据成功:" << std::endl;
std::cout << "ID: " << p2_load.id << std::endl;
std::cout << "Name: " << p2_load.name << std::endl;
std::cout << "Score: " << p2_load.score << std::endl;

return 0;
}

5. 重要陷阱与注意事项

⚠️ 陷阱:二进制 I/O 和 std::string / std::vector

不能像上面的 Player 示例一样,直接对包含 std::stringstd::vector 或其他指针成员的类使用 read/write

  • 原因: std::string 对象本身(在栈上)只包含一个指向堆内存的指针和一些大小信息。write 只会保存这个指针地址,而不是它指向的实际字符数据。当你 read 回来时,得到的是一个无效的指针,程序会崩溃。

  • 对你项目的影响:

    你的 Person 类有 std::string name 成员。因此,你不能像下面这样做:

    1
    2
    3
    // 错误!不能对包含 std::string 的类使用二进制 write
    Manager m("Alice");
    outFile.write(reinterpret_cast<const char*>(&m), sizeof(Manager));
  • 结论: 对于你的项目,使用文本 I/O(即 <<>>)是正确且安全的选择。