【C++】C++的IO流--详解

一.C语言的输入与输出

C 语言最常用的输入输出函数是 scanf()printf()。

  • scanf():从键盘读取标准输入,把读取到的数据存入对应变量。
  • printf():把文本、数据输出到屏幕。

这里要注意掌握输出的宽度控制与精度控制。

同时C语言借助了缓冲区进行输入与输出,如下图所示:

对输入输出缓冲区的理解

  • 缓冲区可以屏蔽底层 I/O 的实现细节。底层 I/O 依赖操作系统内核,屏蔽这部分差异后,写出来的程序具备更好的可移植性。
  • 借助缓冲区还能实现按行读取。计算机本身并没有 "行" 的概念,依靠缓冲区,我们可以定义出行的逻辑,解析缓冲区内的数据,完成一行内容的读取返回。

二.流是什么

对 C++ 流的理解

流,顾名思义就是流动,代表数据从一个地方流向另一个地方,是对有序、连续、带方向数据的抽象,数据单位可以是比特、字节或者数据包

C++ 里的流,用来描述信息的输入输出过程:数据从键盘这类外部设备流向内存,或是内存中的数据输出到显示器等外部设备,这个过程就被形象称作流。

流具备两个特点:有序连续、带有方向

C++ 提供了I/O 标准类库来实现这套数据流动,库中每一个类都叫做流类,分别负责不同的输入输出功能


三.C++IO流

如下图所示,C++ 系统实现了一个庞大的类库,其中 ios 为基类,其他类都是直接或间接派生自 ios 类。

C++ 流类继承结构图简要说明

最顶层基类ios_base --> ios,提供流的状态、格式控制等基础功能。streambuf:负责缓冲区管理(数据真正存放的地方)。

输入 / 输出流基类

istream:输入流(读数据),cin就是它的对象

ostream:输出流(写数据),cout/cerr/clog是它的对象

iostream:多重继承istream+ostream,同时支持读写

三类不同用途的流(右侧)

<iostream>:标准控制台流,键盘输入、屏幕输出(cin/cout)

<fstream>:文件流

ifstream 文件读;ofstream 文件写;fstream 文件读写;filebuf 文件缓冲区

<sstream>:字符串流,把字符串当作流使用

istringstream 读字符串;ostringstream 写字符串;stringstream 读写;stringbuf 字符串缓冲区

简单总结:所有流都继承自顶层基类,分为控制台、文件、字符串 3 种流,istream 负责读,ostream 负责写,streambuf 管理缓冲区。


1.C++标准IO流

C++ 标准库提供了四个常用的全局流对象:cin、cout、cerr 和 clog。

cout 用于标准输出,将数据从程序输出到控制台(显示器)。

cin 用于标准输入,通过键盘将数据输入到程序中。

cerr 专门用于输出标准错误信息。

clog 主要用于输出日志信息。

从类继承关系来看,cout、cerr 和 clog 都是 ostream 类的实例。虽然它们都用于输出,但设计上存在细微差异:cout 和 clog 是缓冲输出,而 cerr 是非缓冲输出,确保错误信息能立即显示。使用这些流对象时,需要包含相应的头文件(通常是 <iostream>)并引入 std 命名空间。

关于 cin 的几点注意事项:

1.缓冲特性:cin 是缓冲流。键盘输入的数据会先存储在缓冲区中,程序提取数据时直接从缓冲区读取。输入过多的数据会保留在缓冲区中供后续使用。

2.错误处理:如果输入的数据类型与程序期望的类型不匹配,会导致流状态错误,但程序不会立即终止,而是需要程序员手动检查和处理流状态。

3.分隔符:空格和回车符默认作为数据分隔符,因此多个数据可以在同一行或不同行输入。但需要注意,cin 默认会跳过空格,无法直接读取空格字符,字符串中也不能包含空格。

4.cin 和 cout 能够直接处理内置数据类型(如 int、float、char 等),是因为标准库已经为这些类型这些类型重载了 <<>> 运算符。

5**.C++ 里自定义的结构体 / 类,直接用cin、cout读写会报错。 想要写成 cin >> 对象、cout << 对象,必须重载>>和<<运算符。**

cpp 复制代码
#include <iostream>
using namespace std;

//自定义类型
struct Student
{
    int id;
    string name;
};

