C++ stringstream

如果一个字符串里只包含单个数字,我们调用前面学过的转换函数很快就能处理完。但如果拿到的是像 "Jack 18 92.5" 这样包含多个字段的整段文本,要是手动去查找空格位置、切分字符串再逐个转换,写起来就会非常繁琐。

针对这种多字段解析与文本组装的需求,C++ 在 <sstream> 头文件中提供了 “字符串流”。平时我们使用的 std::cin >> 是从键盘读取数据,而字符串流是从现成的 std::string 里读取。平时使用的 std::cout << 是往屏幕上打印,而字符串流则是往内存里的 std::string 写入。

很多初学的小伙伴一看到 stringstream 这个名字,就习惯在所有场景下都只使用 std::stringstream。实际上,<sstream> 头文件中一共提供了 3 种字符串流,它们的职责分工如下表所示。

3 种字符串流
类型名 英文原意 主要用途
std::istringstream input string stream(输入流) 只读(>>),从已有 string 中依次提取多个字段
std::ostringstream output string stream(输出流) 只写(<<),将多个不同类型的值拼接格式化为 string
std::stringstream string stream(读写流) 可读可写(>> 和 <<),在同一个流中写入并读取数据

在实际开发中,如果只是从字符串里提取数据,我们优先使用 std::istringstream;如果只是把多个数据组装成字符串,优先使用 std::ostringstream,这样能让代码的读写方向更加清晰。

使用 istringstream 解析字符串

我们先来看常见的多字段解析场景。根据字段之间的分隔方式与校验需求,std::istringstream 主要有以下 3 种实用写法。

1. 按空格提取不同类型的数据

当字符串中的各个字段是以空格隔开时,我们可以像使用 std::cin 一样,通过 >> 把内容依次读取到 std::string、int 和 double 变量中。

语法:

std::istringstream 流名称(源字符串);
流名称 >> 变量1 >> 变量2;

说明:

第一行使用源字符串创建一个输入字符串流对象,第二行从左到右依次提取数据。每成功读取一个值,流内部的读取位置就会自动往后移动,并且会自动跳过字段之间的空格、制表符或换行符。

示例 1:按空格解析多个字段

#include <iostream>
#include <sstream>                    // 提供字符串流
#include <string>

int main() {
    std::string text = "Jack 18 92.5";
    std::istringstream input(text);

    std::string name;
    int age = 0;
    double score = 0.0;

    input >> name >> age >> score;    // 按空格依次提取字段

    std::cout << name << '\n';
    std::cout << age << '\n';
    std::cout << score << '\n';
    return 0;
}

运行结果如下。

Jack
18
92.5

分析:

在这个例子中,我们先使用字符串 text 创建了输入字符串流 input,接着通过 input >> name >> age >> score; 按空格依次提取 3 个字段。

在读取过程中,"Jack" 被存入字符串 name,"18" 自动解析为 int 存入 age,"92.5" 自动解析为 double 存入 score。整个过程既不用手动切分文本,也不用分别调用 std::stoi() 和 std::stod()。

2. 按指定分隔符提取字段

使用 >> 读取字符串时,一旦遇到空格就会停下,但遇到逗号或竖线并不会自动断开。如果各个字段是通过逗号 ',' 隔开的(比如 CSV 表格行数据),我们可以把 std::istringstream 传给前面学过的 std::getline() 函数来逐段切分。

语法:

std::getline(输入字符串流, 接收变量, 分隔字符)

说明:

每次调用 std::getline() 时,它会从输入字符串流中读取字符并存入接收变量,直到遇到指定的分隔字符或读完整个字符串为止。我们配合 while 循环,就能把所有字段逐个提取出来。

示例 2:按逗号提取多个字段

#include <iostream>
#include <sstream>
#include <string>

int main() {
    std::string line = "Jack,广州,C++";
    std::istringstream input(line);
    std::string item;

    // 每次读取到逗号为止
    while (std::getline(input, item, ',')) {
        std::cout << item << '\n';
    }
    return 0;
}

运行结果如下。

Jack
广州
C++

分析:

每次循环执行 std::getline(input, item, ',') 时,都会从字符串流 input 中读取一段文本存入 item。一旦遇到逗号 ',' 就会停下并跳过该逗号,直到把整段字符串全部读完后退出循环。

补充一点:如果原字符串中出现了两个连续的逗号(如 "Jack,,C++"),中间那一次读取并不会跳过,而是会正常读出一个空字符串 ""。

3. 判断解析是否成功

字符串流在解析失败时默认不会抛出异常。如果当前位置的文本内容与目标变量的类型不匹配,比如本应是数字的地方写成了 "abc",流就会进入失败状态。因此在实际开发中,我们通常把读取表达式直接放进 if 条件里做判断。

示例 3:检查多字段解析结果

#include <iostream>
#include <sstream>
#include <string>

int main() {
    std::string text = "18 abc";
    std::istringstream input(text);

    int age = 0;
    double score = 0.0;

    if (input >> age >> score) {
        std::cout << "解析成功" << '\n';
    } else {
        std::cout << "解析失败" << '\n';
        std::cout << age << '\n';
    }
    return 0;
}

运行结果如下。

解析失败
18

分析:

