针对初学者的C和C++23的对比学习-14.文件和流
14.文件和流
- 学习目标:
- 学习内容:
- 14.1 文件和流的基本概念
- 14.2 文件类型
- 14.3 计算机中的流
- 14.4 C++输入/输出类层次结构
- 14.5 标准I/O流详解
- 14.6 文件流与对象I/O
- 14.6.12 对象序列化与反序列化
- 14.7 C和C++23中的文件操作对比
- 14.8 RAII原则在文件操作中的应用
学习目标:
掌握C和C++23中文件和流操作的核心概念与实际应用
- 理解文件和流的基本概念及其重要性
- 掌握文件标识符的组成和作用
- 区分文本文件和二进制文件的特点与选择
- 了解C++输入/输出类层次结构
- 掌握文件操作的基本方法和最佳实践
- 学会在C和C++23中进行文件读写操作
学习内容:
14.1 文件和流的基本概念
14.1.1 为什么需要文件操作?
程序数据的持久化存储需求
在之前的学习中,我们使用的输入输出语句都是从键盘读取数据,然后在屏幕上显示数据。
- 程序数据存储在计算机内存中
- 当程序终止时,内存中的数据就会丢失,这样,每次运行程序时都要重新输入这些数据
文件流输入和输出 使用外部存储设备,例如,用硬盘和USB盘来存储数据。
- 这些都是具有持久性的存储设备,保存的数据在程序终止后不会丢失
14.1.2 计算机中文件的定义
文件是存储在计算机外部媒体上的数据集合
- 计算机中的文件是以硬盘等外部设备为载体,存储在计算机中的数据的集合
- 文本文档、图片、程序、音频等都是文件
14.1.3 文件标识符 (File Identifier)
文件标识符的作用是在计算机中唯一标识一个文件
文件标识符的组成:文件路径、文件名主干、文件扩展名
D:\RedPanda-CPP\cpp_std.cpp
|-----------| |-------| |--|
路径 文件名主干 扩展名
- 操作系统以文件为单位对数据进行管理
14.2 文件类型
14.2.1 文件分类
根据不同编码方式的不同,计算机中的文件分为:
文本文件 (Text File)
- 以"行"为基本结构组织和存储数据
- 常用编码包括 ASCII、UNICODE、UTF-8、GBK 等
- 文本文件:以字符或字符串为单位,常用编码有ASCII码、UNICODE码、UTF-8、GBK等
| 编码方式 | 语言 | 字符数 | 字节数 |
|---|---|---|---|
| ASCII | 英文 | 1 | 1 |
| UTF-8 | 中文 | 1 | 3 |
| 英文 | 1 | 1 | |
| Unicode | 中文 | 1 | 2 |
| 英文 | 1 | 2 | |
| GBK | 中文 | 1 | 2 |
| 英文 | 1 | 1 |
二进制文件 (Binary File)
- 以二进制形式编码和存储在计算机中
- 一般是可执行文件、图像、声音、视频等
- 二进制文件:以二进制形式编码存储在计算机中,一般是可执行文件、图像、声音、视频等
14.2.2 文件存储的重要说明
注意:
- 计算机在物理层以二进制形式存储数据
- 文本文件与二进制文件的区别不在于物理上的存储方式,而在逻辑上数据的组织方式
- 两种文件的区别在于,它们存储数值类型(整型、浮点型等)数据的方式不同
存储方式对比
在二进制文件中,数值类型数据是按二进制形式存储的;而在文本文件中,数值类型数据是按照ASCII字符形式存储的。
例如,如果有:
short n{123};
| 字符 | ‘1’ | ‘2’ | ‘3’ |
|---|---|---|---|
| ASCII值(十进制) | 49 | 50 | 51 |
| ASCII值(二进制) | 00110001 | 00110010 | 00110011 |
- 变量n在内存中占两个字节,若将n存储到文本文件中,则需要3个字节的存储空间
- 在文本文件中,数值的每一位数字都需要一个字节的存储空间
在二进制文件中,数值的每一位并不占据独立的存储空间,数值当做一个整体来存储。
所以变量n,其值为123的变量n将存储为图中所示的形式。
00000000 01111011
14.2.3 如何选择文件类型?
两种类型的文件都有其优势
文本文件的优势
- 文本文件容易在屏幕上使用简单系统工具显示,如Windows的type命令,或者Linux的cat命令,因此可以很方便地检查程序创建的这类文件
- 文本文件的另一个优点是,它们可以很方便地被其他程序读取,包括文本编辑器、Word文档处理器、电子制表软件、数据库系统等
文本文件的缺点
- 使用文本文件的一个缺点是,数字数据必须从ASCII码转换成二进制当它从文本文件中读取时必须从ASCII码转换成二进制,写入文本文件时必须从二进制转换成ASCII码
二进制文件的优势
- 另一方面,一个二进制文件不需要任何类型的转换,并且比一个含有相同数字数据的文本文件在磁盘上占用更少的空间
二进制文件的缺点
- 但是,尽管二进制文件比文本文件更有效,但是二进制文件真其他非C语言程序中不容易读取
14.3 计算机中的流
14.3.1 流的概念
流是在不同输入/输出设备(键盘、内存、外存、显示器等)之间进行传递的数据的抽象
- 输入的字节序列被称为输入流,输出的字节序列被称为输出流
14.3.2 流的分类
计算机中传输的数据如同管道中流动的水,根据数据格式,输入输出流可以分为:
文本流 (Text Stream)
- 文本流中输入输出的数据是字符或字符串,可以被修改
二进制流 (Binary Stream)
- 二进制流中输入输出的是一系列字节,不能以任何方式修改
14.4 C++输入/输出类层次结构
14.4.1 C++的I/O类库架构
C++没有内置的输入/输出(I/O)命令,在C++中,I/O命令被包含在类库中
类层次结构组成
- 基类
ios_base管理格式化标志和输入/输出异常 - 类模板
basic_ios<>管理任意流缓冲
主要类说明
using ios=basic_ios<char>;ios是char类型的流缓冲中的基类ios是char类型的流缓冲中的基类
14.4.2 输入流类 istream
派生类 istream 通过添加从流中读取数据的成员函数来扩展基类
- 该类通过重载运算符
>>来提供基本的输入处理,用于从流中读取内置数据类型(char、int、float等)的数据,从而提供基本的输入处理操作 - 流
cin是该派生类 istream 的对象,通常与键盘输入相关联
14.4.3 输出流类 ostream
派生类 ostream 包含向流输出数据的成员函数
- 该类通过重载插入运算符
<<来提供基本的输出处理,用于向流中输出内置数据类型的数据,从而提供了基本的输出处理操作 - 流
cout是派生类 ostream 的对象,通常与屏幕输出相关联
14.4.4 字符串流类
istringstream(字符串输入流)
istringstream是从 istream 派生的类,用于创建字符串输入对象
ostringstream(字符串输出流)
ostringstream则是从 ostream 派生的类,用于创建字符串输出对象
stringstream(字符串双向流)
- 用于创建字符串对象的类
stringstream,既能用于输入也能用于输出
14.4.5 文件流类
ifstream(文件输入流)
ifstream是从 istream 派生的类,用于创建文件输入对象
ofstream(文件输出流)
ofstream则是从 ostream 派生的类,用于创建文件输出对象
fstream(文件双向流)
- 用于创建文件对象的类
fstream,既能用于输入也能用于输出
类库的定义包含在头文件 iostream、fstream 和 sstream 中
14.4.6 预定义流对象
C++标准库预定义了四个重要的流对象:
| 对象名 | 所属类 | 对应设备 | 含义 |
|---|---|---|---|
cin | istream | 键盘 | 标准输入,有缓冲 (buffered standard input) |
cout | ostream | 屏幕 | 标准输出,有缓冲 (buffered standard output) |
cerr | ostream | 屏幕 | 标准错误输出,无缓冲 (no-buffered standard error output) |
clog | ostream | 屏幕 | 标准日志输出,有缓冲 (buffered standard log output) |
14.5 标准I/O流详解
14.5.1 标准输出流对象
ostream类预定义了三个输出流对象:cout、cerr 和 clog
格式化输出
ostream类重载了运算符"<<“,输出流对象与”<<"运算符结合使用,可以格式化的输出各种类型的数据。
// C++23格式化输出示例
#include <bits/stdc++.h>
int main() {
int number = 42;
double pi = 3.14159;
std::string text = "Hello";
// 使用cout进行格式化输出
std::cout << "数字: " << number << std::endl;
std::cout << "Pi值: " << pi << std::endl;
std::cout << "文本: " << text << std::endl;
// C++23推荐的格式化输出
std::println("数字: {}", number);
std::println("Pi值: {:.2f}", pi);
std::println("文本: {}", text);
return 0;
}
流操纵符 (Manipulators)
流操纵符可以用于输入流和输出流
| 操纵符 | 功能 |
|---|---|
endl | 换行,并刷新输出流缓冲区 |
fixed | 以定点表示法显示实数,默认小数部分6位 |
setw(n) | 设置当前输出项的宽度为n个字符位 |
setfill(c) | 设置当前空白的填充字符为字符c,为字符,默认为空格 |
setprecision(n) | 设置浮点数的小数部分包括小数点的位数,默认为6 |
setiosflags(flag) | 设置格式标志 |
resetiosflags() | 重新设置格式标志为默认格式 |
// 流操纵符使用示例
#include <bits/stdc++.h>
int main() {
double value = 123.456789;
std::cout << "默认输出: " << value << std::endl;
std::cout << "固定小数点: " << std::fixed << value << std::endl;
std::cout << "设置精度: " << std::setprecision(2) << value << std::endl;
std::cout << "设置宽度: " << std::setw(10) << value << std::endl;
std::cout << "填充字符: " << std::setfill('*') << std::setw(10) << value << std::endl;
// C++23现代方式
std::println("现代格式化: {:.2f}", value);
std::println("宽度和精度: {:*>10.2f}", value);
return 0;
}
无格式输出成员函数
-
put()输出单个字符ostream& put(char_type ch); -
write()输出字符块,count表示输出的字符个数ostream& write(const char* s, std::streamsize count);
put() 和 write() 函数返回的是输出流对象,因此与输出运算符"<<"一样,可以连续调用。
// 无格式输出示例
#include <bits/stdc++.h>
int main() {
const char* text = "Hello World";
// 使用put()输出单个字符
std::cout.put('A').put('B').put('C') << std::endl;
// 使用write()输出字符块
std::cout.write(text, 5) << std::endl; // 输出"Hello"
std::cout.write(text, std::strlen(text)) << std::endl; // 输出完整字符串
return 0;
}
处理流错误
IO流错误可能在各种情况下发生
| 状态函数 | 说明 |
|---|---|
bool operator bool() const | 检查是否没有错误发生(fail()的反义词) |
bool operator!() const | 检查是否有错误发生(fail()的同义词) |
int good() const | 检查是否没有错误发生,如可以执行下次输入/输出操作 |
int eof() const | 检查是否到达了文件末尾 |
int fail() const | 检查是否发生了可恢复的错误 |
int bad() const | 检查是否发生了不可恢复的错误 |
// 流错误处理示例
#include <bits/stdc++.h>
int main() {
int number;
std::println("请输入一个整数:");
std::cin >> number;
// 检查输入状态
if (std::cin.good()) {
std::println("输入成功: {}", number);
} else if (std::cin.fail()) {
std::println("输入失败,类型不匹配");
std::cin.clear(); // 清除错误标志
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 清空缓冲区
} else if (std::cin.bad()) {
std::println("严重错误");
}
return 0;
}
14.5.2 标准输入流对象
istream类预定义标准输入流对象cin
格式化输入
istream类重载了运算符">>“,输入流对象与”>>"运算符结合使用,可以格式化的输入各种类型的数据。
// 格式化输入示例
#include <bits/stdc++.h>
int main() {
int age;
double salary;
std::string name;
std::println("请输入姓名、年龄和薪资:");
std::cin >> name >> age >> salary;
std::println("输入的信息:");
std::println("姓名: {}", name);
std::println("年龄: {}", age);
std::println("薪资: {:.2f}", salary);
return 0;
}
输入流成员函数详解
| 函数原型 | 说明 |
|---|---|
int get() | 提取一个字符(包括空格),若到达回车符的值;若不能提取字符,返回EOF并将其对应的failbit标志。 |
istream& get(char& ch) | 提取一个字符(包括空格性)到ch,返回istream对象的引用,若表示get()成功。 |
istream& get(char* pch, int nCount, char delim = '\n') | 提取多个nCount-1个字符串pch中或者遇到分隔delim(默认为’\n’)或者达到文件末尾为止,不会提取delim字符,并保留在输入流中。 |
istream& getline(char* pch, int nCount, char delim = '\n') | 提取多个nCount-1个字符到pch中或者遇到分隔delim(默认为’\n’)或者达到文件末尾为止,会提取delim字符,并保留在输入流中。 |
istream& ignore(int nCount = 1, int delim = EOF) | 跳过或忽略输入流中的delim为止的字符或者多个nCount个字符,跳过ignore()时忽略的字符不会加到istream对象中去,会忽略多个字符,并设行不大于nCount个delim。 |
int gcount() const | 返回前次getline()获取的字符数。 |
int peek() | 返回输入流的下一个字符,如不能读时返回,返回EOF。 |
istream& putback(char ch) | 将上一次从输入流中通过get试取的字符再投回到输入流中。 |
istream& read(char* pch, int nCount) | 从输入流中读取字符,放入pch开始的缓存中直到提取完Count个字符或者到达文件末尾,返回当前输入流的引用。 |
// 输入流成员函数使用示例
#include <bits/stdc++.h>
int main() {
char ch;
char buffer[100];
std::println("请输入一些字符:");
// 使用get()逐个读取字符
std::println("使用get()读取字符 (输入'q'退出):");
while (std::cin.get(ch) && ch != 'q') {
std::cout.put(ch);
}
std::cin.ignore(); // 忽略换行符
// 使用getline()读取一行
std::println("\n请输入一行文本:");
std::cin.getline(buffer, sizeof(buffer));
std::println("读取到: {}", buffer);
std::println("读取字符数: {}", std::cin.gcount());
// 使用peek()查看下一个字符
std::println("请输入一个字符:");
int next_char = std::cin.peek();
if (next_char != EOF) {
std::println("下一个字符是: '{}'", static_cast<char>(next_char));
}
return 0;
}
无格式输入成员函数
get() getline() read() 函数返回的是输入流对象,因此可以连续调用
针对istream类,使用std::getline()无格式输入函数读取C++字符串
// std::getline()使用示例
#include <bits/stdc++.h>
int main() {
std::string line;
std::println("请输入多行文本 (输入空行结束):");
while (std::getline(std::cin, line) && !line.empty()) {
std::println("读取到: '{}'", line);
std::println("长度: {}", line.length());
}
std::println("输入结束");
return 0;
}
14.5.3 字符串流 (String Stream)
字符串流的基本概念
字符串流提供了一种将流语义用于字符串的方法
通过字符串流将流语义用于字符串。通过这种方式,可以得到一个内存中的流来表示文本数据。
字符串流也非常适合于解析文本,因为流内建了标记化的功能
字符串流类的分类
std::ostringstream类用于将数据写入字符串std::istringstream类用于从字符串读取数据- 它们都在
<sstream>中定义
// 字符串流基本使用示例
#include <bits/stdc++.h>
int main() {
// 使用ostringstream写入数据到字符串
std::ostringstream oss;
oss << "姓名: " << "张三" << ", 年龄: " << 25 << ", 分数: " << 89.5;
std::string output = oss.str();
std::println("构建的字符串: {}", output);
// 使用istringstream从字符串读取数据
std::string data = "100 3.14 Hello";
std::istringstream iss{data};
int number;
double decimal;
std::string word;
iss >> number >> decimal >> word;
std::println("解析结果: {} {} {}", number, decimal, word);
return 0;
}
字符串流的行为特点
由于 ostringstream 和 istringstream 继承了与 ostream 和 istream 相同的行为,因此这两个类的使用也非常相似
// 字符串流继承行为示例
#include <bits/stdc++.h>
int main() {
// ostringstream的行为类似ostream
std::ostringstream oss;
oss << std::fixed << std::setprecision(2) << 123.456;
oss << " | " << std::setw(10) << std::setfill('*') << "test";
std::println("格式化结果: {}", oss.str());
// istringstream的行为类似istream
std::string input = "apple banana cherry";
std::istringstream iss{input};
std::string word;
std::println("解析单词:");
while (iss >> word) {
std::println("- {}", word);
}
return 0;
}
字符串流的优势
1. 当前位置追踪
与标准C++字符串相比,字符串流的主要优势是,除了数据之外,这个对象还知道从哪里进行下一次的读或写操作,这个位置也称为当前位置
// 当前位置追踪示例
#include <bits/stdc++.h>
int main() {
std::stringstream ss;
ss << "Hello " << "World " << 123;
// 获取当前写入位置
auto write_pos = ss.tellp();
std::println("写入位置: {}", static_cast<long>(write_pos));
// 重新定位到开始位置进行读取
ss.seekg(0);
std::string word1, word2;
int number;
ss >> word1 >> word2 >> number;
std::println("读取结果: {} {} {}", word1, word2, number);
// 获取当前读取位置
auto read_pos = ss.tellg();
std::println("读取位置: {}", static_cast<long>(read_pos));
return 0;
}
2. 高效的数值转换
istringstream 类的典型用法,是将一个数字字符串转换成对应的数值,而 ostringstream 类的典型用法,是将数值转换为字符串
相比于C语言的转换函数,转换效率更高而且更安全
// 数值转换效率对比示例
#include <bits/stdc++.h>
#include <chrono>
// C语言方式转换
std::string to_string_c_style(int value) {
char buffer[32];
sprintf(buffer, "%d", value);
return std::string{buffer};
}
// C++字符串流方式转换
std::string to_string_stream(int value) {
std::ostringstream oss;
oss << value;
return oss.str();
}
// C++23现代方式转换
std::string to_string_modern(int value) {
return std::format("{}", value);
}
int main() {
const int test_value = 12345;
const int iterations = 100000;
// 测试C语言方式
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
auto result = to_string_c_style(test_value);
}
auto end = std::chrono::high_resolution_clock::now();
auto c_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
// 测试字符串流方式
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
auto result = to_string_stream(test_value);
}
end = std::chrono::high_resolution_clock::now();
auto stream_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
// 测试现代方式
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
auto result = to_string_modern(test_value);
}
end = std::chrono::high_resolution_clock::now();
auto modern_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::println("性能对比 (转换{}次):", iterations);
std::println("C语言方式: {} 微秒", c_time.count());
std::println("字符串流方式: {} 微秒", stream_time.count());
std::println("C++23现代方式: {} 微秒", modern_time.count());
return 0;
}
3. 强大的格式化功能
另一个优势是字符串流支持流操纵符和本地化,且格式化功能更强大
// 强大的格式化功能示例
#include <bits/stdc++.h>
int main() {
std::ostringstream oss;
double pi = 3.14159265359;
int large_number = 1234567;
// 使用流操纵符进行复杂格式化
oss << "科学计数法: " << std::scientific << pi << std::endl;
oss << "固定小数点: " << std::fixed << std::setprecision(3) << pi << std::endl;
oss << "十六进制: 0x" << std::hex << large_number << std::endl;
oss << "八进制: 0" << std::oct << large_number << std::endl;
oss << "十进制: " << std::dec << large_number << std::endl;
// 字段宽度和填充
oss << "右对齐: " << std::setw(15) << std::setfill('*') << "test" << std::endl;
oss << "左对齐: " << std::left << std::setw(15) << std::setfill('-') << "test" << std::endl;
std::println("格式化结果:\n{}", oss.str());
return 0;
}
字符串流操作成员函数
str() 函数
str() 获取或设置底层字符串设备对象的内容,存在复制操作
std::string str() const返回底层字符串的副本void str(std::string s)替换底层字符串
// str()函数使用示例
#include <bits/stdc++.h>
int main() {
std::ostringstream oss;
oss << "初始内容";
// 获取字符串内容
std::string content = oss.str();
std::println("当前内容: {}", content);
// 追加更多内容
oss << " 追加内容";
std::println("追加后: {}", oss.str());
// 替换整个字符串
oss.str("全新内容");
std::println("替换后: {}", oss.str());
// 清空字符串
oss.str("");
oss << "清空后的新内容";
std::println("清空后: {}", oss.str());
return 0;
}
字符串格式化效率对比
对字符串格式化并存储的,从高到低的效率排序是:
std::format()C++20 高效且灵活的字符串格式化机制,通常比std::ostringstream更高效std::ostringstream在内存中构建格式化字符串,因内存分配开销,频繁使用导致性能下降std::print()C++23 打印格式化输出的便捷函数,对于直接输出到控制台的场景,非常高效std::cout标准输出流对象,对简单无格式化输出最快,但不适合用于字符串格式化并存储的场景
// 字符串格式化效率实测
#include <bits/stdc++.h>
#include <chrono>
void test_ostringstream(int iterations) {
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
std::ostringstream oss;
oss << "值: " << i << ", 平方: " << (i * i) << ", 平方根: "
<< std::fixed << std::setprecision(2) << std::sqrt(i);
std::string result = oss.str();
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::println("ostringstream: {} 微秒", duration.count());
}
void test_format(int iterations) {
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
std::string result = std::format("值: {}, 平方: {}, 平方根: {:.2f}",
i, i * i, std::sqrt(i));
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::println("std::format: {} 微秒", duration.count());
}
int main() {
const int iterations = 10000;
std::println("格式化效率测试 ({}次操作):", iterations);
test_format(iterations);
test_ostringstream(iterations);
return 0;
}
14.5.4 字符串流综合应用示例
// 字符串流综合应用示例
#include <bits/stdc++.h>
struct Person {
std::string name{};
int age{};
double salary{};
// 序列化到字符串
std::string to_string() const {
std::ostringstream oss;
oss << name << "," << age << "," << std::fixed << std::setprecision(2) << salary;
return oss.str();
}
// 从字符串反序列化
static std::optional<Person> from_string(const std::string& str) {
std::istringstream iss{str};
std::string name, age_str, salary_str;
if (std::getline(iss, name, ',') &&
std::getline(iss, age_str, ',') &&
std::getline(iss, salary_str)) {
try {
int age = std::stoi(age_str);
double salary = std::stod(salary_str);
return Person{name, age, salary};
} catch (const std::exception&) {
return std::nullopt;
}
}
return std::nullopt;
}
// 格式化显示
std::string format_display() const {
std::ostringstream oss;
oss << std::left << std::setw(15) << name
<< std::right << std::setw(5) << age
<< std::setw(12) << std::fixed << std::setprecision(2) << salary;
return oss.str();
}
};
// 文本解析器类
class TextParser {
private:
std::istringstream m_stream;
public:
explicit TextParser(const std::string& text) : m_stream{text} {}
// 解析整数列表
std::vector<int> parse_integers() {
std::vector<int> result;
int value;
while (m_stream >> value) {
result.push_back(value);
}
return result;
}
// 解析键值对
std::map<std::string, std::string> parse_key_value_pairs() {
std::map<std::string, std::string> result;
std::string line;
m_stream.clear();
m_stream.seekg(0);
while (std::getline(m_stream, line)) {
std::istringstream line_stream{line};
std::string key, value;
if (std::getline(line_stream, key, '=') &&
std::getline(line_stream, value)) {
result[key] = value;
}
}
return result;
}
};
int main() {
// 人员数据处理
std::vector<Person> people = {
{"张三", 30, 8500.50},
{"李四", 25, 6800.00},
{"王五", 35, 12000.00}
};
std::println("=== 人员信息表 ===");
std::println("{:<15} {:>5} {:>12}", "姓名", "年龄", "薪资");
std::println("{:-<32}", "");
for (const auto& person : people) {
std::println("{}", person.format_display());
}
// 序列化和反序列化测试
std::println("\n=== 序列化测试 ===");
std::string serialized = people[0].to_string();
std::println("序列化结果: {}", serialized);
if (auto restored = Person::from_string(serialized)) {
std::println("反序列化成功: {}", restored->format_display());
}
// 文本解析测试
std::println("\n=== 文本解析测试 ===");
// 解析整数列表
TextParser int_parser{"10 20 30 40 50"};
auto integers = int_parser.parse_integers();
std::println("解析到的整数: {}", integers);
// 解析配置文件格式
std::string config_text = R"(
name=应用程序
version=1.0.0
debug=true
max_connections=100
)";
TextParser config_parser{config_text};
auto config = config_parser.parse_key_value_pairs();
std::println("解析到的配置:");
for (const auto& [key, value] : config) {
std::println(" {} = {}", key, value);
}
return 0;
}
14.6 文件流与对象I/O
14.6.1 文件流的基本概念
文件流是以磁盘中的文件作为输入输出对象的数据流
- 输出文件流将数据从内存输出到文件中,这个过程通常称为写文件
- 输入文件流将数据从磁盘中的文件读入内存,这个过程通常称为读文件
14.6.2 二进制文件的读写
文件流类从istream类和ostream类继承了read()和write()函数,用于读写二进制文件
二进制读写函数原型
// istream类的read函数
istream& read(char* s, std::streamsize count);
// ostream类的write函数
ostream& write(const char* s, std::streamsize count);
std::streamsize类型说明
using std::streamsize = int64_t (long long)
表示I/O操作中传输字符数或I/O缓冲区大小的有符号整数类型。
gcount()函数
std::streamsize gcount() const;
返回最近一次无格式输入操作提取的字符数
二进制文件读写示例
// 二进制文件读写示例
#include <bits/stdc++.h>
struct Student {
char name[20];
int id;
double grade;
};
int main() {
const std::string filename = "students.dat";
// 写入二进制数据
{
std::ofstream file{filename, std::ios::binary};
if (!file) {
std::println("无法创建文件");
return 1;
}
Student students[] = {
{"张三", 1001, 85.5},
{"李四", 1002, 92.0},
{"王五", 1003, 78.5}
};
for (const auto& student : students) {
file.write(reinterpret_cast<const char*>(&student), sizeof(Student));
}
std::println("二进制数据写入完成");
std::println("写入了 {} 个学生记录", std::size(students));
}
// 读取二进制数据
{
std::ifstream file{filename, std::ios::binary};
if (!file) {
std::println("无法打开文件");
return 1;
}
Student student;
std::println("\n读取的学生数据:");
std::println("{:<10} {:<6} {:<8}", "姓名", "学号", "成绩");
std::println("{:-<26}", "");
while (file.read(reinterpret_cast<char*>(&student), sizeof(Student))) {
std::println("{:<10} {:<6} {:<8.1f}",
student.name, student.id, student.grade);
std::println("读取了 {} 字节", file.gcount());
}
// 检查读取状态
if (file.eof()) {
std::println("已到达文件末尾");
} else if (file.fail()) {
std::println("读取过程中发生错误");
}
}
return 0;
}
// 文件流基本概念示例
#include <bits/stdc++.h>
int main() {
// 输出文件流 - 写文件
{
std::ofstream output_file{"example.txt"};
if (output_file) {
output_file << "这是写入文件的数据\n";
output_file << "数字: " << 42 << "\n";
std::println("数据已写入文件");
}
// 文件在作用域结束时自动关闭
}
// 输入文件流 - 读文件
{
std::ifstream input_file{"example.txt"};
if (input_file) {
std::string line;
std::println("从文件读取的内容:");
while (std::getline(input_file, line)) {
std::println("{}", line);
}
}
// 文件在作用域结束时自动关闭
}
return 0;
}
14.6.3 随机存取 (Random Access)
顺序文件处理 vs 随机存取
serial file processing 顺序文件处理
- 在顺序文件处理过程中,对数据项的读写是一个接着一个进行的
random or direct access 随机或直接存取
- 你可以在文件中随意定位,在文件的任何位置读写数据
文件位置标记 (File Position Marker, FPM)
文件中的位置信息保存在文件位置标记(FPM)中
The position in a file is held in the File Position Marker (FPM).
随机存取相关函数
istream成员函数
istream& seekg(off_type off, std::ios_base::seekdir dir = ios::beg);
seekg() (meaning ‘seek get’) 用来设置输入文件的FPM,用来为输入文件设定FPM,指示下一个读取的数据的位置
- 第一个实参是一个偏移量,它告诉FPM要跳过多少字节
- 第二个实参指定偏移量计算的起始位置 (默认为 ios::beg )
ostream成员函数
ostream& seekp(off_type off, std::ios_base::seekdir dir = ios::beg);
seekp() 设置输出文件的FPM
位置查询函数
std::streampos tellg() const; // 返回输入流中FPM的当前值
std::streampos tellp() const; // 返回输出流中FPM的当前值
偏移量参考点
| 偏移值常量 | 说明 |
|---|---|
ios::beg (=0) | 偏移量从流的开始位置计算 |
ios::cur (=1) | 偏移量从流的当前位置计算 |
ios::end (=2) | 偏移量从流的末尾位置计算 |
14.6.4 随机存取示例
示例:打开包含字符A到J的文件
// 随机存取示例
#include <bits/stdc++.h>
int main() {
// 创建包含字符A到J的文件
{
std::ofstream file{"letters.dat"};
for (char c = 'A'; c <= 'J'; ++c) {
file << c;
}
}
// 随机存取演示
{
std::ifstream file{"letters.dat"};
if (!file) {
std::println("无法打开文件");
return 1;
}
std::println("文件内容: ABCDEFGHIJ");
std::println("位置: 0123456789");
std::println();
// 演示不同的seekg操作
char ch;
// 1. in.seekg(4, ios::beg) 或 in.seekg(4)
file.seekg(4, std::ios::beg);
file.get(ch);
std::println("seekg(4, ios::beg) -> 位置4,读取: '{}'", ch);
std::println("当前FPM位置: {}", file.tellg());
// 2. in.seekg(1, ios::cur)
file.seekg(1, std::ios::cur);
file.get(ch);
std::println("seekg(1, ios::cur) -> 当前位置+1,读取: '{}'", ch);
std::println("当前FPM位置: {}", file.tellg());
// 3. in.seekg(-3, ios::cur)
file.seekg(-3, std::ios::cur);
file.get(ch);
std::println("seekg(-3, ios::cur) -> 当前位置-3,读取: '{}'", ch);
std::println("当前FPM位置: {}", file.tellg());
// 4. in.seekg(0, ios::beg)
file.seekg(0, std::ios::beg);
file.get(ch);
std::println("seekg(0, ios::beg) -> 文件开始,读取: '{}'", ch);
std::println("当前FPM位置: {}", file.tellg());
// 5. in.seekg(0, ios::end)
file.seekg(0, std::ios::end);
std::println("seekg(0, ios::end) -> 文件末尾");
std::println("当前FPM位置: {}", file.tellg());
}
return 0;
}
实际应用:学生记录的随机存取
// 学生记录随机存取示例
#include <bits/stdc++.h>
struct Student {
char name[20];
int id;
double grade;
void display() const {
std::println("姓名: {}, 学号: {}, 成绩: {:.1f}", name, id, grade);
}
};
class StudentDatabase {
private:
std::string m_filename;
public:
explicit StudentDatabase(const std::string& filename) : m_filename{filename} {}
// 在指定位置写入学生记录
void write_student_at(int index, const Student& student) {
std::fstream file{m_filename, std::ios::in | std::ios::out | std::ios::binary};
if (!file) {
// 如果文件不存在,创建新文件
file.open(m_filename, std::ios::out | std::ios::binary);
file.close();
file.open(m_filename, std::ios::in | std::ios::out | std::ios::binary);
}
// 定位到指定位置
file.seekp(index * sizeof(Student), std::ios::beg);
file.write(reinterpret_cast<const char*>(&student), sizeof(Student));
std::println("学生记录已写入位置 {}", index);
}
// 从指定位置读取学生记录
std::optional<Student> read_student_at(int index) {
std::ifstream file{m_filename, std::ios::binary};
if (!file) {
return std::nullopt;
}
// 定位到指定位置
file.seekg(index * sizeof(Student), std::ios::beg);
Student student;
if (file.read(reinterpret_cast<char*>(&student), sizeof(Student))) {
return student;
}
return std::nullopt;
}
// 获取数据库中的记录数
int get_record_count() {
std::ifstream file{m_filename, std::ios::binary};
if (!file) {
return 0;
}
file.seekg(0, std::ios::end);
auto file_size = file.tellg();
return static_cast<int>(file_size) / sizeof(Student);
}
};
int main() {
StudentDatabase db{"student_db.dat"};
// 在不同位置写入学生记录
db.write_student_at(0, {"张三", 1001, 85.5});
db.write_student_at(2, {"李四", 1002, 92.0}); // 跳过位置1
db.write_student_at(1, {"王五", 1003, 78.5}); // 填充位置1
std::println("\n数据库中的记录数: {}", db.get_record_count());
// 随机读取学生记录
std::println("\n随机读取学生记录:");
for (int i = 0; i < 3; ++i) {
if (auto student = db.read_student_at(i)) {
std::print("位置 {}: ", i);
student->display();
} else {
std::println("位置 {}: 无记录", i);
}
}
return 0;
}
14.6.5 文件流的优势
文件本身非常适合流的抽象,因为读写文件时,除数据外,还涉及读写的位置
在C++中,std::ifstream、std::ofstream 和 std::fstream 类提供了文件的输入输出功能。
在处理文件相关的系统时,错误情形的检测和处理尤为重要
可以用前述的标准错误处理机制来检测错误状况。
14.6.6 文件流对象的构造
在C++中,文件流不像标准I/O流预定义了输入流对象和输出流对象
要使用 std::ifstream、std::ofstream 和 std::fstream 文件流进行文件的读写操作,首先需要使用文件名作为构造函数的参数去创建文件流对象,然后把文件流对象与文件关联起来(打开文件)。
std::ifstream、std::ofstream 和 std::fstream 析构函数自动关闭底层文件,一般不需要显式调用 close() 关闭文件
// 文件流对象构造示例
#include <bits/stdc++.h>
int main() {
// 方法1: 构造时直接指定文件名
std::ofstream file1{"direct_construction.txt"};
if (file1.is_open()) {
file1 << "直接构造的文件\n";
std::println("文件1创建成功");
}
// 方法2: 先构造对象,再打开文件
std::ofstream file2;
file2.open("delayed_open.txt");
if (file2.is_open()) {
file2 << "延迟打开的文件\n";
std::println("文件2创建成功");
file2.close(); // 可以显式关闭,但不是必需的
}
// 方法3: 使用文件路径
std::filesystem::path file_path{"path_based.txt"};
std::ofstream file3{file_path};
if (file3) {
file3 << "基于路径的文件\n";
std::println("文件3创建成功");
}
return 0;
}
14.6.7 常用的文件打开模式
| 文件打开模式 | 说明 |
|---|---|
ios::in | 以只读模式打开文件,若文件不存在,则创建失败 |
ios::out | 以只写模式打开文件,写入时覆盖输入;若文件不存在,则创建一个新文件 |
ios::app | 以追加模式打开文件,若文件不存在,则创建一个新文件 |
ios::ate | 打开一个已存在文件,并移动文件位置指针到文件末尾 |
ios::trunc | 打开一个文件,若文件内容存在,若文件不存在,则创建一个新文件 |
ios::binary | 以二进制方式打开文件 |
各种文件流的默认模式:
ifstream自动包含ios_base::in模式ofstream自动包含ios_base::out模式fstream自动包含ios_base::in | ios_base::out模式,即便没有明确指定in或out作为文件打开模式
// 文件打开模式示例
#include <bits/stdc++.h>
int main() {
const std::string filename = "mode_test.txt";
// 1. 输出模式 - 覆盖写入
{
std::ofstream file{filename, std::ios::out};
file << "第一行数据\n第二行数据\n";
std::println("输出模式写入完成");
}
// 2. 追加模式 - 在文件末尾添加
{
std::ofstream file{filename, std::ios::app};
file << "追加的第三行数据\n";
std::println("追加模式写入完成");
}
// 3. 截断模式 - 清空原内容
{
std::ofstream file{filename, std::ios::out | std::ios::trunc};
file << "截断后的新内容\n";
std::println("截断模式写入完成");
}
// 4. 二进制模式
{
std::ofstream file{filename, std::ios::binary};
int number = 12345;
double value = 3.14159;
file.write(reinterpret_cast<const char*>(&number), sizeof(number));
file.write(reinterpret_cast<const char*>(&value), sizeof(value));
std::println("二进制模式写入完成");
}
// 5. 读取二进制文件
{
std::ifstream file{filename, std::ios::binary};
int number;
double value;
file.read(reinterpret_cast<char*>(&number), sizeof(number));
file.read(reinterpret_cast<char*>(&value), sizeof(value));
std::println("从二进制文件读取: {} {:.5f}", number, value);
}
return 0;
}
14.6.8 文本模式与二进制模式的区别
默认情况下,文件流以文本模式打开。如果指定 ios_base::binary 标志,则文件将以二进制模式打开
二进制模式特点
- 在二进制模式下,写入时,将数据精确写入文件,没有任何转换
- 读取时,把文件中存储的字节原封不动直接读入数据中
文本模式特点
- 在文本模式下,会发生一些隐藏的转换:写入或读取文件的每一行都以
\n结尾 - 但是,在文件中如何对行尾进行编码取决于操作系统
// 文本模式与二进制模式对比
#include <bits/stdc++.h>
struct Data {
int id;
double value;
char name[20];
};
int main() {
Data original_data{42, 3.14159, "测试数据"};
// 二进制模式写入
{
std::ofstream file{"binary_data.bin", std::ios::binary};
file.write(reinterpret_cast<const char*>(&original_data), sizeof(Data));
std::println("二进制数据写入完成");
}
// 二进制模式读取
{
std::ifstream file{"binary_data.bin", std::ios::binary};
Data read_data{};
file.read(reinterpret_cast<char*>(&read_data), sizeof(Data));
std::println("二进制读取结果:");
std::println("ID: {}", read_data.id);
std::println("Value: {:.5f}", read_data.value);
std::println("Name: {}", read_data.name);
}
// 文本模式写入(用于对比)
{
std::ofstream file{"text_data.txt"};
file << original_data.id << "\n";
file << std::fixed << std::setprecision(5) << original_data.value << "\n";
file << original_data.name << "\n";
std::println("文本数据写入完成");
}
// 比较文件大小
auto binary_size = std::filesystem::file_size("binary_data.bin");
auto text_size = std::filesystem::file_size("text_data.txt");
std::println("文件大小对比:");
std::println("二进制文件: {} 字节", binary_size);
std::println("文本文件: {} 字节", text_size);
std::println("二进制文件 {} 小",
binary_size < text_size ? "更" : "不");
return 0;
}
14.6.9 文本文件的读写
对文本文件的读写,C++提供了两种读写方式:
-
使用提取运算符">>“和插入运算符”<<"读写
- → 写文件功能足够,读文件在整行读入时不满足要求
-
调用文件流类的成员函数进行读写
- → 尽量不用或少用
从文件中读取整行存入C++字符串时,使用无格式输入函数 std::getline()
istream& std::getline(std::istream& is, std::string& str, char delimiter);
// 文本文件读写的两种方式
#include <bits/stdc++.h>
int main() {
const std::string filename = "student_data.txt";
// 方式1: 使用运算符读写
std::println("=== 使用运算符读写 ===");
// 写入学生数据
{
std::ofstream file{filename};
file << "张三 20 85.5\n";
file << "李四 19 92.0\n";
file << "王五 21 78.5\n";
std::println("学生数据写入完成");
}
// 使用运算符读取(适合格式化数据)
{
std::ifstream file{filename};
std::string name;
int age;
double score;
std::println("使用>>运算符读取:");
while (file >> name >> age >> score) {
std::println("姓名: {}, 年龄: {}, 成绩: {:.1f}",
name, age, score);
}
}
// 方式2: 使用std::getline读取整行
std::println("\n=== 使用getline读取整行 ===");
// 写入包含空格的数据
{
std::ofstream file{"complex_data.txt"};
file << "张 三,20,85.5\n";
file << "李 四,19,92.0\n";
file << "王 五,21,78.5\n";
}
// 使用getline读取整行
{
std::ifstream file{"complex_data.txt"};
std::string line;
std::println("使用getline读取整行:");
while (std::getline(file, line)) {
std::println("完整行: {}", line);
// 进一步解析行内容
std::istringstream iss{line};
std::string name, age_str, score_str;
if (std::getline(iss, name, ',') &&
std::getline(iss, age_str, ',') &&
std::getline(iss, score_str)) {
int age = std::stoi(age_str);
double score = std::stod(score_str);
std::println(" 解析: 姓名='{}', 年龄={}, 成绩={:.1f}",
name, age, score);
}
}
}
return 0;
}
14.6.10 文件末尾检测
从文件中读取数据时,必须检测是否到达了文件的末尾,以便结束对文件的处理操作
检测方法
-
可用 ios 类的成员函数
eof()检测文件的末尾,ifstream 类也从 ios 类继承了这个函数 -
更常见的,是用重载的 bool 转换运算符及求反 ! 运算符直接检测流的状态
// 文件末尾检测方法
#include <bits/stdc++.h>
int main() {
const std::string filename = "test_data.txt";
// 创建测试文件
{
std::ofstream file{filename};
for (int i = 1; i <= 5; ++i) {
file << "第" << i << "行数据\n";
}
}
std::println("=== 方法1: 使用eof()检测 ===");
{
std::ifstream file{filename};
std::string line;
while (!file.eof()) {
if (std::getline(file, line)) {
std::println("读取: {}", line);
}
}
std::println("到达文件末尾");
}
std::println("\n=== 方法2: 使用bool转换运算符 ===");
{
std::ifstream file{filename};
std::string line;
while (file) { // 检测流状态
if (std::getline(file, line)) {
std::println("读取: {}", line);
}
}
std::println("流状态变为无效(到达末尾或出错)");
}
std::println("\n=== 方法3: 直接在while条件中读取(推荐) ===");
{
std::ifstream file{filename};
std::string line;
while (std::getline(file, line)) { // 读取成功则继续
std::println("读取: {}", line);
}
std::println("读取完成");
}
std::println("\n=== 方法4: 综合状态检测 ===");
{
std::ifstream file{filename};
std::string line;
int line_count = 0;
while (std::getline(file, line)) {
++line_count;
std::println("第{}行: {}", line_count, line);
}
// 检查结束原因
if (file.eof()) {
std::println("正常到达文件末尾,共读取{}行", line_count);
} else if (file.fail()) {
std::println("读取失败");
} else if (file.bad()) {
std::println("发生严重错误");
}
}
return 0;
}
14.6.11 文件流错误处理最佳实践
// 文件流错误处理最佳实践
#include <bits/stdc++.h>
class SafeFileReader {
private:
std::ifstream m_file;
std::filesystem::path m_path;
public:
explicit SafeFileReader(const std::filesystem::path& path)
: m_path{path} {
m_file.open(m_path);
if (!m_file) {
throw std::runtime_error(
std::format("无法打开文件: {}", m_path.string()));
}
}
// 读取所有行
std::vector<std::string> read_all_lines() {
std::vector<std::string> lines;
std::string line;
while (std::getline(m_file, line)) {
lines.push_back(line);
}
// 检查读取状态
if (m_file.bad()) {
throw std::runtime_error("读取文件时发生严重错误");
}
return lines;
}
// 安全的数值读取
template<typename T>
std::optional<T> read_value() {
T value;
if (m_file >> value) {
return value;
}
// 清除错误状态以便继续操作
if (m_file.fail() && !m_file.eof()) {
m_file.clear();
m_file.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
return std::nullopt;
}
// 检查文件状态
bool is_good() const { return m_file.good(); }
bool is_eof() const { return m_file.eof(); }
std::streamsize get_line_count() const { return m_file.gcount(); }
};
int main() {
try {
// 创建测试文件
{
std::ofstream file{"mixed_data.txt"};
file << "123\n";
file << "45.67\n";
file << "invalid_number\n";
file << "89\n";
file << "text line\n";
}
SafeFileReader reader{"mixed_data.txt"};
std::println("安全读取数值:");
while (!reader.is_eof()) {
auto value = reader.read_value<int>();
if (value) {
std::println("读取到整数: {}", *value);
} else {
std::println("无法读取整数,跳过");
}
}
// 读取所有行的示例
SafeFileReader line_reader{"mixed_data.txt"};
auto lines = line_reader.read_all_lines();
std::println("\n所有行内容:");
for (size_t i = 0; i < lines.size(); ++i) {
std::println("第{}行: {}", i + 1, lines[i]);
}
} catch (const std::exception& e) {
std::println("错误: {}", e.what());
}
return 0;
}
14.6.12 对象序列化与反序列化
14.6.12.1 序列化和反序列化的概念
对象序列化与反序列化是现代程序开发中的重要技术
序列化 (Serialization)
Object serialization refers to the process of converting a C++ object into a sequence of bytes.
对象序列化是指将一个C++对象转换为一个字节序列的过程。
- 程序在运行过程中,数据都保存在对象中(内存),但在多数情况下,我们都需要将一些数据永久保存到磁盘上,以便将来从磁盘上读入数据来重建对象
反序列化 (Deserialization)
Deserialize refers to the process of restoring a byte sequence in a stream to a C++ object.
反序列化是指将流中的字节序列恢复为C++对象的过程。
Serialization and deserialization functions are very useful in program development.
在程序开发中,序列化和反序列化的功能非常有用。
14.6.12.2 现代C++序列化解决方案
C++ standard does not provide serialization function at present, so there are various serialization libraries.
目前C++标准没有提供序列化功能,于是就出现了各式各样的序列化库。
推荐的序列化库
- boost library and other formats of libraries - boost库及其他格式的库等
- “JSON for modern C++ nlohmann library” is recommended - 推荐使用 “nlohmann 的现代C++ JSON库”
nlohmann JSON库的优势
Powerful STL container support 强大的STL容器支持:
- std::vector, std::map and std::set containers are used directly
- std::vector, std::map, std::set 等容器可直接使用
Flexible extension mechanism 灵活的扩展机制:
- Use global function to_json() / from_json()
- 使用全局函数 to_json() / from_json()
JSON standard compatibility JSON标准兼容:
- strictly follow RFC 8259 standard
- 严格遵循 RFC 8259 标准
JSON简介
JSON (JavaScript Object Notation) JS对象简谱
JSON is a lightweight data exchange format. It uses a text format completely independent of the programming language to store and represent data. The concise and clear hierarchy makes JSON an ideal data exchange language. It is easy for people to read and write, but also easy for machine to parse and generate, and effectively improve the network transmission efficiency.
JSON是一种轻量级的数据交换格式。它采用完全独立于程序语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得JSON成为理想的数据交换语言。易于人阅读和编写,同时也易于机器解析和生成,并有效地提高网络传输效率。
14.6.12.3 使用nlohmann JSON库
1. 环境准备
#include "json.hpp" // 包含JSON库头文件
using nlohmann::json; // 使用别名简化代码
2. 自定义类要求
Custom class requirements 自定义类要求
- Default constructor must be provided - 必须提供默认构造函数
- Suggest providing getter/setter - 建议提供访问器/修改器(全局函数才用)
#include "json.hpp" // 包含JSON库头文件
using nlohmann::json; // 使用别名简化代码
class Student {
std::size_t m_id{}; // 学号
std::string m_name{}; // 姓名
Date m_dob{2000, 1, 1}; // 生日
std::vector<std::string> m_hobbies; // 业余爱好
std::map<std::string, double> m_course_grades; // 各科成绩
public:
Student() = default; // 默认构造函数
// ... other member functions ...
};
3. 序列化实现
Member function implementation 成员函数实现
// json to_json() const;
This function converts the data members of C++ objects into key-value pairs of JSON objects through initialization operations.
此成员函数通过初始化操作,将C++对象的数据成员转换为json对象的键值对。
class Student {
public:
json to_json() const // 将此 C++ 对象转换为 JSON 对象
{
return {{"id", m_id}, {"name", m_name}, {"dob", m_dob},
{"hobbies", m_hobbies}, {"course_grades", m_course_grades}};
}
};
Global Function Implementation (Unified Interface) 全局函数实现(统一接口)
void to_json(json& j, const Student& s) { j = s.to_json(); } // 调用成员函数
4. 反序列化实现
Member function implementation 成员函数实现
// void from_json(const json& js);
This function extracts values from JSON objects using the at() function and assigns them to the data members of C++ objects using get_to().
此成员函数从JSON对象中用at()函数提取值,并用get_to()赋给C++对象的数据成员。
void from_json(const json& js) // 将 JSON 对象转换为 C++ 对象
{
js.at("id").get_to(m_id); // 基本类型
js.at("name").get_to(m_name); // 字符串
js.at("dob").get_to(m_dob); // 自定义类型
js.at("hobbies").get_to(m_hobbies); // STL vector 容器
js.at("course_grades").get_to(m_course_grades); // STL map 映射
}
Global Function Implementation (Unified Interface) 全局函数实现(统一接口)
void from_json(const json& j, Student& s) { s.from_json(j); } // 调用成员函数
5. 自定义类型序列化/反序列化支持(如Date)
Custom type serialization/deserialization support 自定义类型序列化/反序列化支持(如Date)
Use global functions to perform mutual conversion between Date objects and JSON objects without changing the class Date (non-intrusive).
在不改变类Date的情况下(非侵入),用全局函数完成Date对象与json对象的相互转换。
Serialization: non-intrusive implementation of global functions 序列化:非侵入式全局函数实现
void to_json(json& j, const Date& d) { j = d.to_string(); } // 序列化为 "yyyy-mm-dd" 格式
Deserialization: non-intrusive of global functions 反序列化:非侵入式全局函数实现
void from_json(const json& j, Date& d)
{
auto str = j.get<std::string>();
std::istringstream iss{str};
if (iss >> d.iss) // 使用重载的输入操作符解析
throw std::runtime_error("Invalid date format in JSON");
}
14.6.12.4 集合对象的序列化与写入文件
Collection objects serialization and write to file 群体对象的序列化与写入文件
Collection objects serialization 群体对象序列化
// 序列化 vector std::vector<Student> students;
json js_out = json::array(); // 创建一个JSON数组
for (const auto& stu : students)
js_out.emplace_back(stu); // 直接用全局to_json 将每个JSON对象添加到数组中
Write to file 写入文件
if (std::ofstream fs{"student.json", std::ios::out | std::ios::trunc}) { // RAII
fs << js_out.dump(4); // 将整个JSON数组写入文件
}
else
{
std::println("error on opening student.json"); return 1;
}
// RAII 离开作用域,则 fs 自动关闭
14.6.12.5 读取文件与群体对象的反序列化
Read file and collection objects deserialization 读入文件与群体对象的反序列化
Read file 读入文件
json js_in;
if (std::ifstream fs{"student.json"}) // RAII 离开作用域则fs自动关闭
fs >> js_in; // 从文件读取整个JSON数组
else
{
std::println("error on opening student.json"); return 1;
}
Collection objects deserialization 群体对象反序列化
// 反序列化 vector std::vector<Student> students;
for (students.reserve(js_in.size()); const auto& js : js_in)
try
{
students.emplace_back(); // 直接在vector中构造新的空元素
from_json(js, students.back()); // 反序列化到新元素
}
catch (const std::exception& e) // 处理接反序列化错误
{ students.pop_back(); std::println("Error deserializing student: {}", e.what());}
14.6.12.6 nlohmann JSON库的局限
Limitations of nlohmann JSON library nlohmann JSON 库的局限
多态支持 (Polymorphic support)
- Type distribution needs to be implemented manually - 需手动实现类型分发
静态成员 (Static member)
- Non design target, requiring special treatment - 非设计目标,需特殊处理
多重关联容器 (Multiple Associative Container)
- std::multimap, multiset needs custom format - std::multimap, std::multiset 需自定义格式
指针和复杂结构 (Pointers and complex structures)
- Not directly supported - 不直接支持
14.6.12.7 完整示例
// 完整的对象序列化与反序列化示例
#include <bits/stdc++.h>
#include "json.hpp"
using nlohmann::json;
// 日期类
class Date {
private:
int m_year, m_month, m_day;
public:
Date() = default;
Date(int year, int month, int day) : m_year{year}, m_month{month}, m_day{day} {}
std::string to_string() const {
return std::format("{:04d}-{:02d}-{:02d}", m_year, m_month, m_day);
}
void from_string(const std::string& str) {
std::istringstream iss{str};
char sep1, sep2;
iss >> m_year >> sep1 >> m_month >> sep2 >> m_day;
}
};
// Date的JSON序列化支持
void to_json(json& j, const Date& d) {
j = d.to_string();
}
void from_json(const json& j, Date& d) {
d.from_string(j.get<std::string>());
}
// 学生类
class Student {
private:
std::size_t m_id{};
std::string m_name{};
Date m_dob{2000, 1, 1};
std::vector<std::string> m_hobbies{};
std::map<std::string, double> m_course_grades{};
public:
Student() = default;
Student(std::size_t id, std::string name, Date dob)
: m_id{id}, m_name{std::move(name)}, m_dob{dob} {}
// 添加爱好
void add_hobby(const std::string& hobby) {
m_hobbies.push_back(hobby);
}
// 添加成绩
void add_grade(const std::string& course, double grade) {
m_course_grades[course] = grade;
}
// 序列化为JSON
json to_json() const {
return {
{"id", m_id},
{"name", m_name},
{"dob", m_dob},
{"hobbies", m_hobbies},
{"course_grades", m_course_grades}
};
}
// 从JSON反序列化
void from_json(const json& js) {
js.at("id").get_to(m_id);
js.at("name").get_to(m_name);
js.at("dob").get_to(m_dob);
js.at("hobbies").get_to(m_hobbies);
js.at("course_grades").get_to(m_course_grades);
}
// 显示学生信息
void display() const {
std::println("学号: {}, 姓名: {}, 生日: {}",
m_id, m_name, m_dob.to_string());
std::println("爱好: {}", m_hobbies);
std::println("成绩:");
for (const auto& [course, grade] : m_course_grades) {
std::println(" {}: {:.1f}", course, grade);
}
}
};
// Student的全局JSON函数
void to_json(json& j, const Student& s) {
j = s.to_json();
}
void from_json(const json& j, Student& s) {
s.from_json(j);
}
// 学生管理类
class StudentManager {
private:
std::vector<Student> m_students;
public:
void add_student(const Student& student) {
m_students.push_back(student);
}
// 保存到JSON文件
void save_to_file(const std::string& filename) {
json js_out = json::array();
for (const auto& stu : m_students) {
js_out.emplace_back(stu);
}
std::ofstream fs{filename, std::ios::out | std::ios::trunc};
if (!fs) {
throw std::runtime_error("无法打开文件进行写入");
}
fs << js_out.dump(4); // 格式化输出,缩进4个空格
std::println("已保存{}个学生记录到文件: {}", m_students.size(), filename);
}
// 从JSON文件加载
void load_from_file(const std::string& filename) {
std::ifstream fs{filename};
if (!fs) {
throw std::runtime_error("无法打开文件进行读取");
}
json js_in;
fs >> js_in;
m_students.clear();
m_students.reserve(js_in.size());
for (const auto& js : js_in) {
try {
m_students.emplace_back();
from_json(js, m_students.back());
} catch (const std::exception& e) {
m_students.pop_back();
std::println("反序列化学生记录时出错: {}", e.what());
}
}
std::println("已从文件{}加载{}个学生记录", filename, m_students.size());
}
// 显示所有学生
void display_all() const {
std::println("学生名单 (共{}人):", m_students.size());
std::println("{:-<50}", "");
for (const auto& student : m_students) {
student.display();
std::println("{:-<50}", "");
}
}
};
int main() {
try {
StudentManager manager;
// 创建学生数据
Student s1{1001, "张三", Date{2000, 5, 15}};
s1.add_hobby("篮球");
s1.add_hobby("编程");
s1.add_grade("数学", 89.5);
s1.add_grade("英语", 76.0);
Student s2{1002, "李四", Date{2001, 8, 22}};
s2.add_hobby("阅读");
s2.add_hobby("音乐");
s2.add_grade("数学", 95.0);
s2.add_grade("英语", 88.5);
manager.add_student(s1);
manager.add_student(s2);
// 保存到文件
manager.save_to_file("students.json");
// 创建新的管理器并从文件加载
StudentManager new_manager;
new_manager.load_from_file("students.json");
// 显示加载的数据
new_manager.display_all();
} catch (const std::exception& e) {
std::println("错误: {}", e.what());
return 1;
}
return 0;
}
14.7 C和C++23中的文件操作对比
14.7.1 C语言文件操作
#include <stdio.h>
int main() {
FILE* file;
char data[100];
// 打开文件进行写入
file = fopen("data.txt", "w");
if (file == NULL) {
printf("无法打开文件\n");
return 1;
}
// 写入数据
fprintf(file, "Hello, World!\n");
fprintf(file, "数字: %d\n", 42);
// 关闭文件
fclose(file);
// 打开文件进行读取
file = fopen("data.txt", "r");
if (file == NULL) {
printf("无法打开文件\n");
return 1;
}
// 读取数据
while (fgets(data, sizeof(data), file) != NULL) {
printf("%s", data);
}
// 关闭文件
fclose(file);
return 0;
}
14.7.2 C++23文件操作
#include <bits/stdc++.h>
int main() {
// 写入文件
{
std::ofstream file("data.txt");
if (!file) {
std::println("无法打开文件进行写入");
return 1;
}
file << "Hello, World!\n";
file << std::format("数字: {}\n", 42);
// 文件在作用域结束时自动关闭
}
// 读取文件
{
std::ifstream file("data.txt");
if (!file) {
std::println("无法打开文件进行读取");
return 1;
}
std::string line;
while (std::getline(file, line)) {
std::println("{}", line);
}
// 文件在作用域结束时自动关闭
}
return 0;
}
14.7.3 C++23现代文件操作(使用filesystem)
#include <bits/stdc++.h>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
try {
const fs::path file_path{"modern_data.txt"};
// 使用C++23的文件操作
{
std::ofstream file{file_path};
if (!file) {
throw std::runtime_error("无法创建文件");
}
// 使用C++23的格式化输出
std::println(file, "现代C++文件操作示例");
std::println(file, "当前时间: {}",
std::chrono::system_clock::now());
std::println(file, "数据: {}", std::vector{1, 2, 3, 4, 5});
}
// 检查文件信息
if (fs::exists(file_path)) {
const auto file_size = fs::file_size(file_path);
const auto last_write = fs::last_write_time(file_path);
std::println("文件 '{}' 信息:", file_path.string());
std::println("文件大小: {} 字节", file_size);
std::println("最后修改时间: {}", last_write);
}
// 读取文件
{
std::ifstream file{file_path};
std::string content{
std::istreambuf_iterator<char>(file),
std::istreambuf_iterator<char>()
};
std::println("文件内容:\n{}", content);
}
} catch (const std::exception& e) {
std::println("错误: {}", e.what());
return 1;
}
return 0;
}
14.8 RAII原则在文件操作中的应用
14.8.1 文件资源管理类
#include <bits/stdc++.h>
class FileHandler {
private:
std::fstream m_file;
std::filesystem::path m_path;
public:
explicit FileHandler(const std::filesystem::path& path,
std::ios::openmode mode = std::ios::in | std::ios::out)
: m_path{path} {
m_file.open(m_path, mode);
if (!m_file.is_open()) {
throw std::runtime_error(
std::format("无法打开文件: {}", m_path.string()));
}
}
// 自动在析构时关闭文件
~FileHandler() {
if (m_file.is_open()) {
m_file.close();
}
}
// 不允许拷贝
FileHandler(const FileHandler&) = delete;
FileHandler& operator=(const FileHandler&) = delete;
// 允许移动
FileHandler(FileHandler&&) = default;
FileHandler& operator=(FileHandler&&) = default;
// 获取文件流引用
std::fstream& stream() { return m_file; }
const std::fstream& stream() const { return m_file; }
// 便捷的写入方法
template<typename... Args>
void write_line(std::format_string<Args...> fmt, Args&&... args) {
std::println(m_file, fmt, std::forward<Args>(args)...);
}
// 读取一行
std::optional<std::string> read_line() {
std::string line;
if (std::getline(m_file, line)) {
return line;
}
return std::nullopt;
}
// 读取所有内容
std::string read_all() {
m_file.seekg(0);
return std::string{
std::istreambuf_iterator<char>(m_file),
std::istreambuf_iterator<char>()
};
}
};
// 使用示例
void demonstrate_file_handler() {
try {
FileHandler file{"example.txt", std::ios::out | std::ios::trunc};
file.write_line("学生信息管理系统");
file.write_line("姓名: {}, 年龄: {}", "张三", 20);
file.write_line("成绩: {:.2f}", 89.5);
std::println("文件写入完成,自动关闭");
} catch (const std::exception& e) {
std::println("错误: {}", e.what());
}
}
更多推荐
所有评论(0)