C++ 流处理、包装器及字符串与数字转换
1. 流包装器的使用
1.1 流参数与包装器概念
std::ios_base
提供的参数(如
left
、
right
、
hex
、
width
、
precision
等)是一个封闭集合,自 20 世纪 80 年代中期定义后基本未变。由于每个操纵器都会修改流状态中的一个参数,所以操纵器集合本质上也是封闭的。现代影响特定数据值格式的方法是使用包装器。
1.2 自定义包装器示例
以下是一个自定义包装器的示例,用于在数据文件中引用值:
template<class InputIt, class OutputIt>
OutputIt do_quote(InputIt begin, InputIt end, OutputIt dest)
{
*dest++ = '"';
while (begin != end) {
auto ch = *begin++;
if (ch == '"') {
*dest++ = '\\';
}
*dest++ = ch;
}
*dest++ = '"';
return dest;
}
struct quoted {
std::string_view m_view;
quoted(const char *s) : m_view(s) {}
quoted(const std::string& s) : m_view(s) {}
};
std::ostream& operator<< (std::ostream& os, const quoted& q)
{
do_quote(
q.m_view.begin(),
q.m_view.end(),
std::ostreambuf_iterator<char>(os)
);
return os;
}
使用这个包装器,我们可以写出如下代码:
std::cout << quoted("I said \"hello\".");
1.3 与标准库包装器的比较
自定义包装器与标准库
<iomanip>
头文件中的
std::quoted
包装器函数类似,但
std::quoted
不使用基于迭代器的算法生成输出,而是在本地
std::string
变量中构造整个输出,然后一次性打印。这意味着
std::quoted
不支持分配器,不适合禁止堆分配的环境。
1.4 推荐使用包装器
建议使用描述自包含格式化操作的包装器,而不是操纵器,因为操纵器会“粘性”地改变流的状态。例如,错误放置的
std::hex
可能会影响后续输出。
2. 解决粘性操纵器问题
2.1 保存和恢复流状态
可以通过在每个复杂输出操作前后保存和恢复
ostream
的状态来解决“粘性
std::hex
”问题,示例代码如下:
void test() {
std::ios old_state(nullptr);
old_state.copyfmt(std::cout);
std::cout << std::hex << 225; // "e1"
std::cout.copyfmt(old_state);
std::cout << 42; // "42"
}
2.2 创建新的 ostream
也可以每次输出时创建一个全新的
ostream
,示例代码如下:
void test() {
std::ostream os(std::cout.rdbuf());
os << std::hex << 225; // "e1"
std::cout << 42; // "42"
}
2.3 iostreams 格式化的缺点
iostreams 格式化存在一些缺点,例如每个消息片段在遇到相应的
operator<<
时会立即输出,并且在国际化方面表现不佳,因为源代码中没有整体消息的“形状”,不利于翻译。因此,不建议将 iostreams 作为代码库的基础,建议使用
<stdio.h>
或第三方库(如
fmt
)进行格式化。
3. 使用 ostringstream 进行格式化
3.1 ostringstream 简介
ostringstream
类似于
ostream
,提供所有常见的
operator<<
功能,但它由
stringbuf
支持,将数据写入可调整大小的字符缓冲区(实际上是
std::string
)。可以使用
oss.str()
方法获取这个字符串的副本。
3.2 字符串化对象的示例
以下是将任何类型
T
的对象“字符串化”的示例:
template<class T>
std::string to_string(T&& t)
{
std::ostringstream oss;
oss << std::forward<T>(t);
return oss.str();
}
在 C++17 中,还可以考虑多参数版本的
to_string
:
template<class... Ts>
std::string to_string(Ts&&... ts)
{
std::ostringstream oss;
(oss << ... << std::forward<Ts>(ts));
return oss.str();
}
4. 本地化问题
4.1 本地化概念
使用
printf
或
ostream
进行字符串格式化(或字符串解析)时,需要注意本地化问题。本地化是用户环境中依赖于语言和文化习俗的子集,通过操作系统以编程方式暴露,允许程序根据用户的首选本地化调整行为。
4.2 本地化问题示例
以下示例展示了本地化对字符串格式化的影响:
std::setlocale(LC_ALL, "C.UTF-8");
std::locale::global(std::locale("C.UTF-8"));
auto json = to_string('[', 3.14, ']');
assert(json == "[3.14]"); // Success!
std::setlocale(LC_ALL, "en_DK.UTF-8");
std::locale::global(std::locale("en_DK.UTF-8"));
json = to_string('[', 3.14, ']');
assert(json == "[3,14]"); // Silent, abject failure!
4.3 本地化的性能成本
“当前本地化”是一个全局变量,每次访问都需要原子访问或全局互斥锁保护。每次调用
snprintf
或
operator<<(ostream&, double)
都必须访问当前本地化,这会带来巨大的性能成本,在多线程代码中可能成为性能瓶颈。
4.4 应对本地化问题的建议
-
应用程序程序员
:对于一定复杂度的应用程序,应在
main()函数的第一行设置std::locale::global(std::locale("C"))。如果不信任用户使用 UTF - 8,可以考虑使用"C.UTF-8",但要注意该名称自 2015 年左右才出现,旧系统可能不支持。 -
第三方库程序员
:有两种选择。较简单的方法是假设库仅在全局本地化设置为
"C"的应用程序中使用,可随意使用snprintf和operator<<;较难的方法是避免使用所有依赖本地化的格式化函数,这在 C++17 中借助一些新的库设施才变得可行。
5. 数字转换为字符串
5.1 整数转换为字符串
C++17 提供了以下几种将整数转换为字符串的方法:
| 方法 | 头文件 | 本地化问题 | 内存分配 | 支持进制 | 优点 | 缺点 |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
|
snprintf
|
<stdio.h>
| 无(
%d
不受本地化影响) | 非分配 | 8、10、16 | 本地化独立,非分配 | 仅支持 8、10、16 进制 |
|
oss << intvalue
|
<sstream>
| 有(可能插入千位分隔符) | 分配,支持分配器 | 8、10、16 | 支持多种进制 | 存在本地化问题,需要分配内存 |
|
std::to_string
|
<string>
| 无(等同于
%d
) | 分配,不支持分配器 | 10 | 方便组合成大消息 | 仅支持十进制,不支持分配器 |
|
std::to_chars
|
<charconv>
| 无 | 非分配 | 2 - 36 | 本地化独立,非分配,支持多种进制 | 是 C++17 新特性,标准库主要实现中可能未包含 |
5.2 浮点数转换为字符串
C++17 提供了以下几种将浮点数转换为字符串的方法:
| 方法 | 头文件 | 本地化问题 | 内存分配 | 格式化调整 | 优点 | 缺点 |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
|
snprintf
|
<stdio.h>
| 有(小数点问题) | 非分配 | 可调整 | 非分配 | 存在本地化问题 |
|
oss << floatvalue
|
<sstream>
| 有(小数点问题) | 分配,支持分配器 | 可调整 | 支持分配器,可调整格式化 | 存在本地化问题,需要分配内存 |
|
std::to_string
|
<string>
| 有(等同于
%f
) | 分配,不支持分配器 | 不可调整 | 方便使用 | 存在本地化问题,不可调整格式化,不支持分配器 |
|
std::to_chars
|
<charconv>
| 无 | 非分配 | 可调整 | 本地化独立,非分配,可调整格式化 | 是 C++17 新特性,标准库主要实现中可能未包含 |
以下是整数转换的 mermaid 流程图:
graph TD;
A[整数转换为字符串] --> B[snprintf];
A --> C[oss << intvalue];
A --> D[std::to_string];
A --> E[std::to_chars];
6. 字符串转换为数字
6.1 字符串转换为整数
C++17 提供了以下几种将字符串转换为整数的方法:
| 方法 | 头文件 | 溢出处理 | 错误处理 | 本地化问题 | 内存分配 | 支持进制 | 优点 | 缺点 |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
|
strtol
|
<stdlib.h>
| 饱和处理 | 设置全局
errno
| 理论上有 | 非分配 | 0、2 - 36 | 支持多种进制,跳过前导空格 | 溢出通过全局变量通信,不适合线程或高性能代码 |
|
sscanf
|
<stdio.h>
| 无法检测 | 返回 0 | 有 | 非分配 | 0、8、10、16 | 简单易用 | 无法检测溢出 |
|
std::stoi
|
<string>
| 抛出异常 | 抛出异常 | 有 | 非分配 | 0、2 - 36 | 错误检测好 | 仅适用于
std::string
类型 |
|
iss >> intvalue
|
<sstream>
| 饱和处理 | 设置
iss.fail()
| 有 | 分配,支持分配器 | 8、10、16 | 支持分配器 | 存在本地化问题,需要分配内存 |
|
std::from_chars
|
<charconv>
| 设置
r.ec != 0
| 设置
r.ec != 0
| 无 | 非分配 | 2 - 36 | 现代且高性能,输入缓冲区无需空终止 | 不能禁止跳过空格,仅支持小写十六进制输入 |
6.2 字符串转换为浮点数
C++17 提供了以下几种将字符串转换为浮点数的方法:
| 方法 | 头文件 | 溢出处理 | 错误处理 | 本地化问题 | 内存分配 | 支持进制 | 优点 | 缺点 |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
|
strtof
|
<stdlib.h>
| 饱和处理 | 设置全局
errno
| 有 | 非分配 | 10 或 16(自动检测) | 非分配 | 溢出通过全局变量通信,不适合线程或高性能代码 |
|
sscanf
|
<stdio.h>
| 无法检测 | 返回 0 | 有 | 非分配 | 10 或 16(自动检测) | 简单易用 | 无法检测溢出 |
|
std::stof
|
<string>
| 抛出异常 | 抛出异常 | 有 | 非分配 | 10 或 16(自动检测) | 错误检测好 | 仅适用于
std::string
类型 |
|
iss >> floatvalue
|
<sstream>
| 饱和处理 | 设置
iss.fail()
| 有 | 分配,支持分配器 | 10 或 16(自动检测) | 支持分配器 | 存在本地化问题,需要分配内存 |
|
std::from_chars
|
<charconv>
| 设置
r.ec != 0
| 设置
r.ec != 0
| 无 | 非分配 | 10 或 16(自动检测) | 本地化独立,非分配 | 不能禁止跳过空格 |
6.3 解析方法的选择建议
-
避免使用
atoi,因为其在无效输入时行为未定义。 -
std::stoi适合一次性解析用户输入,但不适合高性能工作,且仅适用于std::string类型。 -
std::from_chars是解析整数最现代和高性能的选择,但有一些限制,如不能禁止跳过空格,仅支持小写十六进制输入。
6.4 输入解析的建议
建议将输入的验证(或词法分析)与解析分开。如果能预先验证字符串只包含数字或符合有效浮点数的正则表达式语法,只需选择能检测溢出和/或尾随文本的解析方法,如
std::stof
或
std::from_chars
。
以下是字符串转换为整数的 mermaid 流程图:
graph TD;
A[字符串转换为整数] --> B[strtol];
A --> C[sscanf];
A --> D[std::stoi];
A --> E[iss >> intvalue];
A --> F[std::from_chars];
7. 总结与最佳实践
7.1 流处理与包装器总结
-
流包装器是现代 C++ 中调整数据值格式的有效方式,相较于传统的操纵器,它能避免流状态的“粘性”改变,使代码更具可读性和可维护性。例如自定义的
quoted包装器和标准库中的std::quoted都展示了包装器的使用方法。 -
解决粘性操纵器问题可以通过保存和恢复流状态或创建新的
ostream来实现,但 iostreams 格式化存在输出即时性和国际化方面的缺点,因此建议使用<stdio.h>或第三方库进行格式化。
7.2 本地化问题总结
- 本地化问题在字符串格式化和解析中是一个重要的考虑因素,它不仅会影响输出结果的正确性,还会带来性能成本。
- 应用程序程序员和第三方库程序员应根据自身情况采取不同的策略来应对本地化问题,如设置全局本地化或避免使用依赖本地化的函数。
7.3 数字与字符串转换总结
-
在数字转换为字符串和字符串转换为数字的过程中,C++17 提供了多种方法,每种方法都有其优缺点。例如,
std::to_chars和std::from_chars是比较现代和高性能的选择,但它们也有一些限制。 - 在选择转换方法时,需要考虑本地化问题、内存分配、支持的进制、错误处理等因素。
7.4 最佳实践建议
- 使用包装器 :优先使用描述自包含格式化操作的包装器,避免使用粘性操纵器。
- 处理本地化 :在代码中合理处理本地化问题,根据应用场景选择合适的本地化设置。
-
选择转换方法
:根据具体需求选择数字与字符串转换的方法,如对于高性能场景,优先考虑
std::to_chars和std::from_chars;对于一次性解析用户输入,可使用std::stoi或std::stof。 - 分离验证与解析 :将输入的验证(或词法分析)与解析分开,提高解析的准确性和效率。
7.5 整体流程总结
以下是一个整体的流程图,展示了从数字转换为字符串、字符串转换为数字以及处理本地化问题的流程:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
A(数字):::process -->|转换为字符串| B{选择方法}:::process
B -->|snprintf| C(snprintf 转换):::process
B -->|oss << intvalue| D(oss 转换):::process
B -->|std::to_string| E(std::to_string 转换):::process
B -->|std::to_chars| F(std::to_chars 转换):::process
G(字符串):::process -->|转换为数字| H{选择方法}:::process
H -->|strtol| I(strtol 解析):::process
H -->|sscanf| J(sscanf 解析):::process
H -->|std::stoi| K(std::stoi 解析):::process
H -->|iss >> intvalue| L(iss 解析):::process
H -->|std::from_chars| M(std::from_chars 解析):::process
N(本地化设置):::process -->|影响转换| B
N -->|影响解析| H
7.6 代码示例汇总
以下是本文中涉及的主要代码示例汇总:
自定义包装器示例
template<class InputIt, class OutputIt>
OutputIt do_quote(InputIt begin, InputIt end, OutputIt dest)
{
*dest++ = '"';
while (begin != end) {
auto ch = *begin++;
if (ch == '"') {
*dest++ = '\\';
}
*dest++ = ch;
}
*dest++ = '"';
return dest;
}
struct quoted {
std::string_view m_view;
quoted(const char *s) : m_view(s) {}
quoted(const std::string& s) : m_view(s) {}
};
std::ostream& operator<< (std::ostream& os, const quoted& q)
{
do_quote(
q.m_view.begin(),
q.m_view.end(),
std::ostreambuf_iterator<char>(os)
);
return os;
}
保存和恢复流状态示例
void test() {
std::ios old_state(nullptr);
old_state.copyfmt(std::cout);
std::cout << std::hex << 225; // "e1"
std::cout.copyfmt(old_state);
std::cout << 42; // "42"
}
创建新的 ostream 示例
void test() {
std::ostream os(std::cout.rdbuf());
os << std::hex << 225; // "e1"
std::cout << 42; // "42"
}
字符串化对象示例
template<class T>
std::string to_string(T&& t)
{
std::ostringstream oss;
oss << std::forward<T>(t);
return oss.str();
}
template<class... Ts>
std::string to_string(Ts&&... ts)
{
std::ostringstream oss;
(oss << ... << std::forward<Ts>(ts));
return oss.str();
}
本地化问题示例
std::setlocale(LC_ALL, "C.UTF-8");
std::locale::global(std::locale("C.UTF-8"));
auto json = to_string('[', 3.14, ']');
assert(json == "[3.14]"); // Success!
std::setlocale(LC_ALL, "en_DK.UTF-8");
std::locale::global(std::locale("en_DK.UTF-8"));
json = to_string('[', 3.14, ']');
assert(json == "[3,14]"); // Silent, abject failure!
通过以上内容,我们对 C++ 中的流处理、包装器、本地化问题以及数字与字符串转换有了更深入的了解。在实际编程中,我们可以根据具体需求选择合适的方法和策略,以提高代码的质量和性能。
88

被折叠的 条评论
为什么被折叠?