在字符串 "18 abc" 中,第 1 个字段 "18" 可以正常解析为 int,所以变量 age 的值已经变成了 18。但第 2 个字段 "abc" 无法解析为 double,于是整条 input >> age >> score 表达式返回假,程序进入了 else 分支。

这里有一个容易忽略的细节:连续提取是按从左到右的顺序一步步执行的,后面的字段解析失败,并不会把前面已经写入 age 的值退回原状。所以在处理外部文本时,小伙伴们一定要先通过 if (input >> ...) 确认整条读取链都成功了,再拿这些变量参与后续计算。

使用 ostringstream 组装字符串

当我们需要把字符串、整数、浮点数等多个不同类型的值组合成一段完整文本时,使用 std::ostringstream 会比反复调用 std::to_string() 再用 + 号拼接省事得多。

语法:

std::ostringstream 流名称;
流名称 << 值1 << 值2;
std::string 结果字符串 = 流名称.str();

说明:

我们可以通过 << 一次或分多次往 std::ostringstream 对象中写入不同类型的数据。全部写完后,调用不带参数的成员函数 str(),就能取出组装好的完整 std::string。

示例 4:拼接多种类型的数据

#include <iostream>
#include <sstream>
#include <string>

int main() {
    std::string name = "Jack";
    int age = 18;
    double score = 92.5;

    std::ostringstream out;
    out << "姓名:" << name << ",年龄:" << age;
    out << ",成绩:" << score;

    std::string result = out.str();    // 取出拼接好的字符串
    std::cout << result << '\n';
    return 0;
}

运行结果如下。

姓名:Jack,年龄:18,成绩:92.5

分析:

在这个例子中,我们分两次通过 << 把 std::string、int 和 double 这 3 种类型的数据按顺序写进了字符串流 out 里,最后调用 out.str() 就拿到了拼接完整的字符串。

如果在组装文本的同时还想控制浮点数的小数位数,我们也可以往流里写入 <iomanip> 头文件中的 std::fixed 和 std::setprecision()。下面我们来看一个例子。

示例 5:组装文本时控制小数位数

#include <iomanip>    // 提供 std::setprecision()
#include <iostream>
#include <sstream>
#include <string>

int main() {
    std::string item = "笔记本";
    int count = 3;
    double unitPrice = 12.5;
    double total = count * unitPrice;

    std::ostringstream out;
    out << std::fixed << std::setprecision(2);
    out << "商品:" << item << ",数量:" << count;
    out << ",总价:" << total << " 元";

    std::cout << out.str() << '\n';
    return 0;
}

运行结果如下。

商品:笔记本,数量:3,总价:37.50 元

分析:

往流对象 out 写入 std::fixed 和 std::setprecision(2) 后,这一格式设置会对后续写入该流的浮点数持续生效。因此后面写入总价 total 时,原本的 37.5 会自动补齐为保留 2 位小数的 "37.50",一步完成了多类型拼接与小数位数控制。

使用 stringstream 读写与清空复用

std::stringstream 同时具备 << 写入和 >> 读取两种能力。在循环里反复使用同一个流对象转换多组数据时,初学者常会误以为像 vector.clear() 那样,只调用一次 stream.clear() 就能把流里的旧文本清空。

实际上,字符串流里的 “文本内容” 与 “内部状态标志” 是分开管理的。你可以把内部状态看作一盏指示灯:当流把内容一直读到字符串末尾时,就会点亮 “已读完(EOF)” 指示灯。想要彻底清空并复用同一个流对象,我们需要配合完成以下 2 步操作:

  • stream.str(""):清空流里的旧字符串。注意区分 str() 的两种用法:不传参数写 stream.str() 是读取流里的内容,传入空字符串写 stream.str("") 则是把流里的内容替换为空。
  • stream.clear():重置流的内部状态标志,相当于把 “已读完” 或 “解析出错” 的指示灯关掉,让流恢复到可以正常读写的状态。

示例 6:清空并复用 stringstream

#include <iostream>
#include <sstream>

int main() {
    std::stringstream stream;

    // 第 1 次写入并读取
    stream << "100";
    int a = 0;
    stream >> a;
    std::cout << a << '\n';

    // 清空旧内容并重置状态标志
    stream.str("");
    stream.clear();

    // 第 2 次写入并读取
    stream << "200";
    int b = 0;
    stream >> b;
    std::cout << b << '\n';
    return 0;
}

运行结果如下。

100
200

分析:

为什么第 1 次明明正常读出了 100,后面还要调用 stream.clear() 呢?因为字符串 "100" 后面没有其他字符了,stream >> a 一直读到字符串尽头才确认整数结束,此时流内部的 “已读完” 状态就已经被触发了。

这两步操作缺一不可。如果我们只调用 stream.clear() 而不写 stream.str(""),上一次没读完的残留字符还会留在流里。反过来,如果像下面这样只清空字符串而漏掉 stream.clear(),流依然停留在 “已读完” 状态,第 2 次读取就会失败:

stream.str("");    // 仅清空了旧字符串
stream << "200";
stream >> b;       // 读取失败:未重置状态标志

因此大家只要记住这么一个固定搭配:凡是要复用同一个字符串流对象,把 stream.str(""); 和 stream.clear(); 这两行成对写上就可以了。

提示 平时如果只是单个数值与字符串互转,我们优先使用 std::stoi()、std::stod() 和 std::to_string() 即可;遇到多字段解析或多类型拼接时,再选用字符串流。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号