我们进入下一个核心主题:文件流操作。这是C++流类库中实用性最强的部分之一,用于持久化存储和读取数据。
第四部分:文件流操作
C++通过 ifstream、ofstream 和 fstream 三个类来处理文件。它们分别继承自 istream、ostream 和 iostream,因此你之前学到的所有格式化控制、成员函数等都能直接用于文件流。
5.1 文件流类简介
cpp
#include <fstream> // 必须包含此头文件
std::ifstream; // 只用于输入(读文件)
std::ofstream; // 只用于输出(写文件)
std::fstream; // 既可用于输入也可用于输出(读写文件)
5.2 文件打开模式
打开文件时需要指定模式,这些模式在 ios_base 类中定义为常量,可以用位或操作符 | 组合使用。
| 模式常量 | 含义 | 适用于 |
|---|---|---|
std::ios::in |
为输入(读)而打开 | ifstream, fstream |
std::ios::out |
为输出(写)而打开 | ofstream, fstream |
std::ios::binary |
以二进制模式打开 | 所有文件流 |
std::ios::ate |
打开后定位到文件末尾 | 所有文件流 |
std::ios::app |
所有输出都追加到文件末尾 | ofstream, fstream |
std::ios::trunc |
如果文件存在,先清空它 | ofstream, fstream(当out指定时默认) |
重要说明:
-
默认模式:
-
ifstream默认ios::in -
ofstream默认ios::out | ios::trunc(输出并清空) -
fstream默认ios::in | ios::out
-
-
ios::app和ios::ate的区别:-
app(append):所有写入都强制追加到文件尾,即使移动了文件指针。 -
ate(at end):打开时初始位置在文件尾,但之后可以移动指针到其他地方写。
-
-
ios::trunc:如果与ios::in一起使用,文件会被清空(可能不是你想要的)。
5.3 打开文件的几种方式
方式1:先创建对象,再调用 open()
cpp
std::ofstream outFile;
outFile.open("example.txt", std::ios::out);
if (!outFile) { // 或者 outFile.fail(), !outFile.is_open()
std::cerr << "无法打开文件 example.txt" << std::endl;
return 1;
}
// 使用 outFile...
outFile.close();
方式2:在构造函数中直接打开(更简洁)
cpp
std::ofstream outFile("example.txt", std::ios::out);
if (!outFile.is_open()) { // 显式检查
std::cerr << "无法打开文件 example.txt" << std::endl;
return 1;
}
// 使用 outFile...
// 文件会在 outFile 析构时自动关闭,但显式关闭是好习惯
outFile.close();
方式3:使用 fstream 进行读写
cpp
std::fstream ioFile("data.txt", std::ios::in | std::ios::out);
if (!ioFile) {
// 如果文件不存在,可能需要创建
ioFile.open("data.txt", std::ios::out); // 创建文件
ioFile.close();
ioFile.open("data.txt", std::ios::in | std::ios::out);
}
5.4 文本文件与二进制文件
-
文本文件 :存储的是字符序列,人类可读。使用
<<和>>运算符进行格式化IO。 -
二进制文件 :存储的是字节序列,不可直接阅读。使用
read()和write()成员函数进行原始字节IO。
5.5 文本文件操作示例
示例1:写入文本文件
cpp
#include <iostream>
#include <fstream>
#include <string>
int main() {
std::ofstream outFile("output.txt");
if (!outFile) {
std::cerr << "创建文件失败!" << std::endl;
return 1;
}
// 像使用 cout 一样使用 outFile
outFile << "Hello, File IO!" << std::endl;
outFile << "整数: " << 42 << std::endl;
outFile << "浮点数: " << 3.14159 << std::endl;
std::string name = "Alice";
int age = 25;
outFile << "姓名: " << name << ", 年龄: " << age << std::endl;
outFile.close();
std::cout << "数据已写入 output.txt" << std::endl;
return 0;
}
示例2:读取文本文件(多种方式)
cpp
#include <iostream>
#include <fstream>
#include <string>
int main() {
std::ifstream inFile("output.txt");
if (!inFile) {
std::cerr << "无法打开文件!" << std::endl;
return 1;
}
std::cout << "=== 方式1:逐词读取(使用 >>)===" << std::endl;
std::string word;
inFile.clear(); // 重置状态
inFile.seekg(0); // 移动文件指针到开头
while (inFile >> word) { // >> 会跳过空白字符
std::cout << word << " ";
}
std::cout << std::endl;
std::cout << "\n=== 方式2:逐行读取(推荐)===" << std::endl;
inFile.clear();
inFile.seekg(0);
std::string line;
while (std::getline(inFile, line)) {
std::cout << line << std::endl;
}
std::cout << "\n=== 方式3:逐个字符读取 ===" << std::endl;
inFile.clear();
inFile.seekg(0);
char ch;
while (inFile.get(ch)) {
std::cout << ch;
}
inFile.close();
return 0;
}
示例3:文件拷贝(文本模式)
cpp
#include <iostream>
#include <fstream>
bool copyTextFile(const std::string& source, const std::string& destination) {
std::ifstream src(source, std::ios::in);
if (!src) {
std::cerr << "无法打开源文件: " << source << std::endl;
return false;
}
std::ofstream dst(destination, std::ios::out);
if (!dst) {
std::cerr << "无法创建目标文件: " << destination << std::endl;
src.close();
return false;
}
// 关键:取消跳过空白字符,否则会丢失空格、换行等
src.unsetf(std::ios::skipws);
char ch;
while (src >> ch) { // 现在会读取所有字符,包括空白
dst << ch;
}
src.close();
dst.close();
std::cout << "文件拷贝完成: " << source << " -> " << destination << std::endl;
return true;
}
int main() {
copyTextFile("output.txt", "copy.txt");
return 0;
}
5.6 二进制文件操作
二进制文件使用 read() 和 write() 函数,它们直接操作内存字节。
write() 和 read() 函数原型
cpp
// 写入二进制数据
ostream& write(const char* buffer, streamsize size);
// 读取二进制数据
istream& read(char* buffer, streamsize size);
重要提示:
-
buffer应该是内存块的起始地址 -
size是要读写的字节数 -
通常需要使用
reinterpret_cast<char*>()来转换非字符类型指针
示例:二进制文件读写
cpp
#include <iostream>
#include <fstream>
#include <cstring>
struct Person {
char name[50];
int age;
double salary;
};
int main() {
// 准备数据
Person people[3] = {
{"Alice", 25, 50000.0},
{"Bob", 30, 60000.0},
{"Charlie", 35, 70000.0}
};
// 1. 写入二进制文件
std::ofstream outFile("people.dat", std::ios::binary);
if (!outFile) {
std::cerr << "无法创建二进制文件!" << std::endl;
return 1;
}
for (int i = 0; i < 3; ++i) {
outFile.write(reinterpret_cast<const char*>(&people[i]), sizeof(Person));
}
outFile.close();
// 2. 读取二进制文件
std::ifstream inFile("people.dat", std::ios::binary);
if (!inFile) {
std::cerr << "无法打开二进制文件!" << std::endl;
return 1;
}
Person temp;
std::cout << "从二进制文件读取的数据:" << std::endl;
// 方法1:知道确切记录数
for (int i = 0; i < 3; ++i) {
inFile.read(reinterpret_cast<char*>(&temp), sizeof(Person));
if (inFile.gcount() == sizeof(Person)) { // 确保读取完整
std::cout << "姓名: " << temp.name
<< ", 年龄: " << temp.age
<< ", 薪资: " << temp.salary << std::endl;
}
}
// 方法2:一直读到文件末尾
inFile.clear();
inFile.seekg(0); // 回到文件开头
std::cout << "\n再次读取(使用eof检测):" << std::endl;
while (!inFile.eof()) {
inFile.read(reinterpret_cast<char*>(&temp), sizeof(Person));
if (inFile.gcount() > 0) { // 实际读取到了数据
std::cout << "姓名: " << temp.name
<< ", 年龄: " << temp.age
<< ", 薪资: " << temp.salary << std::endl;
}
}
inFile.close();
return 0;
}
二进制文件操作的注意事项
-
字节对齐(Padding) :结构体可能有内存对齐填充,
sizeof(Person)可能大于各成员大小之和。 -
平台依赖性:二进制数据与平台相关(如字节序、类型大小)。在不同平台间交换二进制文件可能有问题。
-
指针和动态内存:不能直接读写包含指针的结构体,因为指针值在其他程序运行中无意义。需要序列化(serialize)数据。
-
字符串处理 :使用固定大小的字符数组(如
char name[50])而不是std::string,因为std::string包含指针。
5.7 文件随机访问
通过移动文件指针,可以在文件中任意位置读写,特别是对二进制文件。
文件定位函数
cpp
// 获取当前读取位置
streampos tellg();
// 设置读取位置
istream& seekg(streampos pos);
istream& seekg(streamoff off, ios_base::seekdir dir);
// 获取当前写入位置
streampos tellp();
// 设置写入位置
ostream& seekp(streampos pos);
ostream& seekp(streamoff off, ios_base::seekdir dir);
seekdir 枚举:
-
ios::beg:从文件开头开始 -
ios::cur:从当前位置开始 -
ios::end:从文件末尾开始
示例:随机访问二进制文件
cpp
#include <iostream>
#include <fstream>
struct Record {
int id;
char data[100];
};
int main() {
// 创建一些记录
Record records[5];
for (int i = 0; i < 5; ++i) {
records[i].id = i + 1;
sprintf(records[i].data, "这是第%d条记录", i + 1);
}
// 写入文件
std::fstream file("records.dat",
std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc);
for (int i = 0; i < 5; ++i) {
file.write(reinterpret_cast<const char*>(&records[i]), sizeof(Record));
}
// 随机读取:直接读取第3条记录(索引2)
std::cout << "读取第3条记录:" << std::endl;
Record rec;
// 计算位置:每条记录大小 * 索引
file.seekg(2 * sizeof(Record), std::ios::beg);
file.read(reinterpret_cast<char*>(&rec), sizeof(Record));
std::cout << "ID: " << rec.id << ", 数据: " << rec.data << std::endl;
// 修改第2条记录(索引1)
std::cout << "\n修改第2条记录:" << std::endl;
rec.id = 999;
strcpy(rec.data, "这条记录被修改了!");
file.seekp(1 * sizeof(Record), std::ios::beg);
file.write(reinterpret_cast<const char*>(&rec), sizeof(Record));
// 验证修改
file.seekg(1 * sizeof(Record), std::ios::beg);
file.read(reinterpret_cast<char*>(&rec), sizeof(Record));
std::cout << "修改后 - ID: " << rec.id << ", 数据: " << rec.data << std::endl;
// 从文件末尾添加新记录
std::cout << "\n在文件末尾添加新记录:" << std::endl;
rec.id = 1000;
strcpy(rec.data, "这是新添加的记录");
file.seekp(0, std::ios::end); // 移动到文件末尾
file.write(reinterpret_cast<const char*>(&rec), sizeof(Record));
// 获取文件中的记录总数
file.seekg(0, std::ios::end);
streampos fileSize = file.tellg();
int numRecords = fileSize / sizeof(Record);
std::cout << "文件中共有 " << numRecords << " 条记录" << std::endl;
file.close();
return 0;
}
5.8 文件操作的完整模式
在面向对象程序中,文件操作通常与对象的生命周期绑定:
cpp
#include <iostream>
#include <fstream>
#include <vector>
class Student {
private:
int id;
std::string name;
double score;
public:
Student(int i = 0, const std::string& n = "", double s = 0.0)
: id(i), name(n), score(s) {}
// 显示
void display(std::ostream& os = std::cout) const {
os << "学号: " << id << ", 姓名: " << name << ", 分数: " << score;
}
// 重载输出运算符(用于文本文件)
friend std::ostream& operator<<(std::ostream& os, const Student& s) {
os << s.id << " " << s.name << " " << s.score;
return os;
}
// 重载输入运算符(用于文本文件)
friend std::istream& operator>>(std::istream& is, Student& s) {
is >> s.id >> s.name >> s.score;
return is;
}
// 二进制保存
void saveBinary(std::ofstream& ofs) const {
// 保存id
ofs.write(reinterpret_cast<const char*>(&id), sizeof(id));
// 保存字符串:先保存长度,再保存内容
size_t len = name.length();
ofs.write(reinterpret_cast<const char*>(&len), sizeof(len));
ofs.write(name.c_str(), len);
// 保存分数
ofs.write(reinterpret_cast<const char*>(&score), sizeof(score));
}
// 二进制加载
void loadBinary(std::ifstream& ifs) {
// 读取id
ifs.read(reinterpret_cast<char*>(&id), sizeof(id));
// 读取字符串
size_t len;
ifs.read(reinterpret_cast<char*>(&len), sizeof(len));
char* buffer = new char[len + 1];
ifs.read(buffer, len);
buffer[len] = '\0';
name = buffer;
delete[] buffer;
// 读取分数
ifs.read(reinterpret_cast<char*>(&score), sizeof(score));
}
};
class StudentManager {
private:
std::vector<Student> students;
std::string filename;
public:
StudentManager(const std::string& fname) : filename(fname) {}
// 从文件加载学生(构造函数中)
void loadFromFile() {
std::ifstream inFile(filename);
if (!inFile) {
std::cout << "文件不存在,将创建新文件。" << std::endl;
return;
}
Student s;
while (inFile >> s) {
students.push_back(s);
}
inFile.close();
std::cout << "从文件加载了 " << students.size() << " 名学生" << std::endl;
}
// 保存学生到文件(析构函数中)
void saveToFile() {
std::ofstream outFile(filename);
if (!outFile) {
std::cerr << "无法保存到文件!" << std::endl;
return;
}
for (const auto& s : students) {
outFile << s << std::endl;
}
outFile.close();
std::cout << "已保存 " << students.size() << " 名学生到文件" << std::endl;
}
// 其他管理函数...
void addStudent(const Student& s) {
students.push_back(s);
}
void displayAll() const {
for (const auto& s : students) {
s.display();
std::cout << std::endl;
}
}
};
int main() {
StudentManager manager("students.txt");
// 加载已有数据
manager.loadFromFile();
// 添加新学生
manager.addStudent(Student(1001, "张三", 85.5));
manager.addStudent(Student(1002, "李四", 92.0));
manager.addStudent(Student(1003, "王五", 78.5));
// 显示所有学生
std::cout << "\n所有学生信息:" << std::endl;
manager.displayAll();
// 保存到文件(在实际应用中,这可能在析构函数中自动调用)
manager.saveToFile();
return 0;
}
这部分的核心总结:
-
三个文件流类 :
ifstream(读)、ofstream(写)、fstream(读写)。 -
文件打开模式 :理解
in、out、app、ate、trunc、binary的含义和组合。 -
文本文件操作 :像使用
cin/cout一样使用文件流对象,注意unsetf(ios::skipws)在完整拷贝时的重要性。 -
二进制文件操作 :使用
read()/write()函数,注意类型转换和平台依赖性。 -
随机访问 :通过
seekg()/seekp()和tellg()/tellp()在文件中定位,特别适用于数据库风格的记录存取。 -
与对象结合:在面向对象设计中,通常在构造函数中加载数据,在析构函数中保存数据。