【C++】IO库
1. IO继承家族类
C++语言本身并不直接处理输入输出操作,而是通过一组定义在标准库中的类型来实现IO功能。这些类型提供了从各种设备读取数据和向设备写入数据的能力,支持的设备包括但不限于:
- 控制台窗口(标准输入/输出)
- 文件系统上的文件
- 内存中的字符串对象
已使用的IO类型分析
到目前为止,我们主要使用的IO类型和对象都是处理char
类型数据的,例如:
std::cin
- 标准输入流std::cout
- 标准输出流std::cerr
- 标准错误流std::clog
- 标准日志流
这些对象默认关联到用户控制台窗口,但C++的IO系统功能远不止于此。
扩展IO功能
支持多种数据源/目标:
- 文件IO:通过
fstream
、ifstream
、ofstream
等类 - 字符串IO:通过
stringstream
、istringstream
、ostringstream
等类
- 文件IO:通过
支持宽字符:
- 除了
char
类型,IO类还支持wchar_t
宽字符 - 对应有
wcin
、wcout
等宽字符版本 - 实现方式是通过模板设计,如
basic_istream<char>
和basic_istream<wchar_t>
- 除了
继承体系设计:
- C++采用继承家族类的方式统一管理各种IO操作
- 基类提供通用接口,派生类实现特定功能
- 这种设计实现了代码复用和扩展性
• 通过下图1-1和1-2可以看到C++IO类型设计的是一个继承家族,通过继承家族类解决控制台/文件/string的IO操作。
1-1
1-2
C++ IO 继承家族详解
图表(1-1和1-2)精准地描绘了C++标准库中IO流类的核心设计架构。这个设计体现了两个关键思想:继承 和 模板。
1. 核心思想:模板与继承的结合
C++的IO系统是通过一系列类模板构建的,这些模板以字符类型(char
或 wchar_t
)作为参数。这就是为什么IO库能同时支持窄字符(char
)和宽字符(wchar_t
)操作。
basic_istream
: 所有输入流的基类模板。basic_ostream
: 所有输出流的基类模板。basic_iostream
: 同时继承basic_istream
和basic_ostream
,用于双向流。
我们日常使用的类型(如 istream
, cout
)是这些模板的特化别名:
typedef basic_istream<char> istream;
typedef basic_ostream<char> ostream;
typedef basic_iostream<char> iostream;// 宽字符版本
typedef basic_istream<wchar_t> wistream;
typedef basic_ostream<wchar_t> wostream;
// ... 以此类推
2. 继承结构解析
整个IO家族可以清晰地分为三个分支,每个分支都通过公有继承从对应的基类模板派生而来,实现了 “is-a” 的关系。例如,ifstream
is-a
istream
,这意味着所有在istream
上能进行的操作(如>>
),也都能在ifstream
对象上进行。
第一分支:控制台IO(标准流对象)
这是最常用、最早接触的分支,直接管理与用户控制台的交互。
类:
iostream
(继承自istream
和ostream
)预定义对象:
cin
: 一个istream
对象,用于标准输入。cout
: 一个ostream
对象,用于标准输出(缓冲)。cerr
: 一个ostream
对象,用于标准错误输出(无缓冲)。clog
: 一个ostream
对象,用于标准错误输出(带缓冲)。
第二分支:文件IO(文件流)
这个分支用于对磁盘文件进行读写操作。
类:
ifstream
(继承自istream
): 用于从文件读取数据。ofstream
(继承自ostream
): 用于向文件写入数据。fstream
(继承自iostream
): 用于同时对文件进行读写。
特点: 这些类除了继承来的流操作功能外,还增加了管理文件句柄、打开/关闭文件、查询文件状态等成员函数(如
open()
,close()
,is_open()
)。
示例:
#include <fstream>
#include <iostream>int main() {std::ifstream in_file("input.txt"); // 创建一个ifstream对象并打开文件int value;if (in_file) { // 检查文件是否成功打开in_file >> value; // 使用继承自istream的>>操作符从文件读取std::cout << "Read value: " << value << std::endl;} else {std::cerr << "Failed to open file!" << std::endl;}// in_file的析构函数会自动关闭文件return 0;
}
第三分支:字符串IO(字符串流)
这个分支允许我们将字符串作为一个流来处理,用于格式化字符串、数据转换等,非常强大。
类:
istringstream
(继承自istream
): 从字符串读取数据。ostringstream
(继承自ostream
): 向字符串写入数据。stringstream
(继承自iostream
): 用于同时对字符串进行读写。
特点: 除了流操作功能,还有管理底层字符串的成员函数(如
str()
用于获取或设置底层字符串)。
3. 总结与优势
这种继承家族的设计带来了巨大的好处:
接口一致性: 无论你是从控制台、文件还是字符串读取数据,你都使用相同的接口(
>>
,get()
,getline()
等)。这大大降低了学习成本和代码复杂度。你写的函数可以接受一个istream&
参数,它可以被传递给任何继承自istream
的对象(如cin
,ifstream
,istringstream
)。代码复用: 格式化、状态检查、错误处理等核心功能都在基类(如
basic_istream
)中实现。派生类只需专注于自己特有的操作(如文件管理、字符串管理),避免了代码重复。可扩展性: 这种设计是开放的。理论上,你可以通过继承这些基类来创建你自己的流类型(例如,一个用于网络通信的流)。
2. IO流状态
IO流状态是C++输入输出库中用于错误处理的核心机制。它通过一系列状态位来反映流当前的健康状况,程序可以通过检查这些状态位来决定如何继续执行。
四种状态位 (State Bits)
这些状态位是在 ios_base
类中定义的静态常量,其本质是位掩码 (bitmask),因此可以进行位运算组合。它们代表了流可能处于的四种核心状态:
状态位 | 含义 | 说明 |
---|---|---|
std::ios_base::goodbit | 良好状态 | 值为0,表示流未发生任何错误,一切正常。 |
std::ios_base::eofbit | 文件结束 | 表示流已经到达了输入序列的末尾(End-Of-File)。例如,从文件读取到最后,或者用户在控制台输入了EOF(Windows下Ctrl+Z,Unix下Ctrl+D)。 |
std::ios_base::failbit | 操作失败 | 表示上一次IO操作因为逻辑错误而失败,但流本身未被破坏。例如,试图将"hello" 读入一个int 变量,或者期望读取一个数字但遇到了文件结尾。流通常可以从此状态恢复。 |
std::ios_base::badbit | 流损坏 | 表示流发生了不可恢复的系统级错误或流缓冲区本身已损坏。例如,在写入时设备空间已满,或读取时流缓冲区断裂。一旦设置,流通常无法再使用。 |
状态检查与操作函数
流对象提供了一系列成员函数来检查和操作这些状态位。
函数 | 作用 |
---|---|
.good() | 如果流状态为 goodbit (即所有错误位均未设置),则返回 true 。 |
.eof() | 如果 eofbit 被设置,则返回 true 。 |
.fail() | 如果 failbit 或 badbit 被设置,则返回 true 。用于判断操作是否失败。 |
.bad() | 如果 badbit 被设置,则返回 true 。 |
.rdstate() | 返回当前流状态的完整位掩码。返回值是 iostate 类型,是上述状态位的组合。 |
.clear() | 重置流状态。无参调用时,将所有状态位重置为 goodbit 。可以传入一个状态位掩码(如 std::ios_base::failbit )来设置特定的状态。 |
.setstate(iostate state) | 设置指定的状态位。此操作是“附加”的,不会清除其他已设置的位。 |
重要关系:
if (stream)
或if (!stream)
: 这些条件检查本质上调用的是!stream.fail()
。如果流可用(即既没有failbit
也没有badbit
),则为true
。到达文件末尾(
eofbit
)通常也会同时设置failbit
,因为尝试在EOF之后进行读取是一个逻辑错误。
可以参考下图2-1和2-2进行理解。
2-1
2-2
一个常见的IO流错误是cin>>i, i是⼀个int类型的对象,如果我们在控制台输入一个字符,cin对象的failbit状态位就会被设置,cin就进入错误状态,一个流一旦发生错误,后续的IO操作都会失败,我们可以调用cin.clear()函数来恢复cin的状态为goodbit。
#include<iostream>
using namespace std;
int main()
{cout << cin.good() << endl;cout << cin.eof() << endl;cout << cin.bad() << endl;cout << cin.fail() << endl << endl;int i = 0;// 输入一个字符或多个字符,cin读取失败,流状态被标记为failbitcin >> i;cout << i << endl;cout << cin.good() << endl;cout << cin.eof() << endl;cout << cin.bad() << endl;cout << cin.fail() << endl << endl;if (cin.fail()){// clear可以恢复流状态位goodbitcin.clear();// 我们还要把缓冲区中的多个字符都读出来,读到数字停下来,否则再去cin>>i还是会失败char ch = cin.peek();while (!(ch >= '0' && ch <= '9')){ch = cin.get();cout << ch;ch = cin.peek();}cout << endl;}cout << cin.good() << endl;cout << cin.eof() << endl;cout << cin.bad() << endl;cout << cin.fail() << endl << endl;cin >> i;cout << i << endl;return 0;
}
运行结果:
状态转换与总结
图表2-2非常形象地展示了状态之间的转换关系。可以总结为:
初始状态:
goodbit
。成功操作: 状态保持
goodbit
。可恢复错误(如类型错误): 设置
failbit
。调用clear()
可以清除错误,使流回到goodbit
状态。不可恢复错误: 设置
badbit
(通常也会设置failbit
)。即使调用clear()
,底层问题可能依然存在,流可能无法真正恢复。到达文件尾: 设置
eofbit
(并通常同时设置failbit
)。调用clear()
可以清除eofbit
和failbit
,但为了再次读取,你通常还需要使用seekg()
等函数重置文件指针位置。
最佳实践:
在关键的IO操作之后,始终检查流状态(
if (stream)
或if (stream.fail())
)。使用
while (stream >> data)
或while (getline(stream, line))
这种模式来循环读取,直到失败(通常是EOF)。在尝试从
failbit
状态恢复时,记得先clear()
再ignore()
缓冲区的无效数据。区分
fail()
和bad()
:fail()
更常用作通用检查,而bad()
用于判断严重错误。
3. 管理输出缓冲区
缓冲区的作用
每个输出流(如cout、cerr、clog等)都维护着一个缓冲区(buffer),用于临时存储程序写入的数据。当执行类似os<<"hello world"
这样的输出操作时,字符串可能不会立即输出到目标设备,而是被存储在缓冲区中。这种机制允许操作系统将多个小的输出操作合并为单个较大的系统级写入操作。
性能优势:
- 设备I/O操作(如磁盘写入、网络传输等)通常耗时较长
- 通过缓冲机制,减少了实际的I/O操作次数
- 可以显著提高程序的整体性能
缓冲区刷新时机
1. 程序正常结束
当main()
函数执行return
语句或程序正常退出时,所有缓冲区会被自动刷新。
2. 缓冲区满
当缓冲区容量达到上限时,会自动刷新(具体缓冲区大小取决于实现)。
3. 使用操纵符显式刷新
endl
- 输出换行并刷新缓冲区
flush
- 只刷新缓冲区,不添加任何内容
ends
- 输出空字符并刷新(主要用于字符串流)
4. 使用unitbuf
操纵符
unitbuf
使流在每次输出操作后都自动刷新缓冲区。
cerr
默认设置了unitbuf
,因此所有输出到cerr
的内容都会立即刷新
5. 流关联(Tie)
流可以相互关联,当一个流进行I/O操作时,会触发其关联流的缓冲区刷新。
默认关联
cin
关联到cout
:读取cin
前会刷新cout
cerr
关联到cout
:写入cerr
前会刷新cout
因此,当写入cerr
时,从cin
读取时,都会导致cout
的缓冲区被刷新
void func(ostream& os)
{os << "hello world";os << "hello bit";// "hello world"和"hello bit"是否输出不确定system("pause");// 遇到endl,"hello world"和"hello bit"一定刷新缓冲区输出了os << endl;//os << flush;int i;cin >> i;os << "hello cat";// "hello cat"是否输出不确定system("pause");
}
int main()
{//ofstream ofs("test.txt");func(cout);// unitbuf设置后,ofs每次写都直接刷新// ofs << unitbuf;// cin绑定到ofs,cin进行读时,会刷新ofs的缓冲区// cin.tie(&ofs);//func(ofs);return 0;
}
运行结果:
我们使用VS2022运行,可以看到不管有没有刷新,内容都被输出到控制台上了(在pause前就已经打印输出了)
可以再看一下文件流的运行结果:
int main()
{ofstream ofs("test.txt");//func(cout);// unitbuf设置后,ofs每次写都直接刷新// ofs << unitbuf;// cin绑定到ofs,cin进行读时,会刷新ofs的缓冲区// cin.tie(&ofs);func(ofs);return 0;
}
刷新前:
endl刷新后:
最后一条内容func函数结束也还在缓冲区中没有刷新出来
直到程序return 0,才被刷新
int main()
{// 在io需求比较高的地方,如部分大量输入的竞赛题中,加上以下几行代码可以提高C++IO效率// 并且建议用'\n'替代endl,因为endl会刷新缓冲区// 关闭标准 C++ 流是否与标准 C 流在每次输入/输出操作后同步。ios_base::sync_with_stdio(false);// 关闭同步后,以下程序可能顺序为b a c// std::cout << "a\n";// std::printf("b\n");// std::cout << "c\n";// 解绑cin和cout关联绑定的其他流cin.tie(nullptr);cout.tie(nullptr);return 0;
}
缓冲区管理的最佳实践
关键信息使用立即刷新:对于错误消息或重要进度提示,使用
cerr
或显式刷新。批量操作减少刷新次数:对于大量输出,尽量减少不必要的刷新操作以提高性能。
谨慎使用
endl
:除非确实需要立即刷新,否则使用\n
代替endl
可以提高性能。理解流关联:了解默认的流关联行为,避免不必要的性能开销。
4. 标准IO流
C++标准IO流是C++标准库提供的输入输出功能,前面已经使用得比较多了。C++标准IO流默认是关联到控制台窗口的,通过标准输入输出设备进行数据交互。cin是istream类型的全局对象,负责处理标准输入;cout是ostream类型的全局对象,处理标准输出;cerr和clog也是ostream类型,但用于错误输出(cerr无缓冲,clog有缓冲)。
C++标准库提供了四个预定义的全局流对象,用于处理标准输入输出:
cin -
istream
类型的对象,用于标准输入(通常来自键盘)cout -
ostream
类型的对象,用于标准输出(通常显示在控制台)cerr -
ostream
类型的对象,用于标准错误输出(无缓冲)clog -
ostream
类型的对象,用于标准错误输出(带缓冲)
对于内置类型(如int、double、string等),这两个类都直接实现了运算符重载,所以可以直接使用<<和>>运算符进行输入输出。而对于自定义类型(如自定义的类或结构体),则需要我们重载<<和>>运算符才能实现类似的输入输出功能。
核心机制解析
不可拷贝性:
istream
/ostream
禁止拷贝构造和赋值(拷贝构造函数为protected
):ostream os1(cout); // 错误:拷贝构造函数不可访问 ostream os2 = cout; // 错误:赋值操作被禁用
正确用法:始终通过引用传递流对象:
void logData(ostream& os) { os << "Data: " << 42; }
流状态到
bool
的隐式转换:istream
对象(如cin
)可转换为bool
值,用于条件判断:int value; while (cin >> value) { // 等价于 !cin.fail() && !cin.bad()// 读取成功时执行 }
转换规则:
- 状态为
goodbit
或eofbit
→true
- 状态为
failbit
或badbit
→false
- 状态为
自定义类型支持:
- 需重载
<<
和>>
运算符:struct Point {int x, y; };// 输出重载 ostream& operator<<(ostream& os, const Point& p) {return os << "(" << p.x << "," << p.y << ")"; }// 输入重载 istream& operator>>(istream& is, Point& p) {char dummy;return is >> dummy >> p.x >> dummy >> p.y >> dummy; }// 使用 Point p; cin >> p; // 输入格式: (10,20) cout << p; // 输出: (10,20)
- 需重载
ostream和istream类还提供了许多其他接口,如:
tellg()/tellp()
:获取当前流位置seekg()/seekp()
:设置流位置clear()
:清除错误状态rdstate()
:获取当前流状态fill()
:设置填充字符precision()
:设置浮点数精度等
这些接口在文件操作或需要精细控制输入输出时很有用,但在日常控制台输入输出中相对⽤得⽐较少。当需要使用时,可以查阅C++标准库文档或相关参考资料。
5. 文件IO流
C++文件IO流主要通过三个类来实现:
ofstream
- 输出文件流(写文件),继承自ostream
ifstream
- 输入文件流(读文件),继承自istream
fstream
- 输入输出文件流(读写文件),继承自iostream
类名 | 继承关系 | 功能 | 典型场景 |
---|---|---|---|
ofstream | 派生自 ostream | 写文件(内存数据 → 硬盘) | 创建/覆盖文件、追加数据 |
ifstream | 派生自 istream | 读文件(硬盘数据 → 内存) | 读取文本/二进制文件 |
fstream | 派生自 iostream | 读写双向操作 | 需同时读写的文件(如日志更新) |
关键特性 :
- 支持运算符重载(
<<
、>>
),简化基础类型和自定义类型的读写。- 析构时自动关闭文件(无需手动调用
close()
),但显式关闭可避免资源占用过长。- 不可拷贝(拷贝构造函数为
protected
),需通过引用传递。
文件打开模式(openmode
)详解
打开模式是ios_base
中定义的位掩码常量,可通过按位或组合使用:
模式 | 值(位) | 作用 | 组合示例 |
---|---|---|---|
ios::in | 0x01 | 以读方式打开(ifstream 默认) | ios::in | ios::binary |
ios::out | 0x02 | 以写方式打开(默认清空文件内容,ofstream 默认) | ios::out | ios::app |
ios::binary | 0x04 | 二进制模式(避免文本转换,如\n →\r\n ) | ios::in | ios::binary |
ios::ate | 0x08 | 打开后定位到文件末尾(可移动指针) | ios::out | ios::ate |
ios::app | 0x10 | 追加模式(禁止移动指针,写操作始终在末尾) | ios::out | ios::app |
ios::trunc | 0x20 | 清空文件内容(若与ios::out 组合,显式强调截断行为) | ios::out | ios::trunc |
关键区别 :
app
vsate
:
app
:写操作强制追加,无法用seekp()
移动指针(如银行日志防篡改)。ate
:打开时指针在末尾,但可自由移动(如修改文件中间数据)。out
vsout|trunc
:
ios::out
默认清空内容,trunc
显式强调该行为(增强代码可读性)。
文件流在打开或读写失败时,也会设置IO流状态标记,我们可以通过调用operator bool或operator!来进行判断。
读写操作接口
写入操作(ofstream
)
方法 | 用途 | 示例 |
---|---|---|
<< | 写入格式化数据(类型安全) | ofs << x << " " << y; |
put(char) | 写入单个字符 | ofs.put('A'); |
write() | 写入二进制数据块 | ofs.write(buffer, sizeof(buffer)); |
读取操作(ifstream
)
方法 | 用途 | 示例 |
---|---|---|
>> | 读取格式化数据 | ifs >> x >> y; |
get(char&) | 读取单个字符 | char c; ifs.get(c); |
getline() | 读取一行文本 | string line; getline(ifs, line); |
read() | 读取二进制数据块 | ifs.read(buffer, 100); |
文件流操作中,ifstream读取数据可使用get()、read()或>>运算符重载,而ofstream写入数据则常用put()、write()或<<运算符重载。具体用法请参考以下示例代码:
• 相⽐c语⾔⽂件读写的接⼝,C++fstream流功能更强⼤⽅便,使⽤<<和>>进⾏⽂件读写很⽅便,尤其是针对⾃定义类型对象的读写。

5-1
int main()
{ofstream ofs("test.txt");// 字符和字符串的写ofs.put('x');ofs.write("hello\nworld", 11);// 使用<<进行写ofs << "22222222" << endl;int x = 111;double y = 1.11;ofs << x << endl;ofs << y << endl;ofs.close();// app和ate都是尾部追加,不同的是app不能移动文件指针,永远是在文件尾写// ate可以移动文件指针,写到其他位置ofs.open("test.txt", ios_base::out | ios_base::app);ofs << "1111111" << endl;ofs.seekp(0, ios_base::beg);ofs << x << " " << y << endl;ofs.close();ofs.open("test.txt", ios_base::out | ios_base::ate);ofs << "1111111" << endl;ofs.seekp(0, ios_base::beg);ofs << x << " " << y << endl;ofs.close();// out和 out|trunc都会先把数据清掉,再写数据(官方文档也明确是这样写的)// https://en.cppreference.com/w/cpp/io/basic_filebuf/open// 那么trunc存在的意义是什么呢?out|trunc更明确的表达了文件中有内容时要清除掉内容// 对于代码维护者和阅读者来说能清晰地理解这个行为,在一些复杂的文件系统环境或不同的// C++文件流实现库中,out行为不完全等同于截断内容的情况(虽然当前主流实现基本一致),// out|trunc更明确的表达要清除内容的行为ofs.open("test.txt", ios_base::out);//ofs.open("test.txt", ios_base::out | ios_base::trunc);ofs << "xxxx";ofs.close();return 0;
}
运行结果:
使用put,write将数据向test.txt文件写入时:
以追加的方式打开文件写入数据时,尽管使用seekp移动文件指针到0处,任然还是在文件尾写入,所以以app方式打开文件时不能移动文件指针
当我们使用ate方式打开文件时:
发现之前的内容都被清空,那是因为out默认会清空文件内容,那为什么以out | app却没有清空文件内容呢?
这是由于app是要强制在文件末尾追加内容,不可移动文件指针,说明我们肯定都要在原来文件内容的后面追加写入内容了,所以不会将内容清空,但ate虽然会定位到文件末尾,但是文件指针是可以移动的,说明数据是可以修改的,然后out又默认清空文件内容,所以会看到之前的内容被清空
我们后续再向文件流缓冲区中写入字符串1111111,同时endl刷新到文件中,然后将文件指针移动到开头0处,再次写入x和y时,它就会从开头开始往后写,将之前的1111111覆盖,如下图:
最后我们只以out方式打开文件:
证明文件内容确实会被清空,同时我们写入的xxxx还在缓冲区中没有被刷新到文件里。
最后return时,被刷新出来
我们可以再来两个示例:
示例一:演示如何使用C++的文件IO流进行二进制和文本格式的数据读写,将结构化数据(服务器配置信息)以两种不同的方式存储到文件中,然后再读取出来。
class Date
{friend ostream& operator << (ostream& out, const Date& d);friend istream& operator >> (istream& in, Date& d);
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
istream& operator >> (istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}
ostream& operator << (ostream& out, const Date& d)
{out << d._year << " " << d._month << " " << d._day << endl;return out;
}
struct ServerInfo
{// 二进制读写时,这里不能用string,否则写到文件中的是string中指向字符数组的指针// 若string对象析构后再去文件中读取string对象,string中读到是一个野指针。char _address[32];//string _address;int _port;Date _date;
};
struct ConfigManager
{
public:ConfigManager(const char* filename):_filename(filename){}// 二进制写// 内存中怎么存,囫囵吞枣,就怎么直接写出去void WriteBin(const ServerInfo& info){ofstream ofs(_filename, ios_base::out | ios_base::binary);ofs.write((const char*)&info, sizeof(info));}// 二进制读// 将文件中的内容直接囫囵吞枣,直接读到内存中void ReadBin(ServerInfo& info){ifstream ifs(_filename, ios_base::in | ios_base::binary);ifs.read((char*)&info, sizeof(info));}void WriteText(const ServerInfo& info){ofstream ofs(_filename);ofs << info._address << " " << info._port << " " << info._date;}void ReadText(ServerInfo& info){ifstream ifs(_filename);ifs >> info._address >> info._port >> info._date;}
private:string _filename; // 配置文件
};
void WriteBin()
{ServerInfo winfo = { "192.0.0.1111111111111111111111", 80, { 2025, 1, 10 }};// 二进制读写ConfigManager cf_bin("test.bin");cf_bin.WriteBin(winfo);
}
void ReadBin()
{// 二进制读写ConfigManager cf_bin("test.bin");ServerInfo rbinfo;cf_bin.ReadBin(rbinfo);cout << rbinfo._address << " " << rbinfo._port << " " << rbinfo._date <<endl;
}
void WriteText()
{ServerInfo winfo = { "192.0.0.1", 80, { 2025, 1, 10 } };// 文本读写ConfigManager cf_text("test.txt");cf_text.WriteText(winfo);
}
void ReadText()
{ConfigManager cf_text("test.txt");ServerInfo rtinfo;cf_text.ReadText(rtinfo);cout << rtinfo._address << " " << rtinfo._port << " " << rtinfo._date << endl;
}
int main()
{WriteBin();ReadBin();WriteText();ReadText();return 0;
}
代码主要结构:
1. 数据结构定义
Date
类:表示日期,包含年、月、日三个成员变量,并重载了<<
和>>
运算符以便于输入输出。ServerInfo
结构体:表示服务器配置信息,包含地址、端口和日期信息。ConfigManager
类:负责管理配置文件的读写操作。
2. 二进制文件读写演示
WriteBin()
函数:将ServerInfo
对象以二进制格式写入文件。这种方式直接内存拷贝,效率高但不具备可读性。ReadBin()
函数:从二进制文件中读取数据并重建ServerInfo
对象。
3. 文本文件读写演示
WriteText()
函数:将ServerInfo
对象以文本格式写入文件。这种方式可读性好但效率较低。ReadText()
函数:从文本文件中读取数据并重建ServerInfo
对象。
运行结果:
示例二:实现一个图片文件的复制,需要用二进制方式打开读写
int main()
{// 实现一个图片文件的复制,需要用二进制方式打开读写,第一个参数可以给文件的绝对路径ifstream ifs("C:\\Users\\x\\Desktop\\股票问题dp.png",ios_base::in | ios_base::binary);ofstream ofs("C:\\Users\\x\\Desktop\\股票问题dp-copy.png",ios_base::out | ios_base::binary);int n = 0;while (ifs && ofs){char ch = ifs.get();ofs << ch;++n;}cout << n << endl;return 0;
}
运行结果:
可以看到写入324484次,也成功将图片文件复制下来了
6. string IO流
C++提供了三种字符串流类,都在<sstream>
头文件中定义:
ostringstream
- 输出字符串流(向字符串写入数据),继承自ostream
istringstream
- 输入字符串流(从字符串读取数据),继承自istream
stringstream
- 输入输出字符串流(既可读又可写),继承自iostream
类名 | 继承关系 | 功能 | 典型场景 |
---|---|---|---|
ostringstream | 派生自 ostream | 向字符串写入数据(内存输出流) | 数据序列化、字符串拼接 |
istringstream | 派生自 istream | 从字符串读取数据(内存输入流) | 数据解析、字符串拆分 |
stringstream | 派生自 iostream | 双向读写操作(最常用) | 复杂字符串转换与处理 |
关键特性:
- 底层维护
std::string
对象存储数据,通过str()
访问或修改 。- 支持与文件流/控制台流相同的接口(
<<
、>>
、getline
),实现代码复用 。- 避免手动类型转换(如
atoi
),提供类型安全的操作 。
核心操作与底层机制
(1) 数据读写:<<
与 >>
运算符
写入数据(
ostringstream
/stringstream
):std::stringstream ss; ss << "Value: " << 42 << " | " << 3.14; // 拼接字符串和数字
读取数据(
istringstream
/stringstream
):int val; double pi; std::string prefix; ss >> prefix >> val; // 解析为 "Value:", 42 ss.ignore(2); // 跳过 "| " ss >> pi; // 解析为 3.14
(2) 底层字符串管理:str()
函数
方法 | 作用 | 示例 |
---|---|---|
std::string str() const | 获取底层字符串副本 | std::string content = ss.str(); |
void str(const string& s) | 重置底层字符串(清空流) | ss.str(""); // 清空流 |
注:直接操作底层字符串需谨慎,可能破坏流状态 。
class Date
{friend ostream& operator << (ostream& out, const Date& d);friend istream& operator >> (istream& in, Date& d);
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
istream& operator >> (istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}
ostream& operator << (ostream& out, const Date& d)
{out << d._year << " " << d._month << " " << d._day << endl;return out;
}int main()
{int i = 123;Date d = { 2025, 4, 10 };ostringstream oss;oss << i << endl;oss << d << endl;string s = oss.str();cout << s << endl;//stringstream iss(s);//stringstream iss;//iss.str("100 2025 9 9");istringstream iss("100 2025 9 9");int j;Date x;iss >> j >> x;cout << j << endl;cout << x << endl;int a = 1234;int b = 5678;string str;// 将一个整形变量转化为字符串,存储到string类对象中stringstream ss;ss << a << " " << b;ss >> str;cout << str << endl;cout << ss.fail() << endl;cout << ss.bad() << endl;// 注意多次转换时,必须使用clear将上次转换状态清空掉// stringstreams在转换结尾时(即最后一个转换后),会将其内部状态设置为badbit和failbit// 因此下一次转换是必须调用clear()将状态重置为goodbit才可以转换// 但是clear()不会将stringstreams底层字符串清空掉,str给一个空串可以清掉底层的字符串ss.clear();ss.str("");double dd = 12.34;ss << dd;ss >> str;cout << str << endl;return 0;
}
运行结果:
我们也可以来一个示例:
如何使用C++的字符串流(stringstream)进行数据序列化和反序列化,特别是在网络通信或数据存储场景中的应用。
class Date
{friend ostream& operator << (ostream& out, const Date& d);friend istream& operator >> (istream& in, Date& d);
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
istream& operator >> (istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}
ostream& operator << (ostream& out, const Date& d)
{out << d._year << " " << d._month << " " << d._day << endl;return out;
}struct ChatInfo
{string _name; // 名字int _id; // idDate _date; // 时间string _msg; // 聊天信息
};
int main()
{// 结构信息序列化为字符串ChatInfo winfo = { "张三", 135246, { 2022, 4, 10 }, "晚上⼀起看电影吧" };ostringstream oss;oss << winfo._name << " " << winfo._id << " " << winfo._date << " " <<winfo._msg;string str = oss.str();cout << str << endl << endl;// 我们通过网络这个字符串发送给对象,实际开发中,信息相对更复杂,// 一般会选用Json、xml等方式进行更好的支持// 字符串解析成结构信息ChatInfo rInfo;istringstream iss(str);iss >> rInfo._name >> rInfo._id >> rInfo._date >> rInfo._msg;cout << "-------------------------------------------------------" << endl;cout << "姓名:" << rInfo._name << "(" << rInfo._id << ") ";cout << rInfo._date << endl;cout << rInfo._name << ":>" << rInfo._msg << endl;cout << "-------------------------------------------------------" << endl;return 0;
}
代码的主要功能和目的:
1. 数据结构定义
Date
类:表示日期,包含年、月、日三个成员变量,并重载了<<
和>>
运算符以便于序列化和反序列化。ChatInfo
结构体:表示聊天信息,包含发送者姓名、ID、发送时间和消息内容。
2. 序列化过程(对象 → 字符串)
创建一个
ChatInfo
对象并初始化其各个字段使用
ostringstream
将对象的各个字段转换为一个格式化的字符串字段之间使用空格分隔,形成可传输或存储的字符串格式
3. 反序列化过程(字符串 → 对象)
使用
istringstream
从格式化的字符串中提取数据按照序列化时的顺序和格式,将字符串解析为各个字段
重建原始的
ChatInfo
对象
运行结果:
局限性:
如代码注释所述,这种简单的空格分隔方法在实际开发中有限制:
如果数据本身包含空格,会导致解析错误
缺乏标准化的错误处理和验证机制
不支持嵌套复杂数据结构
可扩展性较差
因此,在实际项目中,通常会使用更强大的序列化库或格式(如Protocol Buffers、JSON、XML等)。