字符转换

1. 为什么我们需要新的转换

在 C++17 之前,C++ 程序员在处理字符和数字转换时经常面临两个痛苦的选择:

  1. C 语言风格函数 (atoi, strtol, sprintf):

    类型不安全(容易崩溃),不支持 std::string,功能受限,无法区分错误(比如输入 123abc 会被当成 123 处理)。

  2. C++ 流 (std::stringstream/std::to_string):

    • std::stringstream - 极其(涉及内存分配、状态设置),代码冗长(写一行转换要三五行代码),不仅影响性能,还容易写错。
    • std::to_string - 可能抛出bad_alloc异常,并且无法控制精度、格式等

C++17 引入了 <charconv> 头文件,提供了一组全新的函数( std::to_charsstd::from_chars)。这些函数不仅速度极快(被认为是目前通用 C++ 库中最快的),而且极其方便(不分配内存,不抛异常)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sstream>
#include <string>
#include <iostream>

int main()
{
// 1. 使用 stringstream(慢且有异常)
std::stringstream ss;
int value = 42;
ss << value;
std::string str = ss.str();

// 2. 使用 to_string(有异常且无法控制精度、格式等)
std::string str1 = std::to_string(3.14159);
std::cout << "str = " << str << ", str1 = " << str1 << std::endl;

return 0;
}

<charconv> 的优势

  • 无异常:所有错误通过返回码处理
  • 高性能:比传统方法快5-10
  • 无内存分配:使用栈上缓冲区
  • 支持任意字符类型char, wchar_t, char16_t, char32_t
  • 区域设置无关:始终使用 C 区域设置,意思是说该库不遵循电脑或操作系统的本地化习惯(如中文、德文等),而是严格按照 C 语言的默认标准来处理数字格式

在 C++ 中,“C” 区域设置是最基础、最底层的标准格式。这主要影响两个方面:数字的分隔符小数点符号

  1. 小数点符号(最直观的影响)

    在某些国家(如德国、法国、部分南美国家),小数点不是写成 .(点),而是写成 ,(逗号)。

    • 普通流(受区域影响)
      如果电脑区域设置成了德国,那么当使用传统的 scanfstd::cin 读取数字 3,14 时,它能识别出这是 3.14
    • <charconv> (无区域影响):无论电脑设置在哪个国家,<charconv>只认 .(英文的点)。
      • 它能解析 3.14 -> 成功 (3.14)
      • 它遇到 3,14 -> 失败 / 错误。
  2. 千位分隔符(千分位)

    • 普通流:在很多地区,数字写作 1,000,000(美式)或者 1.000.000(欧式)。传统的库如果在本地模式下,可能会尝试去读取这些逗号或点。
    • <charconv>:它拒绝处理任何千位分隔符。
      • 1000000 -> 成功
      • 1,000,000 -> 失败。因为它只认识纯数字和唯一的那个小数点。

2. 数字转字符串 - std::to_chars

std::to_chars函数用于将整数或浮点数转换为字符序列。

1
2
3
4
5
std::to_chars_result to_chars(char* first, char* last, integer-type value, int base = 10);
std::to_chars_result to_chars(char*, char*, bool, int = 10 ) = delete;
std::to_chars_result to_chars(char* first, char* last, floating-point-type value);
std::to_chars_result to_chars(char* first, char* last, floating-point-type value, std::chars_format fmt);
std::to_chars_result to_chars(char* first, char* last, floating-point-type value, std::chars_format fmt, int precision);

参数详解:

  1. char* first: 缓冲区的起始指针。
  2. char* last: 缓冲区的结束指针(不包含 last,即 [first, last) 半开区间)。
  3. Value: 要转换的数值(支持 int, double, float 等)。
  4. base(仅整数有效): 进制,默认 10合法范围是 2 到 36(包含 2 和 36)
  5. fmt(仅浮点数):格式控制,是一个枚举类:
    • std::chars_format::scientific: 科学计数法(如 1.23e+5)。
    • std::chars_format::fixed: 定点格式(如 123.456)。
    • std::chars_format::general: 自动选择(默认),在科学计数法和普通计数法之间选择最短的一种。
    • std::chars_format::hex: 十六进制浮点数(如 1.23p+5)。
  6. precision(仅浮点数) : 要使用的浮点精度。 -1,表示使用该格式下能表示该数字的最短精度。

