如何在C++中将字符串解析为整数?

286

如何使用C ++将一个字符串(以char *表示)解析为整数?强大且清晰的错误处理是一个优点(而不是返回零)。


22
以下是一些示例,可以参考此链接:http://www.codeproject.com/KB/recipes/Tokenizer.aspx。 它们非常高效且有一定的优雅性。 - Matthieu N.
@Beh Tou Cheh,如果您认为这是一种好的解析int的方法,请将其发布为答案。 - Eugene Yokota
1
将字符串转换为整数:C语言中同样适用。 - Ciro Santilli OurBigBook.com
17个回答

231

不应该做什么

这是我的第一条建议:不要使用stringstream。虽然一开始可能看起来很简单,但如果你想要稳健的处理和良好的错误处理,你会发现需要做很多额外的工作。

下面是一个直觉上似乎应该起作用的方法:

bool str2int (int &i, char const *s)
{
    std::stringstream ss(s);
    ss >> i;
    if (ss.fail()) {
        // not an integer
        return false;
    }
    return true;
}
这里存在一个重大问题:str2int(i, "1337h4x0r")会愉快地返回true,而i的值将变为1337。我们可以通过确保在转换后没有更多字符留在stringstream中来解决这个问题:
bool str2int (int &i, char const *s)
{
    char              c;
    std::stringstream ss(s);
    ss >> i;
    if (ss.fail() || ss.get(c)) {
        // not an integer
        return false;
    }
    return true;
}

我们解决了一个问题,但还有其他几个问题。

如果字符串中的数字不是以10为基底怎么办?我们可以尝试通过将流设置到正确的模式(例如ss << std::hex)来适应其他基数,然而这意味着调用者必须事先知道数字的基数 - 但调用者怎么可能知道呢?调用者甚至还不知道它是一个数字!他们怎么能知道它的基数呢?我们可以强制所有输入到程序的数字都必须是十进制,并拒绝十六进制或八进制输入,因为这不够灵活或健壮。这个问题没有简单的解决方案。你不能仅仅为每个基数尝试一次转换,因为十进制转换总是会成功地将八进制数字(带前导零)转换为十进制,而八进制转换可能会对某些十进制数字成功。所以现在你必须检查是否有前导零。但等等!十六进制数字也可以以前导零开头(0x...)。叹息。

即使您成功处理了以上问题,仍然存在另一个更大的问题:调用者需要区分坏的输入(例如“123foo”)和超出int范围的数字(例如32位int的“4000000000”)怎么办?使用stringstream,我们无法进行此区分。我们只知道转换成功或失败。如果它失败了,我们就没有办法知道为什么失败。正如您所见,如果您想要强大的和清晰的错误处理,stringstream并不是一个理想的选择。

这让我想到我的第二个建议:不要使用Boost的lexical_cast。考虑一下lexical_cast文档所说的:

在需要更高程度控制转换时,std::stringstream 和 std::wstringstream 提供了一条更适当的路径。在需要非流式转换时,lexical_cast 不是解决该场景的正确工具。

什么?我们已经看到stringstream的控制级别很低,然而它却说如果需要“更高的控制级别”,应该使用stringstream而不是lexical_cast。此外,因为lexical_cast只是对stringstream的包装,所以它遭受与stringstream相同的问题:对多个数字基数的支持差和差劲的错误处理。

最佳解决方案

幸运的是,有人已经解决了所有以上问题。C标准库包含strtol和其它函数,它们没有这些问题。

enum STR2INT_ERROR { SUCCESS, OVERFLOW, UNDERFLOW, INCONVERTIBLE };

STR2INT_ERROR str2int (int &i, char const *s, int base = 0)
{
    char *end;
    long  l;
    errno = 0;
    l = strtol(s, &end, base);
    if ((errno == ERANGE && l == LONG_MAX) || l > INT_MAX) {
        return OVERFLOW;
    }
    if ((errno == ERANGE && l == LONG_MIN) || l < INT_MIN) {
        return UNDERFLOW;
    }
    if (*s == '\0' || *end != '\0') {
        return INCONVERTIBLE;
    }
    i = l;
    return SUCCESS;
}

