如果一个字符串里只包含单个数字,我们调用前面学过的转换函数很快就能处理完。但如果拿到的是像 "Jack 18 92.5" 这样包含多个字段的整段文本,要是手动去查找空格位置、切分字符串再逐个转换,写起来就会非常繁琐。
针对这种多字段解析与文本组装的需求,C++ 在 <sstream> 头文件中提供了 “字符串流”。平时我们使用的 std::cin >> 是从键盘读取数据,而字符串流是从现成的 std::string 里读取。平时使用的 std::cout << 是往屏幕上打印,而字符串流则是往内存里的 std::string 写入。
很多初学的小伙伴一看到 stringstream 这个名字,就习惯在所有场景下都只使用 std::stringstream。实际上,<sstream> 头文件中一共提供了 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() 即可;遇到多字段解析或多类型拼接时,再选用字符串流。