返回值 to_chars_result,其定义如下:

1
2
3
4
5
struct to_chars_result 
{
char* ptr;
std::errc ec;
};
  • 如果成功:ec == std::errc{}ptr 指向写入内容的末尾。
  • 如果失败(缓冲区太小):ec == std::errc::value_too_large

std::to_chars_resultC++17 引入的一个结构体,用于返回 std::to_chars 函数的执行状态和结果。

它的设计非常轻量级,不包含任何虚函数或复杂的内存管理,仅仅包含两个成员变量。下面是对这两个成员及其逻辑的详细解析:

  • char* ptr

    • 含义:这是一个指针,指向写入字符串结束位置的下一个字符

    • 作用

      • 它并不像 C 语言字符串那样以 \0 结尾,而是通过这个指针告诉你写到了哪里。
      • 你可以通过计算 ptr - first 来获得实际生成的字符串长度。
      • 它指向的位置是安全的(即 ptr <= last),你可以在这个位置手动添加 '\0' 来结束字符串。
    • 类比:它类似于标准库算法中的“尾后迭代器”。

  • std::errc ec

    • 含义:这是一个枚举类型(std::errc),用于存储错误码。

    • 可能的取值

      • std::errc() (即默认值 0):表示成功。转换顺利完成。
      • std::errc::value_too_large:表示失败。这是最常见的错误,意味着转换后的数字太长,超出了提供的缓冲区范围 [first, last)
    • 注意:不像其他 C++ 函数会抛出异常,std::to_chars 不会抛出异常,所有的错误信息都通过这个 ec 传递。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <charconv>
#include <iostream>
#include <array>

void float_formatting()
{
double pi = 3.141592653589793;
std::array<char, 50> buffer;

// 1. 通用格式(自动选择固定或科学计数法)
auto [ptr1, ec1] = std::to_chars(buffer.data(),
buffer.data() + buffer.size(),
pi);

// 2. 科学计数法
auto [ptr2, ec2] = std::to_chars(buffer.data(),
buffer.data() + buffer.size(),
pi,
std::chars_format::scientific);

// 3. 固定小数格式
auto [ptr3, ec3] = std::to_chars(buffer.data(),
buffer.data() + buffer.size(),
pi,
std::chars_format::fixed);

// 4. 指定精度
auto [ptr4, ec4] = std::to_chars(buffer.data(),
buffer.data() + buffer.size(),
pi,
std::chars_format::general,
5); // 精度

std::cout << "通用格式: " << std::string(buffer.data(), ptr1) << std::endl;
std::cout << "科学计数: " << std::string(buffer.data(), ptr2) << std::endl;
std::cout << "固定格式: " << std::string(buffer.data(), ptr3) << std::endl;
std::cout << "精度5位: " << std::string(buffer.data(), ptr4) << std::endl;
}

int main()
{
// 1. 准备一个足够大的缓冲区
// std::array 是很好的选择,因为它分配在栈上,速度快且不会溢出
std::array<char, 20> buffer;

// 2. 要转换的数字
int value = 12345;

// 3. 执行转换
auto result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value);

// 4. 检查是否成功
if (result.ec == std::errc())
{
// result.ptr 指向写入后的下一个位置
// 我们可以用它构造一个 std::string_view 来打印,避免拷贝
std::string_view sv(buffer.data(), result.ptr - buffer.data());
std::cout << "转换成功: " << sv << std::endl;
}
else
{
std::cout << "转换失败:缓冲区太小" << std::endl;
}

float_formatting();

return 0;
}

3. 字符串转数字 - std::from_chars

std::from_chars函数的作用是把字符序列解析为整数或浮点数。函数原型如下:

1
2
3
4
std::from_chars_result from_chars(const char* first, const char* last, 
integer-type& value, int base = 10);
std::from_chars_result from_chars(const char* first, const char* last,
floating-point-type& value, std::chars_format fmt = std::chars_format::general);

参数详解

  1. first: 字符串起始位置。
  2. last: 字符串结束位置。
  3. base: 进制(默认 10),合法范围是 2 到 36(包含 2 和 36)
  4. value: 引用类型,用来存放解析成功后的结果。
  5. fmt(仅浮点数) : 告诉函数期望什么样的格式。注意:如果输入是 1.23e4 而你设置 fmt = fixed,解析会失败。如果解析整数,此参数通常不需要。

