【C++快速上手】数值转换字符串方法小结

1,302 阅读6分钟

引言

在C++编程中,我们经常遇到的一个基础问题是类型转换,比如将某个数值类型的变量(整型或者浮点型)转换成文字字符串的一部分展示给用户,这个时候我们就需要将数值类型转换成字符串。特别是这种转换十分频繁的时候,我们希望转换的方式是不容易出错且效率非常高的。在这篇文章中,我将系统地总结C++中将数值类型转换成字符串的各种方法并从效率和代码鲁棒性的方面对各个方法进行比较。

C++17 前的常用方法

在C++17之前我们主要有如下集中方式将数值类型转换为字符串:

  • sprintf / snprintf
  • stringstream
  • to_string

sprintf / snprintf

std::sprintf 和std::snprintf包含在头文件<cstdio>中,函数签名如下:

int sprintf( char* buffer, const char* format, ... );
int snprintf( char* buffer, std::size_t buf_size, const char* format, ... );

其中,buffer是指向目标字符串地址的指针,format是以“%d”开头,以“\0”结尾的C风格字符串,定义数值以何种方式输出到目标字符串中。sprintf将数值转换成“format”字符串定义格式的字符串并在末尾加上null terminator,最后写入到buffer指定的字符串存储空间中。这种方法的一个显著问题是sprintf函数本身不负责任何的内存管理,因此这种方法比较发生写越界的错误。 为了提高写入安全性,C++11引入了snprintf,相比较sprintf,这个函数多了buf_size这个参数来指定写入buffer的字符串的最大长度:最多buf_size - 1个字符(不包含结尾的null terminator)会被写入buffer的地址空间。 两个函数的函数返回值均为转换后除去null terminator的字符个数,对于snprintf,如果返回值小于buf_size,则buffer中只会写入部分转换后的字符串。在出错的情况下函数返回 -1。

Example

#include <cstdio>
#include <string>
#include <cinttypes>

std::string str(15, ' ');
uint32_t integer = 100;
auto result_1 = sprintf(str.data(), "%d", interger);
auto result_2 = snprintf(str.data(), str.size(), "%d", integer);

stringstream

stringstream包含在头文件 <sstream>当中,利用stringstream可以方便的以流运算符 << 将数值以各种格式或者进制写入stringstream对象中,不用担心写越界的问题,同时可方便地与其他字符串拼接在一起。但是相对于其他转换方法stringstream的效率比较低,一方面写入时的动态内存分配需要一定的开销,另一方面其成员函数str()在取出字符串时会进行一次字符串的值拷贝操作。

Example

#include <sstream>
std::stringstream ss;
std::string result;
uint32_t integer = 100;
ss << std::hex << integer << " is a hex number.";

to_string

C++11提供了std::to_string来将数值转换成字符串,std::to_string包含在头文件<string>中,该函数对多种数值类型进行了重载,使用起来非常方便,可以看作是sprintf “C++化”的使用方法,因为它具备了根据类型处理,抛出异常以及自动内存管理。缺点同样是字符串动态内存分配带来的开销和可能抛出异常,另外std::to_string在转换格式上依赖于运行环境的设置并不能进行精确控制,如在进行浮点数转换时无法指定精度,可能会出现和想象中不一样的结果。std::to_string最大的好处还是提现在易用性上面。

std::string to_string( int value );
std::string to_string( long value );
std::string to_string( long long value );
std::string to_string( unsigned value );
std::string to_string( unsigned long value );
std::string to_string( unsigned long long value );
std::string to_string( float value );
std::string to_string( double value );
std::string to_string( long double value );

C++17的std::to_chars

C++17中提供了更为高效和安全的std::to_chars,对整型和浮点数类型进行了重载。包含在头文件<charconv>中。 对于整数类型,函数声明如下:

std::to_chars_result to_chars(char* first, char* last, TYPE value, int base = 10);

TYPE可以是所有的有符号和无符号整型类型。base是2~36之间的数,表示转换后字符串的进制,因此转换后的字符串中大于9的数字用a~z的小写字母表示。 浮点数的函数声明包含更多的参数:

std::to_chars_result to_chars(char* first, char* last, FLOAT_TYPE value);

FLOAT_TYPE可以表示float, double以及long double。调用这个函数将使用默认的“C”转换的设置,相当于使用%f或者%e格式说明符来进行类型转换。 to_chars还提供如下声明:

std::to_chars_result to_chars(char* first, char* last, FLOAT_TYPE value,
std::chars_format fmt);

用户可以通过fmt字符串来指定输出格式。 完成版本的浮点数类型的to_chars声明如下,用户可以通过precision变量进一步指定转换精度:

std::to_chars_result to_chars(char* first, char* last, FLOAT_TYPE value,
std::chars_format fmt, int precision);

调用std::to_chars,若转换成功,则转换后的字符串将写入指针指向的[first, last)内存区间。 函数的返回值std::to_chars_result的定义如下:

struct to_chars_result {
	char* ptr;
    std::errc ec;
};

这个类型用来保存转换结果:

  1. 转换成功: ==转换后的字符串结尾没有 null-terminator==。ptr将指向转换后字符串最后一个字符的下一个字符,同时ec将等于std::errc的默认值。
  2. 无效参数:ptr将等于first,同时ec等于std::errc::invalid_argument。
  3. 转换后的字符串越界:ec等于std::errc::value_too_large,同时内存区间[first, last)中的内容处于未知状态。

Example

#include <iostream>
#include <charconv> // to_chars
#include <string>
int main()
{
	std::string str { "********" };
	const int value = 2019;
	const auto res = std::to_chars(str.data(),str.data() + str.size(), value);
	if (res.ec == std::errc())
	{
		std::cout << str << ", filled: " << res.ptr - str.data() << " characters\n";
	}
	else
	{
		std::cout << "value too large!\n";
	}
	return 0;
}

运行结果如下:

输入 输出
2019 2019**** filled: 4 characters
-2019 -2019****, filled: 5 characters
20192019 20192019, filled: 8 characters
-20192019 value too large! (the buffer is only 8 characters)

性能对比

为了对比各种方法的运行效率,我进行了如下测试:

  • 生成一个包含1000个随机整数的数组
  • 分别调用如下方法将整数转换为字符串:
    • sprintf
    • to_string
    • stringstream
    • to_chars
  • 重复1000次

测试代码中to_chars部分如下,完整代码详见benchmark

template <typename tFunc>
void run(const char* title, tFunc func) {
    const auto start = std::chrono::steady_clock::now();
    (void)func();
    const auto end = std::chrono::steady_clock::now();
    std::cout << title << ": " << std::chrono::duration <double, std::milli>(end - start).count() << " ms\n";
}

void benchmark(uint32_t times, uint32_t size) {
    const auto testIntVec = generateTestVec(size);
    std::vector<std::string> testStrVec(testIntVec.size());

    std::string temp(15, ' ');
    run("to_chars", [&]() {
        for (size_t iter = 0; iter < times; ++iter) {
            for (size_t i = 0; i < testIntVec.size(); ++i) {
                const auto res = std::to_chars(temp.data(), temp.data() + temp.size(), testIntVec[i]);
                testStrVec[i] = std::string_view(temp.data(), res.ptr - temp.data());
            }
        }
    });
  }

wandbox上gcc10.0.0的运行结果如下:

可见to_chars的运行效率是最高的。虽然to_chars的api是C风格的比较”裸“,但是它兼顾了效率和内存检查,安全性和灵活性都得到了提高。在使用中,可以根据实际用例编写to_chars的wrapper,使其更接近于现代C++风格。