C++中int型和std::string互相转换

每次用到时,都要到网上搜索一下的感觉很不好。尤其是经常用到,每次搜索时你完全能认识到你已经查过很多遍了。另外,和不自信(拿不准的心理)以及养成了这种坏习惯都有关系,查得多了自然知道调用 C 标准库 atoi(itoa 不是 C 标准库函数)以及使用 stringstream 流来解决问题,但每每觉得差那么一点意思,不够简洁。每次用到时都要搜索一下,可能是希望找到一种让内心舒坦的转换“手法”吧。

参考 C++11 中的 string - atoi/itoa,岂止是参考,根本就是抄袭。可是好不喜欢原文的排版。

我哭,我发现又是从英文原文翻译过来的,大致看了一眼,就明白了翻译的不可靠啊。比如译文中叙述

相比于 atoi,strtol 多了最后一个参数 “radix” 表明函数采用的是几进制(这个进制数可以从2到34,这个数值范围的原因显而易见)

34显而易见你妹啊,当时就觉得诡异(当然再google一下就知道怎么回事了),相信译者也是笔误而已。来看英文原文:

strtol’s third parameter specifies the radix whose value is between 2 and 36 inclusively.

多简洁的表述!!!额。。。译者貌似不是笔误,看英文原文下的评论,汗

我们对比另一个位置的表述:(我怎么这么幼稚呢。。)

值得注意的是,在 C++98 代码中,虽然字符串的存储使用字符串数组也是完全可以的,

It is noteworthy that although strings can still be stored in c-arrays in C++ code,

一定要抽空看英文啊,上面发现的问题真是瞟了一眼,并没有通读原文的。转帖中,认为译文表述不恰当的地方已经用英文原文替代。

懂得历史,才能明白现在。在处理 atoi/itoa 问题时觉得凌乱、不成体系是因为在工作中只求开发效率,只看解决方法时看到的只是时间的一张快照,要回去探索历史,明白其中各自归属就能和内心“和解”了。下面依序描述 C,C++98,C++11 是如何处理 atoi/itoa 问题的:

在C时代

atoi

在 C 时代,通常我们遇到 atoi(字符串到数值转换)的问题的时候我们会使用 <stdlib.h> 中的 atoi 函数:

1
int num = atoi(cstr);

这里的 cstr 通常为 char* 或者 const char* 类型的字符串。函数返回的结果则是该字符串所表示的一个十进制的 integer。函数的整个效果则等同于 <stdlib.h> 中的另外一个函数strtol

1
int num = strtol(cstr, NULL, 10);

strtol’s third parameter specifies the radix whose value is between 2 and 36 inclusively. 。除去 strtol 会在出错时设置全局的 errno 外,其效果与 atoi 系列中的atol则几乎是完全等同的。

itoa

而 C 时代解决 itoa(数值到字符串的转换)的时候,则采用了 sprintf 函数:

1
2
3
int myint;
char buf[SIZE];
sprintf(buf, "my data is %d", myint);

这里字符的输出控制交给了 %d 这样的特殊字符。通过特殊字符以及变长参数的配合(sprintf是变长参数函数),我们获得预期的 formatted I/O 的输出。

小结

这里我们可以看到 C 中对 atoi/itoa 的处理的特点,基本可以归纳如下:

  1. atoi 不检查字符串中错误。这对使用 API 的程序员而言意味着他必须检查错误,或者必须判断出错误在实际使用中总是不存在或者是可以被程序忍受的。
  2. atoi 的替代版本 strtol 检查字符串的错误,但使用的是 POSIX中 的标准方式,设置 errno。这意味着使用 strtol 的程序员如果要检测字符串中的错误,需要在调用 strtol 后检测全局变量 errno。
  3. sprintf 不负责任何的内存管理。通常情况下,程序员都会被告诫使用 snprintf 或者其它有内存边界检查的版本替代 sprintf 。这样一来会减少发生缓冲区溢出的可能性。不过总的来说这只是一种编程中的防御手段,从程序员的角度而言,内存管理的烦恼依然存在。
  4. sprintf 跟 printf 一样,不检查参数类型(因为是以变长函数的方式实现的),所以如果参数和 escape character 不匹配的话,会在运行时才发现不匹配的输出。不过相对于其它三点,这种错误是最容易修正的。

所以说 C 中的 atoi/itoa 问题的解决方式并算不得让程序员愉悦。在坏的输入情况下,程序员必须小心处理各种异常,以防程序误入歧途。不过反过来看,C 中的 atoi/itoa 的处理也非常直观,易于理解,所以即使在 C++ 中这样的代码也并非少见。

C++98时代

先强调一点:在 C++ 代码中,虽然字符串的存储使用C风格的字符数组也是完全可以的,但在 C++ 代码中使用 std::string 类型,内存可以自行有效地管理,而且成员函数可以抛出异常,所以更适用于 C++ 代码。

atoi/itoa

到了 C++98 时代,atoi/itoa 可以使用新的 C++ 标准库来完成。具体地就是使用 C++ 的流(stream)模板类。而关于 std::string 类型的流模板类型就是 std::stringstream。通过全局重载的 operator <<以及 operator >>std::stringstream 可以很轻松地完成 atoi 或者是 itoa 的任务,比如:

1
2
3
ostringstream oss;
oss << 15 << " is int, " << 3.14f << " is float." << endl;
cout << oss.str();

oss 就是一个字符串流对象,可以用于 itoa 的工作。而

1
2
3
4
5
istringstream iss("12 14.1f");
int a;
float b;
iss >> a >> b;
cout << a << " " << b << endl;

上面代码中的 iss 字符串流对象,则可用作 atoi。

小结

从设计上讲,std::stringstream 算得上是一种好的设计。这是由于使用 std::stringstream 的代码看起来非常地直观。As a standard component of ISO C++ library, it relieves programmers of handling exceptions–因为如果代码没有 try-catch block 的话,exception 一旦抛出,程序就会直接直接终止(调用 std::terminate)。这种解决出错的方式对于程序员来说更为爽快,因为程序在问题点终止,就很容易找到出问题的代码位置。而 C 时代的 atoi/itoa,如同我们讲到的,需要程序员关注异常,如果漏过处理异常之后(其实这很常见),程序可能带病运行。当然,由于 stringstream 总是”附着”于一个内存可以自行管理的 string 对象,所以程序员通常也不必担心任何的内存分配问题。

从设计角度出发看,std::stringstream 几乎无可挑剔。但在实际使用中,如我们在上面提到的,很多人还是愿意使用 C 中的处理方法来完成 atoi/itoa。这大概有两方面的原因:

  1. std::stringstream 在概念上的间接性。这点间接性来源于 std::stringstreamstd::string 间的关联。通常情况下,一个 std::stringstream 对象总是会与其”附着”的 std::string 对象发生联系。或者其是从一个string对象(上例中的 iss("12 14.1f"))构造而来以使用,或者其必须转化为一个string对象(上例中的 oss.str())而使用。而新手常会会直觉地写出 string a << 12 << " is int"; 这样的错误代码。
  2. 格式化输出的不便利性。相比于sprintfstd::stringstream是一个流对象,意味着其也有了更高的学习代价。简单的 sprintf,只需要翻查 escape character 的手册,就能漂亮地进行格式化的输出。而使用流进行格式化输出的话,则需要控制一个状态机。很多时候,程序员需要关心上一状态对现有输出的影响。而且通常也意味着需要输入更多的代码。很多时候程序员都会觉得非常麻烦。所以即使 sprintf 在C++代码中缺失了类型匹配、异常处理、内存管理等等,程序员依然义无反顾地使用了它。(关于这一点,boost::format 可能给出了一种跨平台的中间的解决方案)

从以上两个方面看,使用 std::stringstream 完成 atoi/itoa 虽然是更为 C++ 风格地、功能完备方式,但由于学习代价的增高以及格式化输出中的不便利性,其在实际场景中的应用也大大受限。

C++11时代

新标准中又有什么新特性等着我们呢?

itoa

到了 C++11 中,标准委员会可能是注意到这种”简单比完备”更重要的情况,于是在 C++11 中,标准增加了全局函数 std::to_string,以及 std::stoi/stol/stoll 等等函数。(最初的 paper 称之为 simple numeric access,N1982)其用法非常简单:

1
2
3
4
string s;
s += to_string(12) + " is int, ";
s += to_string(3.14f) + " is float.";
cout << s << endl;

这里的 to_string 会根据参数的类型完成相应类型地转换。在多线程中禁用,要么加锁——好坑爹

std::to_string 由于格式化目的依赖本地环境,从而从多个线程同时调用 std::to_string 可能会导致调用的部分序列化结果。 C++17 提供高性能、不依赖本地环境的替用品 std::to_chars

atoi

而:

1
2
3
string s("12");
int i = stoi(s);
cout << i << endl;

这样的代码则可以顺利完成 atoi 的任务。由于其是 C++11 引入的函数,所以具备 C 所不具备的所有的 C++ 库代码特征:根据类型的处理,抛出异常,以及自动内存管理。

小结

可以看到,std::to_string 在实际使用中可能会涉及一些字符串的连结。如我们在文章一开始提到的,C++98 中字符串连结一直是 C++ 语言被诟病性能低于C的一个重要方面。而这在 C++11 引入了右值引用后得到了很大的缓解。因此此时 std::to_string 这样的函数的实用性就大大增强了。不过 std::to_string 并不是itoa的一种终极方式。以浮点数为例, to_string 甚至连浮点数小数位显示控制这样基本的控制功能都不具备,因此其最大地特点还是突出在其易用性上。C++ 程序员不必定义一个 std::stringstream 对象就可以完成安全有效且不必关心任何内存的 itoa 工作。

而 std::stoi/stol/stoll…系列更是简单到只能完成一个数值的转换,比起总是返回 std::stringstream & 的 operator >> 比起来功能性就差很远了。后者能在一行代码中转化出多个数值。但前者最大地特点仍然突出在易用性上,不必”附着”一个 std::stringstream 类型。这对很多无需复杂 atoi 的程序而言也就足够了。