返回值 from_chars_result,其原型如下:

1
2
3
4
5
struct from_chars_result 
{
const char* ptr; // 指向解析停止的位置
std::errc ec; // 错误码
};

std::from_chars_result 是 C++17 引入的 std::from_chars 函数的返回类型。它的作用是告诉调用者:字符串解析在哪里停止了,以及是否发生了错误。与 to_chars_result 类似,它也是一个轻量级的结构体,旨在提供最高性能的数值转换。下面是对其两个成员变量的详细解析:

  • const char* ptr
    • 含义:指向解析操作停止位置的那个字符。
    • 作用
      • 如果解析完全成功,它通常指向字符串中最后一个数字后面的那个字符(可能是空格、逗号、字符串结束符 \0 或其他非数字字符)。
      • 如果解析失败(例如第一个字符就不是数字),它会等于传入的起始指针 first(即没有移动任何位置)。
    • 应用场景:常用于解析格式化的数据流(例如 "123, 456"),可以利用 ptr 快速跳过已处理的数字,直接定位到下一个分隔符。
  • std::errc ec
    • 含义:错误码,指示解析的结果状态。
    • 可能的取值
      • std::errc() (默认值 0):成功。成功解析出了数值。
      • std::errc::invalid_argument无效参数。无法解析任何数字。例如字符串是 "abc",无法转换为数字。此时,输出的 value 参数不会被修改。
      • std::errc::result_out_of_range结果超出范围。字符串确实包含数字,但数值太大,超出了目标类型的存储范围。例如将 "99999999999999999999" 解析为 int。此时,输出的 value 参数不会被修改。

from_chars 最大的优点是严格。它不喜欢多余的垃圾字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <charconv>
#include <iostream>
#include <string>

int main()
{
std::string str1 = "12345";
std::string str2 = "123abc"; // 后面有非数字字符
std::string str3 = " 123"; // 前面有空格

int result = 0;

// --- 案例 1:完美解析 ---
auto res1 = std::from_chars(str1.data(), str1.data() + str1.size(), result);
if (res1.ec == std::errc())
{
std::cout << "解析成功1: " << result << std::endl;
}

// --- 案例 2:包含字母 ---
result = 0;
auto res2 = std::from_chars(str2.data(), str2.data() + str2.size(), result);
if (res2.ec == std::errc())
{
std::cout << "解析成功2: " << result << std::endl; // 输出 123
// 检查是否完全解析
if (res2.ptr != str2.data() + str2.size())
{
std::cout << " -> 警告:字符串未完全解析!" << std::endl;
}
}

// --- 案例 3:包含空格 ---
result = 0;
auto res3 = std::from_chars(str3.data(), str3.data() + str3.size(), result);
if (res3.ec == std::errc::invalid_argument)
{
std::cout << "解析失败3:遇到非法字符 (空格)" << std::endl;
}

return 0;
}

4. 注意事项

尽管std::to_charsstd::from_chars这两个函数很强大,但使用时也有不少门槛:

  1. 不添加 null 终止符 (\0)

    std::to_chars 不会 在输出末尾自动加 \0。如果想要把它当 C 字符串用,必须手动加:

    1
    2
    3
    4
    5
    auto res = std::to_chars(buf, buf+10, 42);
    if (res.ec == std::errc{})
    {
    *res.ptr = '\0'; // 必须手动!
    }
  2. 缓冲区溢出由使用者负责

    使用者必须确保提供的缓冲区足够大。对于 32 位整数,char buf[12] 足够;但对于 double,可能需要几十个字节。如果给的空间不够,函数会返回错误值,不会越界写入,但需要检查返回值。

  3. 编译器支持差异

    C++17 标准规定了接口,但早期的编译器(如 GCC 8, MSVC 19.14)只实现了整数版本。浮点数版本的 std::to_chars 是后来才补全的(GCC 9+, Clang 7+, MSVC 19.2x+)。如果编译浮点数版本报错,请检查编译器版本。

  4. 格式限制

    std::from_chars 目前不支持像 %03d 这样的填充格式。它只做纯数值转换。如果需要 0012 这种格式,请自己补 0

如果我们需要编写对性能敏感的底层 C++ 代码,请务必抛弃 std::stringstreamsprintf,拥抱 C++17 的 std::to_charsstd::from_chars