这个函数处理所有错误情况,并支持从2到36的任何进制转换,非常简单。如果base为零(默认值),它将尝试从任何基数进行转换。或者,调用者可以提供第三个参数并指定仅尝试特定基数的转换。它是强大而健壮的,能够以极小的努力处理所有错误。

使用strtol(等)的其他原因:

  • 它表现出更好的运行时性能
  • 它引入的编译时开销较少(与其他方法相比,后者需要几乎多20倍的SLOC头文件)
  • 它产生最小的代码大小

没有任何好理由使用其他方法。


26
POSIX要求strtol是线程安全的。POSIX还要求errno使用线程本地存储。即使在非POSIX系统上,几乎所有在多线程系统中实现的errno都使用线程本地存储。最新的C++标准要求errno符合POSIX规范。最新的C标准也要求errno具有线程本地存储。即使在绝对不符合POSIX的Windows上,errno也是线程安全的,因此strtol也是线程安全的。 - Dan Moulding
8
我很难理解你反对使用boost::lexical_cast的推理。正如他们所说,std::stringstream确实提供了很多控制权——你可以自己完成所有事情,从错误检查到确定基础。当前的文档这样描述它:“对于更复杂的转换,比如需要比lexical_cast的默认行为提供更严格的精度或格式控制的情况,建议使用传统的std::stringstream方法。” - fhd
13
这是在C++中不合适的C编码。标准库中包含std::stol,它会适当地抛出异常而不是返回常量值。请使用它来代替。 - fuzzyTew
32
在C++中增加std::stol之前,我写了这个答案。尽管如此,我认为说这是“在C++中使用C编码”是不公平的。当std::strtol被明确地纳入C++语言时,将其称为C编码是愚蠢的。我的答案在撰写时完全适用于C++,即使有了新的std::stol,它仍然适用。在每种编程情况下,调用可能引发异常的函数并不总是最好的选择。 - Dan Moulding
12
@fuzzyTew:磁盘空间不足是一种特殊情况。由计算机生成的格式错误的数据文件也是一种特殊情况。但用户输入中的打字错误并不是特殊情况。拥有一种能够处理正常、非异常解析失败的解析方法是很好的。 - Ben Voigt
显示剩余19条评论

183

C++11引入了一些函数来完成这个任务:stoi,stol,stoll,stoul等等。

int myNr = std::stoi(myString);

在转换错误时它会抛出异常。

即使这些新函数仍然存在与Dan指出的相同问题:它们会将字符串"11x"愉快地转换为整数"11"。

更多信息请参见:http://en.cppreference.com/w/cpp/string/basic_string/stol


6
但是它们接受的参数不止这些,其中一个参数是指向size_t类型的指针,如果它不为NULL,则将其设置为第一个未转换的字符。 - Zharf
1
是的,使用 std::stoi 的第二个参数可以检测无效输入。但你仍然需要自己编写转换函数... - CC.
就像被接受的答案所做的那样,但使用这些标准函数会更加清晰简洁。 - Zharf
2
请记住,这些函数中的第二个参数可用于告知是否已转换整个字符串。如果结果的 size_t 不等于字符串的长度,则它会提前停止。在这种情况下,它仍将返回 11,但 pos 将为 2 而不是字符串长度 3。https://coliru.stacked-crooked.com/a/cabe25d64d2ffa29 - Zoe stands with Ukraine

70

这是比atoi()更为安全的C语言方式

const char* str = "123";
int i;

if(sscanf(str, "%d", &i)  == EOF )
{
   /* error */
}