//重载输出运算符 <<
ostream& operator<<(ostream& os, const Student& s)
{
    os << s.id << " " << s.name;
    return os;
}

//重载输入运算符 >>
istream& operator>>(istream& is, Student& s)
{
    is >> s.id >> s.name;
    return is;
}

int main()
{
    Student st;
    //直接用cin、cout读写自定义对象
    cin >> st;
    cout << st << endl;
    return 0;
}

6.OJ 在线判题里 C++ 输入输出:

做算法题提交到在线 OJ 的时候,输入输出是很容易踩坑丢分的地方,很多人代码逻辑没问题,就是输入写法或者输出空格、换行不对,直接判成答案错误 WA。这里把循环输入的几种写法、适用场景、常见 bug 和细节全部整理清楚。

先说最重要的一个概念:EOF。在线 OJ 会一次性把所有测试数据给到程序,程序读到数据末尾没有内容了,就是 EOF,循环条件cin>>变量在读到 EOF 的时候返回 false,循环自动结束。 本地用 VS 调试的时候,键盘按下 Ctrl+Z,就相当于手动模拟 EOF,程序终止循环。在线提交不用自己加结束标记,系统自动处理。还有一个硬性要求:输出格式必须严格和题目描述一模一样,多一个空格、少一个空格,多换行或者末尾多出空行,都会 WA,这点一定要留心

6.1.循环不断读取单个元素(数字、不带空格的字符串)

原始写法:

cpp 复制代码
while(cin>>a)
{
    //处理代码
}

**原理:**cin >> a会自动跳过所有空格、换行、Tab,只抓取有效数据。只要成功读到数据,循环继续;读到末尾 EOF,循环停止。 适用场景:题目有多组测试样例,没有先给出一共有多少组,每组只输入一个数。 注意:这种写法只能拿到一个个单独的数据,不能读取一整行,遇到空格就停下。


6.2.循环一次读取多个元素

先看这个写法,while(c>>a>>b>>c),它是是错误的。

问题在哪:输入流名字是 c,后面又定义了变量 c,名字冲突,编译直接报错。 正确写法:

cpp 复制代码
int a,b,c;
while(cin>>a>>b>>c)
{
    //处理每组的a、b、c
}

一次循环读取一组里的多个数据,数据之间换行或者空格隔开都没关系,cin 自动忽略空白。 适用场景:多组样例,每组给出好几个数字,不知道一共有多少组,读到输入结束为止。

补充区分另一种题型:题目第一行先输入 n,代表一共有 n 组测试数据。这种情况不能用 while 循环读 EOF,要用 for 循环固定循环 n 次。 示例如下:

cpp 复制代码
int n,a,b;
cin>>n;
for(int i=1;i<=n;i++)
{
    cin>>a>>b;
}

我们大家有时候两种场景写混,直接造成超时或者读取数据错乱。


6.3.读取一整行数据(重点,极易混淆)

cin>>str只要碰到空格就停止读取,只能获取一个单词,根本读不了一整行,如果一行里面包含空格,用它会读不全。想要读取完整一行(保留行里面的空格),要用 getline 函数:

cpp 复制代码
string line;
while(getline(cin,line))
{
    //line保存完整一行内容
}

getline 会从当前位置读到换行符为止,换行本身不会存入字符串,读完一行继续下一行,直到全部输入结束。

这里举个非常经典的坑:cin 和 getline 混用

如果前面代码用过cin >> x;,输入完成按下回车,回车对应的换行符会留在输入缓冲区里。紧接着执行 getline,它会直接读到这个残留换行,得到一个空字符串。 举个出错例子:

cpp 复制代码
int n;
cin>>n;
string s;
getline(cin,s); //这里直接读到空行!

解决办法,读取完数字之后,用 ignore 把缓冲区剩下的换行清理掉:

cpp 复制代码
int n;
cin>>n;
cin.ignore(); //吃掉换行符
string s;
getline(cin,s);

6.4.两种输入模板怎么选,简单分清

数据是一个个数字、单词,数据之间用空格换行隔开,不需要接收带空格的整行 → 使用while(cin>>变量)

需要读取完整一行,一行内部带有空格,要把整行全部拿下来处理 → 使用while(getline(cin,line)) 不要盲目套用模板,看题目输入格式决定


6.5.OJ 输出的一些实战小细节

