将数字类型转换为字符串以及字符串转换为数字类型






4.77/5 (17投票s)
2004 年 2 月 24 日
7分钟阅读

169635

1436
对各种转换技术的基准测试、评论以及一些自定义转换函数。
引言
本文讨论了将字符串转换为数值数据类型以及反向转换的几种方法。我还提供了一些执行这些转换的各种技术的基准测试,并添加了一些自定义函数。
背景
当我刚开始用 C++ 编程时,我发现与 PHP 等脚本语言相比,最难适应的就是在字符串和数值数据类型之间来回转换。C++ 语言本身并不处理这些转换。有各种函数可以执行这些转换:(注意,这只是你可以使用的转换函数的一个子集)
sprintf()
可用于将数值类型转换为整数- CRT 函数,如
itoa
、atoi
和atof
,可用于将整数转换为字符串以及反向转换 std::strstream
和std::stringstream
使你能够添加数值类型并将其提取为字符串,反之亦然- 你可以相对轻松地创建用于将字符串转换为数值类型的自定义函数
基准测试
各种转换函数/方法将在下面更详细地讨论。然而,如果你像我一样,会直接跳到重点部分,所以我将节省你的滚动时间,立即提供实证数据。
所有测试都对一百万次转换进行了基准测试,并使用了一个包含一千个随机生成值(根据测试是数值型还是字符串型的双精度数或整数)的数组。测试运行了几次,并使用平均时间进行比较。测试之间的差异非常小(小于 5%),因此我相信这些测量结果相当准确。我还尝试更改了随机数算法,以确保结果对于双精度数和整数的整个值范围(以及表示它们的各种字符串方式)都有效。
所有基准测试都在 release 版本中进行,重点是速度。没有优化时,结果相似但速度较慢,但字符串流的性能差距真的拉大了,字符串流的性能比 CRT 函数慢一百倍。
双精度数转字符串 | 整数转字符串 | 字符串转双精度数 | 字符串转整数 | |
strstream |
4,078,000 | 1,765,000 | 2,851,000 | 1,850,000 |
stringstream |
3,788,000 | 2,064,000 | 3,124,000 | 2,334,000 |
_snprintf* |
2,155,000 ("%f ") |
589,000 ("%i ") |
不适用 | 不适用 |
CRT 函数 | 4,285,000 _gcvt() |
192,000 itoa() |
1,490,000 atof() |
60,000 atoi() |
我的函数 | 不适用 | 不适用 | 374,000 | 50,000 |
*sprintf
和 _snprintf
之间没有可测量的性能差异。
这些结果中最让我惊讶的是,我自己的函数实际上比 CRT 等效函数性能更好。我的函数比 CRT 函数更灵活,因为它们会在开始转换任何内容之前搜索字符串中的第一个数字。这可以使解析服务器响应更加容易!我的函数还能区分无法将字符串转换为数字的情况和表示数字零的字符串。它们不会简单地返回零,而是在字符串根本不包含数字时抛出 InvalidConversionException
。
字符串流
当然,进行类型转换为字符串的最简单方法是使用字符串流。可以这样编写一个 ToString()
函数
template<class T> void ToString(const T& val, char * str, int count) { std::strstream strm(str,count,0); strm << val; }
其中 val
是几乎任何基本类型的待转换值,str
是你的目标字符串。不会创建临时缓冲区,此函数直接在目标字符串中进行转换。
它速度较慢,但与 STL string 类更直观
template<class T> std::string ToString(const T& val) { std::stringstream<char> strm; strm << val; return strm.str(); }
很多转换函数都是这样编写的。事实上,上面是我一直在 C++ 项目中使用的转换函数。它非常简单易用,但需要付出代价。如果你查看上面的表格,你会发现字符串流在除双精度数转字符串转换外的所有类别中的性能都差 10-20 倍。原因是你要创建和销毁一个相当复杂的对象,有时还要分配和释放临时缓冲区,并在基本转换函数之上添加多层代码。
CRT 函数
我对 CRT 转换函数最大的不满是它们的名称太晦涩了!_itoa()
、atoi()
、atof()
、_gcvt()
、_fcvt()
、_ecvt()
等。但是一旦你弄清楚哪个函数做什么以及如何正确使用它们,它们就非常有用。atoi()
比我的自定义转换类仅慢 20%。而 itoa()
被证明是整数转字符串的最佳方法。然而,其中一些函数的性能令人失望。_gcvt()
比使用字符串流还慢!我很想知道 _fcvt()
的表现如何。
sprintf()
sprintf()
函数就像 printf()
一样工作,只是它将结果保存到字符串而不是打印到输出缓冲区。然而,sprintf()
并不会考虑字符串在内存中是固定长度的设备的事实,如果你不小心,它会导致溢出。因此,我推荐使用 _snprintf()
,它允许你指定最大尺寸。只需确保将大小指定为小于实际分配的内存块,因为如果 _snprintf()
在 count
处截断输出,它不会存储终止的空字符。
void ToString(char * destStr, int count, double val) { _snprintf(destStr,count,"%f",val); }
对于基准测试,我使用了“%f
”和“%i
”作为格式字符串,但值得注意的是 sprintf()
提供了比其他技术更多的格式化选项。例如,你可以将浮点值四舍五入到所需的精度,并用零填充。
自己编写
将字符串转换为数值类型的基本概念与计算机在字符串中表示数字的方式有关。在 ASCII 字符串中,所有字符都由数字 0-127(有符号字节(char)的非负范围)表示。因此,“123”字符串实际上是 49,50,51。你可以使用 switch
语句将每个数字字符替换为其等效的数值,但有更快的解决方法。由于所有数字字符都是连续的,你可以通过确定它们与 ASCII 零(48)的距离来获得它们的数值。例如
char numeral = '7'; int num = numeral-'0';
添加一些 for 循环,并在遍历时乘以 10,你就会得到
const char * str = "12345"; int num = 0; for (; *str != '\0' && *str >= '0' && *str <='9'; str++) num = (num * 10) + (*str - '0');
此外,你需要测试符号值,在转换为双精度数的情况下,你需要测试小数点后的值和指数。一个好的转换函数还应该能够忽略字符串开头的非数字字符(如空格)和尾随字符。它还应该区分无法转换的字符串和像“0.00”这样的字符串。在转换为无符号整型类型时,你也应该有一种处理负数输入的方法。我选择在目标类型为无符号时忽略符号字符,但另一种有效的解决方案是将“-1”转换为 4294967295(对于无符号 int)。
你可以在演示项目中获取这些函数。
这些函数本身速度很快,但仍有很大的改进空间。如果你已经知道字符串的长度(例如,存储在字符串类中),它们会更快。
关于 STL 字符串的说明
你会注意到,许多较好的转换方法都需要创建一个临时字符数组,执行转换,然后将结果复制到 STL 字符串中。这方面没什么可做的。就个人而言,这压垮骆驼的最后一根稻草。我受够了那些需要十行代码和到处都是临时缓冲区的 STL 字符串才能做一些简单的事情。我的下一篇文章将是关于创建一个更好的字符串类的。
附录 A:ASCII 表
Char Dec Oct Hex | Char Dec Oct Hex | Char Dec Oct Hex | Char Dec Oct Hex
-------------------------------------------------------------------------------------
(nul) 0 0000 0x00 | (sp) 32 0040 0x20 | @ 64 0100 0x40 | ` 96 0140 0x60
(soh) 1 0001 0x01 | ! 33 0041 0x21 | A 65 0101 0x41 | a 97 0141 0x61
(stx) 2 0002 0x02 | " 34 0042 0x22 | B 66 0102 0x42 | b 98 0142 0x62
(etx) 3 0003 0x03 | # 35 0043 0x23 | C 67 0103 0x43 | c 99 0143 0x63
(eot) 4 0004 0x04 | $ 36 0044 0x24 | D 68 0104 0x44 | d 100 0144 0x64
(enq) 5 0005 0x05 | % 37 0045 0x25 | E 69 0105 0x45 | e 101 0145 0x65
(ack) 6 0006 0x06 | & 38 0046 0x26 | F 70 0106 0x46 | f 102 0146 0x66
(bel) 7 0007 0x07 | ' 39 0047 0x27 | G 71 0107 0x47 | g 103 0147 0x67
(bs) 8 0010 0x08 | ( 40 0050 0x28 | H 72 0110 0x48 | h 104 0150 0x68
(ht) 9 0011 0x09 | ) 41 0051 0x29 | I 73 0111 0x49 | i 105 0151 0x69
(nl) 10 0012 0x0a | * 42 0052 0x2a | J 74 0112 0x4a | j 106 0152 0x6a
(vt) 11 0013 0x0b | + 43 0053 0x2b | K 75 0113 0x4b | k 107 0153 0x6b
(np) 12 0014 0x0c | , 44 0054 0x2c | L 76 0114 0x4c | l 108 0154 0x6c
(cr) 13 0015 0x0d | - 45 0055 0x2d | M 77 0115 0x4d | m 109 0155 0x6d
(so) 14 0016 0x0e | . 46 0056 0x2e | N 78 0116 0x4e | n 110 0156 0x6e
(si) 15 0017 0x0f | / 47 0057 0x2f | O 79 0117 0x4f | o 111 0157 0x6f
(dle) 16 0020 0x10 | 0 48 0060 0x30 | P 80 0120 0x50 | p 112 0160 0x70
(dc1) 17 0021 0x11 | 1 49 0061 0x31 | Q 81 0121 0x51 | q 113 0161 0x71
(dc2) 18 0022 0x12 | 2 50 0062 0x32 | R 82 0122 0x52 | r 114 0162 0x72
(dc3) 19 0023 0x13 | 3 51 0063 0x33 | S 83 0123 0x53 | s 115 0163 0x73
(dc4) 20 0024 0x14 | 4 52 0064 0x34 | T 84 0124 0x54 | t 116 0164 0x74
(nak) 21 0025 0x15 | 5 53 0065 0x35 | U 85 0125 0x55 | u 117 0165 0x75
(syn) 22 0026 0x16 | 6 54 0066 0x36 | V 86 0126 0x56 | v 118 0166 0x76
(etb) 23 0027 0x17 | 7 55 0067 0x37 | W 87 0127 0x57 | w 119 0167 0x77
(can) 24 0030 0x18 | 8 56 0070 0x38 | X 88 0130 0x58 | x 120 0170 0x78
(em) 25 0031 0x19 | 9 57 0071 0x39 | Y 89 0131 0x59 | y 121 0171 0x79
(sub) 26 0032 0x1a | : 58 0072 0x3a | Z 90 0132 0x5a | z 122 0172 0x7a
(esc) 27 0033 0x1b | ; 59 0073 0x3b | [ 91 0133 0x5b | { 123 0173 0x7b
(fs) 28 0034 0x1c | < 60 0074 0x3c | \ 92 0134 0x5c | | 124 0174 0x7c
(gs) 29 0035 0x1d | = 61 0075 0x3d | ] 93 0135 0x5d | } 125 0175 0x7d
(rs) 30 0036 0x1e | > 62 0076 0x3e | ^ 94 0136 0x5e | ~ 126 0176 0x7e
(us) 31 0037 0x1f | ? 63 0077 0x3f | _ 95 0137 0x5f | (del) 127 0177 0x7f
致谢
感谢 George Anescu 和他的 CPreciseTimer 类。特别感谢(未知的)创建上述 ASCII 表的人。