C++标准库中的stringstream:(感谢CMS

int str2int (const string &str) {
  stringstream ss(str);
  int num;
  if((ss >> num).fail())
  { 
      //ERROR 
  }
  return num;
}

使用boost库:(感谢jk

#include <boost/lexical_cast.hpp>
#include <string>

try
{
    std::string str = "123";
    int number = boost::lexical_cast< int >( str );
}
catch( const boost::bad_lexical_cast & )
{
    // Error
}

编辑:根据CMS和jk在原帖中的评论,修复了stringstream版本以处理错误。


1
请更新您的stringstream版本,包括对stringstream::fail()的检查(如“健壮和清晰的错误处理”问题所请求的)。 - jk.
2
你的stringstream版本将接受像“10haha”这样的内容而不会抱怨。 - Johannes Schaub - litb
3
如果你想要与lexical_cast相同的处理方式,将其改为(!(ss >> num).fail() && (ss >> ws).eof()),而不是((ss >> num).fail())。 - Johannes Schaub - litb
3
使用C++标准库中的stringstream方法,即使进行.fail()检查,也无法处理如"12-SomeString"这样的字符串。 - captonssj
C++11现在包括了标准的快速函数。 - fuzzyTew

22

老牌的 C 语言方法仍然有效。我建议使用 strtol 或 strtoul 函数。你可以通过返回状态和 'endPtr' 来输出良好的诊断信息。它还可以很好地处理多个进制。


5
在C++编程时,请不要使用旧的C语言写法。在C++中,有更好/更简单/更干净/更现代/更安全的方法来实现这一点! - jk.
32
当人们关注“更现代”的解决问题方式时,这很有趣。 - J Miller
1
@Jason,在我看来,相比于C语言的编程思想,更强的类型安全和错误处理是一种更现代的理念。 - Eugene Yokota
6
我已经查看了其他答案,到目前为止没有更好/更容易/更清晰或更安全的解决方案。帖子中提到有一个char *。这限制了你可以获得的安全性的程度 :) - Chris Arguin
1
@JMiller 公正地说,在C和C++的情况下,这是因为老式的方式通常有明显的缺陷,从笨拙、低效到极端不安全。在std::strtol的情况下,你没有办法知道你是否成功解析了一个0或者函数是否失败,除非你手动检查字符串是否解析为0,而当你这样做时,你会不必要地重复工作。更现代化的方法(std::from_chars)不仅告诉你函数何时失败,而且还告诉你为什么失败,这有助于向最终用户提供反馈。 - Pharap
显示剩余6条评论

21
你可以使用Boost的lexical_cast,它将其包装在更通用的接口中。 lexical_cast<Target>(Source)在失败时会抛出bad_lexical_cast

13
lexical_cast的性能极差且效率低下。 - Matthieu N.
3
Boost的更新使性能有了很大提升:http://www.boost.org/doc/libs/1_49_0/doc/html/boost_lexical_cast/performance.html (另见https://dev59.com/JXM_5IYBdhLWcg3wvF3J) - flies

16

从C++17开始,您可以使用位于<charconv>头文件中的std::from_chars,如此处所述。

例如:

#include <iostream>
#include <charconv>
#include <array>

int main()
{
    char const * str = "42";
    int value = 0;

    std::from_chars_result result = std::from_chars(std::begin(str), std::end(str), value);

    if(result.error == std::errc::invalid_argument)
    {
      std::cout << "Error, invalid format";
    }
    else if(result.error == std::errc::result_out_of_range)
    {
      std::cout << "Error, value too big for int range";
    }
    else
    {
      std::cout << "Success: " << result;
    }
}

作为额外的奖励,它还可以处理其他进制,比如十六进制。

16
你可以使用 C++ 标准库中的 stringstream:

你可以使用C++标准库中的stringstream:

stringstream ss(str);
int x;
ss >> x;

if(ss) { // <-- error handling
  // use x
} else {
  // not a number
}

如果在尝试读取整数时遇到非数字字符,流状态将被设置为失败。

有关C ++中错误处理和流的陷阱,请参见Stream pitfalls.


3
即使进行了'stream state'检查,C++的stringstream方法对于类似于"12-SomeString"这样的字符串仍然无法正常工作。 - captonssj

9

您可以使用stringstream的

int str2int (const string &str) {
  stringstream ss(str);
  int num;
  ss >> num;
  return num;
}

5
但这并未处理任何错误。你需要检查流是否存在故障。 - jk.
1
对的,你需要检查流 如果((ss >> num).fail()){ //错误 } - Christian C. Salvadó
2
C++ 的 stringstream 方法即使进行了“流状态”检查,在处理诸如“12-SomeString”这样的字符串时仍无法正常工作。 - captonssj

8

我认为以下这三个链接总结得很好:

stringstream和lexical_cast的解决方案基本相同,因为lexical_cast使用了stringstream。

lexical_cast的一些特殊化采用不同的方法,请参见http://www.boost.org/doc/libs/release/boost/lexical_cast.hpp以获取详细信息。整数和浮点数现在已经专门针对整数转换成字符串进行了特殊化处理。

可以将lexical_cast特殊化为自己的需求并使其快速。这将是满足所有人、清晰简单的最终解决方案。

上述文章已经比较了不同的整数<->字符串转换方法。以下方法是有意义的:旧的c方式、spirit.karma、fastformat、简单的naive循环。

对于整数转换成字符串,lexical_cast是可以接受的。

使用lexical_cast将字符串转换为整数不是一个好主意,因为它比atoi慢10-40倍,这取决于所使用的平台/编译器。

Boost.Spirit.Karma似乎是最快的将整数转换成字符串的库。

ex.: generate(ptr_char, int_, integer_number);

从上面提到的文章中,最基本简单的循环方法是将字符串转换为整数的最快方法,但显然不是最安全的方法,strtol()似乎是一种更安全的解决方案。

int naive_char_2_int(const char *p) {
    int x = 0;
    bool neg = false;
    if (*p == '-') {
        neg = true;
        ++p;
    }
    while (*p >= '0' && *p <= '9') {
        x = (x*10) + (*p - '0');
        ++p;
    }
    if (neg) {
        x = -x;
    }
    return x;
}

7

C++字符串工具包(StrTk)提供以下解决方案:

static const std::size_t digit_table_symbol_count = 256;
static const unsigned char digit_table[digit_table_symbol_count] = {
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF - 0x07
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x08 - 0x0F
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x10 - 0x17
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x18 - 0x1F
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x20 - 0x27
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x28 - 0x2F
   0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 0x30 - 0x37
   0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x38 - 0x3F
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x40 - 0x47
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x48 - 0x4F
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x50 - 0x57
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x58 - 0x5F
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x60 - 0x67
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x68 - 0x6F
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x70 - 0x77
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x78 - 0x7F
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x80 - 0x87
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x88 - 0x8F
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x90 - 0x97
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x98 - 0x9F
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xA0 - 0xA7
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xA8 - 0xAF
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xB0 - 0xB7
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xB8 - 0xBF
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xC0 - 0xC7
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xC8 - 0xCF
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xD0 - 0xD7
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xD8 - 0xDF
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xE0 - 0xE7
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xE8 - 0xEF
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xF0 - 0xF7
   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF  // 0xF8 - 0xFF
 };

template<typename InputIterator, typename T>
inline bool string_to_signed_type_converter_impl_itr(InputIterator begin, InputIterator end, T& v)
{
   if (0 == std::distance(begin,end))
      return false;
   v = 0;
   InputIterator it = begin;
   bool negative = false;
   if ('+' == *it)
      ++it;
   else if ('-' == *it)
   {
      ++it;
      negative = true;
   }
   if (end == it)
      return false;
   while(end != it)
   {
      const T digit = static_cast<T>(digit_table[static_cast<unsigned int>(*it++)]);
      if (0xFF == digit)
         return false;
      v = (10 * v) + digit;
   }
   if (negative)
      v *= -1;
   return true;
}

InputIterator可以是无符号char*、char*或std::string 迭代器,T应该是带符号整数,比如signed int、int或long。


1
警告:这个实现看起来不错,但据我所知它无法处理溢出。 - Vinnie Falco
2
代码没有处理溢出。使用字符串输入时,v = (10 * v) + digit; 会不必要地溢出,特别是当文本值为 INT_MIN 时。相对于简单的 digit >= '0' && digit <= '9',表格的价值存疑。 - chux - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接