尽量少用 endl。endl 每次输出都会刷新缓冲区,数据量大的时候刷新太频繁,容易超时。日常优先写 '\n' 换行,速度更快。

多组样例输出,每组答案换行。警惕最后一组答案后面多输出一个换行,部分 OJ 判题严格,多余空行直接 WA。

输出空格:题目要求两个数字中间空格隔开,不要在每行最后额外多加一个空格,很多人栽在这里。


6.6本地调试和在线提交的区别

VS 本地测试时,输入完数据按 Ctrl+Z 结束循环;代码提交到 OJ 上运行,不用手动输入 Ctrl+Z,系统读到文件末尾自动触发 EOF 退出循环,代码不用做任何修改。


7.istream 类型对象转换为逻辑条件判断值

cpp 复制代码
istream& operator>> (int& val);
explicit operator bool() const;

https://cplusplus.com/reference/istream/istream/operator%3E%3E/

https://cplusplus.com/reference/ios/ios/operator_bool/

实际上我们看到使用 while(cin>>i) 去流中提取对象数据时,调用的是 operator>>,返回值是 istream 类型的对象。 这里能作为逻辑判断条件,根源在于 istream 对象自带 operator bool。 当流读取失败或者读到结束标志 EOF 时,istream的operator bool返回 false,循环终止。

cpp 复制代码
#include <iostream>
using namespace std;
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 << '\n';
    return out;
}

int main()
{
    Date d;
    //标准写法:while判断cin流状态,调用istream自带operator bool()
    //读到EOF、输入格式错误自动退出,OJ多组输入首选
    while (cin >> d)
    {
        cout << d;
    }
    return 0;
}

2.C++文件IO流

C++ 根据文件内容的数据格式分为二进制文件和文本文件。采用文件流对象操作文件的一般步骤:

(1)定义一个文件流对象。

  • ifstream ifile(只输入用,读取文件)
  • ofstream ofile(只输出用,写入文件)
  • fstream iofile(既输入又输出用)

(2)使用文件流对象的成员函数 open 打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系。 补充:打开后最好判断打开是否成功。

(3)使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写。 文本文件多用 >> <<;二进制文件用 read、write 成员函数。

(4)关闭文件,调用 close ()。

补充知识点:

1.常用打开模式

  • ios::in 只读打开
  • ios::out 只写打开,会清空原有内容
  • ios::app 在文件末尾追加写入,不覆盖旧数据
  • ios::binary 二进制方式打开
  • 多种模式可以用 | 组合

2.易错识别

① 默认是文本模式;二进制读写必须带上 ios::binary

② ofstream 默认打开会清空文件,如果想要追加内容,不能直接使用默认模式

③ while (ifile>> x) 读到 EOF 自动结束,原理和 while (cin >> x) 保持一致

④ 文件流对象销毁时会自动关闭文件,从代码规范角度,建议手动调用 close ()

简单写个小Demo

cpp 复制代码
#include <fstream>
#include <iostream>
using namespace std;

int main()
{
    ifstream ifile;
    ifile.open("test.txt");

    if (!ifile) // 文件不存在,创建并写入一些默认数据
    {
        cout << "文件不存在,正在创建..." << endl;
        ofstream ofile("test.txt");
        if (ofile.is_open())
        {
            ofile << "10 20 30 40 50" << endl;
            ofile.close();
            cout << "文件已创建,请重新运行程序读取数据。" << endl;
        }
        else
        {
            cout << "创建文件失败!" << endl;
            return 1;
        }
        return 0; // 创建后退出,下次运行即可读取
    }

    int a;
    while (ifile >> a)
    {
        cout << a << " "; // 处理读取的数据
    }
    cout << endl;

    ifile.close();
    return 0;
}

创建文件:我在里面写了几个数字

再看一个比较难的代码:

cpp 复制代码
#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
using namespace std;

//  Date 类 
class Date
{
public:
    int _year;
    int _month;
    int _day;

    Date() : _year(0), _month(0), _day(0) {}
    Date(int year, int month, int day) : _year(year), _month(month), _day(day) {}

    // 友元声明:让外部函数能访问私有成员(如果成员是私有的话)
    // 这里成员是 public,友元不是必须的,但为了演示规范写法保留
    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);
};

//流插入运算符重载(输出)
ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << "-" << d._month << "-" << d._day;
    return out;
}

//流提取运算符重载(输入)
istream& operator>>(istream& in, Date& d)
{
    // 支持格式:2023-11-10 或 2023 11 10
    char ch1, ch2;
    in >> d._year >> ch1 >> d._month >> ch2 >> d._day;

    // 如果分隔符不是 '-',尝试用空格分隔的方式
    if (ch1 != '-' || ch2 != '-')
    {
        // 回退到用空格分隔
        in.clear();  // 清除错误状态
        in >> d._year >> d._month >> d._day;
    }
    return in;
}
struct ServerInfo
{
    char _address[32];
    int _port;
    Date _date;

    // 默认构造函数
    ServerInfo() : _port(0)
    {
        memset(_address, 0, sizeof(_address));
    }

    // 带参构造函数
    ServerInfo(const char* address, int port, const Date& date)
        : _port(port), _date(date)
    {
        strcpy_s(_address, sizeof(_address), address);
    }
};

// ConfigManager 配置管理类 
class ConfigManager
{
public:
    ConfigManager(const char* filename)
        : _filename(filename)
    {
    }

    // 二进制写入
    void WriteBin(const ServerInfo& info)
    {
        ofstream ofs(_filename, ios_base::out | ios_base::binary);
        if (!ofs.is_open())
        {
            cerr << "二进制文件打开失败(写入): " << _filename << endl;
            return;
        }
        ofs.write((const char*)&info, sizeof(info));
        ofs.close();
        cout << "二进制写入成功: " << _filename << endl;
    }

    //二进制读取
    void ReadBin(ServerInfo& info)
    {
        ifstream ifs(_filename, ios_base::in | ios_base::binary);
        if (!ifs.is_open())
        {
            cerr << "二进制文件打开失败(读取): " << _filename << endl;
            return;
        }
        ifs.read((char*)&info, sizeof(info));
        ifs.close();
        cout << "二进制读取成功: " << _filename << endl;
    }

    // 文本写入 
    void WriteText(const ServerInfo& info)
    {
        ofstream ofs(_filename);
        if (!ofs.is_open())
        {
            cerr << "文本文件打开失败(写入): " << _filename << endl;
            return;
        }
        // C++ 流操作符优势:对内置类型和自定义类型使用相同方式
        ofs << info._address << " " << info._port << " " << info._date;
        ofs.close();
        cout << "文本写入成功: " << _filename << endl;
    }

    //  文本读取
    void ReadText(ServerInfo& info)
    {
        ifstream ifs(_filename);
        if (!ifs.is_open())
        {
            cerr << "文本文件打开失败(读取): " << _filename << endl;
            return;
        }
        ifs >> info._address >> info._port >> info._date;
        ifs.close();
        cout << "文本读取成功: " << _filename << endl;
    }

private:
    string _filename;  // 配置文件名
};

// 主函数
int main()
{
    cout << "开始测试 " << endl << endl;

    // 创建服务器信息对象
    ServerInfo winfo("192.0.0.1", 80, Date(2026, 9, 5));

    //测试二进制读写
    cout << "二进制读写测试 " << endl;
    ConfigManager cf_bin("test.bin");
    cf_bin.WriteBin(winfo);

    ServerInfo rbinfo;
    cf_bin.ReadBin(rbinfo);
    cout << "读取结果: " << rbinfo._address << " "
        << rbinfo._port << " " << rbinfo._date << endl << endl;
    // 测试文本读写 
    cout << "文本读写测试" << endl;
    ConfigManager cf_text("test.txt");  
    cf_text.WriteText(winfo);

    ServerInfo rtinfo;
    cf_text.ReadText(rtinfo);
    cout << "读取结果: " << rtinfo._address << " "
        << rtinfo._port << " " << rtinfo._date << endl;

    cout << endl << " 测试完成" << endl;

    return 0;
}

1. 为什么 Date 必须重载 <<>>

因为 ofs << info._date 这种写法,编译器只认识内置类型。Date 是自定义类型,必须告诉它怎么输入输出。

2. 为什么重载函数必须是全局的?

cpp 复制代码
cout << d;  // 等价于 operator<<(cout, d)

左操作数是 cout(ostream 类型),不是 Date 对象。所以函数不能是 Date 的成员,必须是全局函数。

3. 二进制 vs 文本

方式 核心操作 优点 缺点
二进制 write/read 整体内存拷贝 速度快,代码简单 文件不可读,跨平台有隐患
文本 << >> 逐字段转换 人类可读,方便调试 速度慢,需要重载运算符

4. 这代码的亮点

统一写法:不管 int、char\[\] 还是 Date,都用同样的 << 和 >> 操作。

cpp 复制代码
ofs << info._address << " " << info._port << " " << info._date;

这就是运算符重载的价值------让自定义类型用起来像内置类型。

5. 二进制写入的坑

cpp 复制代码
ofs.write((const char*)&info, sizeof(info));

只是把内存字节原样拷贝。如果结构体里有指针 ,存的是地址,下次读回来地址失效。所以二进制读写只适合全是值类型的结构体


简单小结:想让自定义类型跟内置类型一样用流操作,就得给它实现 << 和 >>。二进制快但不可读,文本慢但方便,按需选择。


四.stringstream的简单介绍

在 C 语言中,如果想要将一个整形变量的数据转化为字符串格式,如何去做?

两种方法:

  1. 使用 itoa() 函数
  2. 使用 sprintf() 函数

来看代码:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;

int main()
{
    int n = 123456789;
    
    // 方法1:使用 _itoa(Windows 特有)
    char s1[32];
    _itoa_s(n, s1, sizeof(s1), 10);  // _itoa_s 更安全,需要指定缓冲区大小
    cout << "_itoa: " << s1 << endl;
    
    // 方法2:使用 sprintf(标准 C 函数)
    char s2[32];
    sprintf_s(s2, sizeof(s2), "%d", n);  // sprintf_s 防止缓冲区溢出
    cout << "sprintf %%d: " << s2 << endl;
    
    // 问题演示:格式不匹配
    char s3[32];
    sprintf_s(s3, sizeof(s3), "%f", n);  // 用 %f 格式化整数,得到错误结果
    cout << "sprintf %%f (错误): " << s3 << endl;
    
    return 0;
}

_itoa vs sprintf

函数 优点 缺点 跨平台
_itoa 专用于整数转字符串 Windows 专用,不是标准 C 不可以
sprintf 标准 C 函数,格式灵活 需要正确匹配格式符 可以

C++ 里可以借助stringstream 类对象来处理这类问题

想要使用 stringstream,必须引入对应的头文件。在这个头文件中,标准库提供了三个类: istringstream、ostringstream、stringstream,分别实现流输入、流输出、同时输入输出。下面重点讲解 stringstream。


stringstream 主要可以用来:

a.将数值类型数据格式化为字符串

来看代码:

cpp 复制代码
#include <sstream>
#include <string>
#include <iostream>
using namespace std;

int main()
{
    int a = 12345678;
    string sa;
    
    // 1. int 转 string
    stringstream ss;
    ss << a;
    ss >> sa;
    cout << "int转string: " << sa << endl;
    
    // 2. 转换下一个(注意清空步骤)
    ss.clear();      // 先清状态标志
    ss.str("");      // 再清内容
    
    double d = 12.34;
    ss << d;
    ss >> sa;
    cout << "double转string: " << sa << endl;
    
    // 3. 另一种获取方式:直接用 str()
    ss.clear();
    ss.str("");
    float f = 56.78f;
    ss << f;
    string sValue = ss.str();  // 直接拿,不用经过 >> 
    cout << "用str()获取: " << sValue << endl;
    
    return 0;
}

为什么要 clear()?

stringstream 在读取完数据后,内部会设置 eofbit 或 badbit 状态。不清除的话,下次转换会失败:

cpp 复制代码
ss << 123;
ss >> sa;      // 读取成功,但内部状态变了
ss << 456;     // 写不进去,因为状态还是坏的
ss >> sa;      // 读不出来

为什么要 str("")?

str("") 是清空底层字符串。不清的话:

cpp 复制代码
ss << 123;
ss >> sa;      // sa = "123"
ss << 456;     // 底层还是 "123456",内容是追加
ss >> sa;      // sa = "123456",不是你想要的

两种获取方式的区别

cpp 复制代码
// 方式1:通过 >> 提取
ss << 123;
int n;
ss >> n;      // 提取到 n,ss 里内容会被"消费"掉

// 方式2:通过 str() 获取
ss << 123;
string s = ss.str();  // 直接拿底层字符串,不影响 ss 状态

b.字符串拼接

cpp 复制代码
#include <sstream>
#include <string>
#include <iostream>
using namespace std;

int main()
{
    stringstream ss;
    
    // 1. 拼接多个字符串
    ss << "first" << " " << "string,";
    ss << " second string";
    cout << "拼接结果: " << ss.str() << endl;
    
    // 2. 清空并重新使用(两件套)
    ss.clear();      // 清状态
    ss.str("");      // 清内容
    ss << "third string";
    cout << "清空后: " << ss.str() << endl;
    
    // 3. 顺便展示一下:不调用 clear() 会怎样
    ss.str("");      // 只清内容
    ss << "第四个字符串";  // 可能失败,也可能成功
    cout << "只清内容不请状态: " << ss.str() << endl;
    // 结果不确定,取决于编译器实现
    
    return 0;
}
cpp 复制代码
ss.clear();   // 清状态
ss.str("");   // 清内容

stringstream 清空那点事 记住两条就行

第一条:清空要两件套

cpp 复制代码
ss.clear();   // 清状态
ss.str("");   // 清内容

别嫌麻烦,少一个都可能翻车。

第二条:分情况对待

  • 只用 << 往里写:只 str("") 就够
  • 用过 >> 往外读:必须两件套都上
  • 搞不清楚:直接两件套,多写一行不吃亏

为啥会这样?

stringstream 读完数据后,内部会记一笔"我读完了",状态变成 eofbit。不清掉的话,下次再写它以为已经结束了,就不让你写。所以只看你干过啥:

  • 只写不读:状态一直是好的,清内容就行

  • 写过也读过:状态可能脏了,状态和内容都得清

  • 懒得记这些破事:每次都两件套,省心

那实际情况咋写呢?

cpp 复制代码
// 情况1:纯写
ss << "hello";
ss.str("");          // 清内容够了
ss << "world";       // 没问题

// 情况2:读写都干过
ss << "123";
int n;
ss >> n;             // 读了,状态脏了
ss.clear();          // 清状态
ss.str("");          // 清内容
ss << "456";         // 现在好了

// 情况3:嫌麻烦
stringstream ss;     // 每次新的,啥都不用管
ss << "就用一次";

主要核心就是:拿不准就 clear() 配 str(""),保你不出错


c.序列化和反序列化结构数据

说白了就是把结构体信息变成字符串发出去,对方收到后再还原成结构体。网络通信里这叫序列化反序列化

cpp 复制代码
#include <iostream>
#include <sstream>
#include <string>
using namespace std;

// Date类(前面说过,必须重载<<和>>才能在流里用)
class Date
{
public:
    int _year;
    int _month;
    int _day;
    
    Date() : _year(0), _month(0), _day(0) {}
    Date(int y, int m, int d) : _year(y), _month(m), _day(d) {}
    
    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);
};

ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << "-" << d._month << "-" << d._day;
    return out;
}

istream& operator>>(istream& in, Date& d)
{
    // 按 "2022-4-10" 这种格式读
    char ch1, ch2;
    in >> d._year >> ch1 >> d._month >> ch2 >> d._day;
    return in;
}

struct ChatInfo
{
    string _name;   // 名字
    int _id;        // id
    Date _date;     // 时间
    string _msg;    // 聊天信息
};

int main()
{
    // 1. 原始数据
    ChatInfo winfo = { "hjq", 135246, Date(2026, 9, 5), "晚上一起看电影吧" };
    
    // 2. 序列化:结构体 --> 字符串
    ostringstream oss;
    oss << winfo._name << " " 
        << winfo._id << " " 
        << winfo._date << " " 
        << winfo._msg;
    string str = oss.str();
    cout << "序列化后的字符串:" << endl;
    cout << str << endl << endl;
    
    // 模拟:通过网络发送这个字符串
    // 实际开发中会复杂得多,这里只是演示
    
    // 3. 反序列化:字符串 --> 结构体
    ChatInfo rInfo;
    istringstream iss(str);
    iss >> rInfo._name 
        >> rInfo._id 
        >> rInfo._date 
        >> rInfo._msg;
    
    // 4. 验证结果
    cout << "反序列化后还原:" << endl;
    cout << "姓名:" << rInfo._name << " (" << rInfo._id << ")" << endl;
    cout << "时间:" << rInfo._date << endl;
    cout << rInfo._name << ":>" << rInfo._msg << endl;
    
    return 0;
}

五.stringstream 底层原理和用法

它底层到底是在干啥?

stringstream 内部维护了一个 string 对象,所有操作都是在这个对象上折腾的。写入的内容会往这个 string 末尾追加,读取的时候从这个 string 里取数据。

核心规则就三条

规则1:清空要用两件套

cpp 复制代码
ss.clear();    // 清状态标志(eofbit 这些)
ss.str("");    // 清底层字符串

为啥要两个一起?因为:

  • clear() 只管状态标志,不改内容
  • str("") 只管清空内容,不改状态
  • 少一个都可能出问题

规则2:内容会被累积

cpp 复制代码
ss << "hello";
ss << " world";
cout << ss.str();  // "hello world"  看到没,是追加不是覆盖

想清空重来?那就用 str("")。


规则3:状态标志会影响后续操作

cpp 复制代码
ss << "123";
int n;
ss >> n;      // 读完数据后,内部状态变了
ss << "456";  // 这行可能写不进去,因为状态还是"已结束"

所以读过数据之后,必须 clear() 清状态。


三种常用操作

cpp 复制代码
stringstream ss;

// 1. 写入数据
ss << 123 << " " << 45.67;

// 2. 取出底层字符串
string s = ss.str();  // "123 45.67"

// 3. 清空准备复用
ss.clear();
ss.str("");
ss << "新的数据";

和 C 风格对比

C 风格的问题:

cpp 复制代码
char buf[10];
sprintf(buf, "%d", 123456789);  // 缓冲区不够,崩了

stringstream 的优势:

cpp 复制代码
stringstream ss;
ss << 123456789;                // 多大都不怕
string s = ss.str();            // 自动扩容

实际使用建议

场景1:一次性转换,直接新建

cpp 复制代码
string intToStr(int n) {
    stringstream ss;   // 每次新建,不用操心清空
    ss << n;
    return ss.str();
}

场景2:频繁转换同一个对象,记住两件套

cpp 复制代码
stringstream ss;
ss << 123;
string s1 = ss.str();

ss.clear();      // 两个都要
ss.str("");
ss << 456;
string s2 = ss.str();

小结就是记住这几点:

  1. 底层是 string,所以会自动扩容,不怕溢出
  2. 状态和内容是两码事,清的时候都要照顾到
  3. 读过的对象要清状态,不然写不进去
  4. 类型自动识别,不用记 %d、%f 那些格式符
    说通俗点就是,stringstream 就是让你像操作 cin/cout 一样操作字符串,不用管内存、不用管格式,省心。

简单总结就是:

  1. stringstream 底层维护了一个 string 对象,用来存放保存的数据。多次进行类型转换时,必须调用 clear () 清空流状态,才能正常转换;
  2. 但 clear () 并不会清空 stringstream 底层的 string 对象。
  3. 可以使用 s.str ("") 把底层 string 对象置为空字符串。
  4. 调用 s.str () 可以获取 stringstream 底层保存的 string 对象。
  5. stringstream 依靠 string 对象替代字符数组,规避了缓冲区溢出隐患;同时能够自动识别参数类型,无需格式化控制,不存在格式化失败的问题,使用起来更加便捷、安全。
相关推荐
zh_xuan1 小时前
c++ string_view
开发语言·c++
FFZero11 小时前
[mpv架构] (4) demux 线程到底在“预读“什么?
c++·架构·音视频·多媒体
西西弗Sisyphus1 小时前
C++ 实现 替代 OpenCV resize INTER_LINEAR 的一种方式
c++·opencv
wabs6662 小时前
关于二叉树【力扣107.二叉树的层序遍历II的思考】
数据结构·c++·算法·leetcode·二叉树·层序遍历
galaxy_strive2 小时前
Qt C++插件化编写项目(1)
开发语言·c++·qt
见叶之秋2 小时前
【C++】C++ 核心进阶(一):泛型编程基石 —— 模板初阶与 STL 体系开篇
开发语言·c++
一木 之林2 小时前
四、STL 容器与数据结构(进阶)(二)
数据结构·c++·哈希算法
会周易的程序员2 小时前
5Draft使用说明书
服务器·c++·分布式·raft·共识
码匠许师傅3 小时前
【设计模式精讲】21.迭代器模式(Iterator)
c++·设计模式·rpc·迭代器模式·软件工程·uml