C++11中如何使用字符串分割

83

如何使用C++11最简单地拆分字符串?

我见过这个帖子中使用的方法,但我认为应该有一种更简洁的方式来使用新标准。

编辑:我想要一个vector<string>作为结果,并能够在单个字符上进行分隔。


1
在空格上分割?我认为C++11在这里没有添加任何内容,我认为被接受的答案仍然是最好的方法。 - Mooing Duck
你在分割后想要做什么?输出到cout吗?还是获取一个子字符串的向量? - balki
这不是正则表达式解析的用途吗? - Nicol Bolas
1
我认为得票最高的答案是最好的。 - Mansuro
1
可能是遍历字符串中单词的最优雅方法的重复问题。 - underscore_d
使用c++17,您可以使用string_view,这将使字符串副本变得多余(有附加条件),或者如果您不介意添加库,您可以使用abseil https://abseil.io/tips/10。我会给出这个答案,但问题特别要求c++11。 - Paul Rooney
11个回答

68

std::regex_token_iterator 基于正则表达式执行通用的分词操作。对于单个字符的简单分割,它可能过于复杂,但它能够起作用而且并不太冗长:

std::vector<std::string> split(const string& input, const string& regex) {
    // passing -1 as the submatch index parameter performs splitting
    std::regex re(regex);
    std::sregex_token_iterator
        first{input.begin(), input.end(), re, -1},
        last;
    return {first, last};
}

3
需要说明这是针对 MSFT 特定的,而在 POSIX 系统上不存在。 - jackyalcine
看起来它在boost中也是可用的。 - phs
16
regex_token_iterator 是 C++11 中定义的,但是直到 GCC 版本 4.9 才原生支持它(可参考这里)。 在早期版本的 GCC 中,您可以使用 Boost regex - Brent Bradburn
2
一个更好的正则表达式是 \\s+,用于匹配空格。此外,在 gcc 4.9 上,我必须显式地使用字符串参数初始化正则表达式,然后将其传递给迭代器构造函数。只需添加 regex re{regex_str}; 作为第一行,其中 regex_str 是示例中称为 regex 的字符串,然后传递 re 即可。 - Alfred Bratterud
这个很好用,但是即使我已经阅读了文档,我仍然不理解以std::sregex_token_iterator...开头的那一行的语法。这是两个名为first和last的迭代器吗?为什么last没有被设置为任何值 - 我想应该有某种默认值...? - code_fodder
显示剩余7条评论

38

这里是一个(可能更简洁的)基于您提到的帖子的字符串分割方法。

#include <string>
#include <sstream>
#include <vector>
std::vector<std::string> split(const std::string &s, char delim) {
  std::stringstream ss(s);
  std::string item;
  std::vector<std::string> elems;
  while (std::getline(ss, item, delim)) {
    elems.push_back(item);
    // elems.push_back(std::move(item)); // if C++11 (based on comment from @mchiasson)
  }
  return elems;
}

12
如果您正在使用C++11,将元素插入到向量中时,您可以使用以下代码避免字符串复制:elems.push_back(std::move(item)); 注意不要改变原意,并保证语言通俗易懂。 - mchiasson
首先,std::move 并不会移动任何东西,它只是将类型转换。item 在栈上定义,它将被复制到向量的堆上。因此,在这里使用 std::move 不会避免复制。 - Jack Zhang
3
即使item在堆栈上定义,但内部数据指针指向在堆上分配的数据。通过使用std::move,将选择push_back(std::string&&)重载,导致向量中的std::string对象通过移动进行初始化--仅复制数据指针,而不是复制整个缓冲区。 - Tyg13
1
编译器不能自动执行std::move优化吗? - dshin
如果字符串以分隔符结尾(例如,在行末的空CSV列),它不会返回空字符串。它只会返回一个较少的字符串。例如:1,2,3,4\nA,B,C, - Paul

24

以下是使用boost将字符串拆分并填充提取元素的示例。

#include <boost/algorithm/string.hpp>

std::string my_input("A,B,EE");
std::vector<std::string> results;

boost::algorithm::split(results, my_input, boost::is_any_of(","));

assert(results[0] == "A");
assert(results[1] == "B");
assert(results[2] == "EE");

20

受其他答案启发,这是另一种正则表达式解决方案(链接),但希望更短且易于阅读:

std::string s{"String to split here, and here, and here,..."};
std::regex regex{R"([\s,]+)"}; // split on space and comma
std::sregex_token_iterator it{s.begin(), s.end(), regex, -1};
std::vector<std::string> words{it, {}};

好的回答。在哪里可以找到初始化向量的语法 words{it, {}}; 的描述? - Gardener
3
在这里找到了答案:空花括号作为范围的结尾 - Gardener
2
如果有其他人也在疑惑:sregex_token_iterator 构造函数的 -1 参数会导致对象在匹配之间迭代片段。默认值 0 会迭代与正则表达式匹配的片段。更多详情请参见此处 - xperroni

6

我不知道这是否更简洁,但对于那些更熟悉动态语言(如javascript)的人来说,这可能更容易理解。它只使用了C++11的auto和基于范围的for循环特性。

#include <string>
#include <cctype>
#include <iostream>
#include <vector>

using namespace std;

int main()
{
  string s = "hello  how    are you won't you tell me your name";
  vector<string> tokens;
  string token;

  for (const auto& c: s) {
    if (!isspace(c))
      token += c;
    else {
      if (token.length()) tokens.push_back(token);
      token.clear();
    }
  }

  if (token.length()) tokens.push_back(token);
     
  return 0;
}

16
为什么不使用 for (auto const c : s) {...} - Sebastian Mach

4

我选择使用boost::tokenizer,但是我没有进行大量数据的测试和处理。

这里是来自boost文档的示例代码,经过了Lambda修改:

#include <iostream>
#include <boost/tokenizer.hpp>
#include <string>
#include <vector>

int main()
{
   using namespace std;
   using namespace boost;

   string s = "This is,  a test";
   vector<string> v;
   tokenizer<> tok(s);
   for_each (tok.begin(), tok.end(), [&v](const string & s) { v.push_back(s); } );
   // result 4 items: 1)This 2)is 3)a 4)test
   return 0;
}

7
在C++11中, for (auto && s : tok) { v.push_back(s); } - Drew Dormann

4
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>


using namespace std;

vector<string> split(const string& str, int delimiter(int) = ::isspace){
  vector<string> result;
  auto e=str.end();
  auto i=str.begin();
  while(i!=e){
    i=find_if_not(i,e, delimiter);
    if(i==e) break;
    auto j=find_if(i,e, delimiter);
    result.push_back(string(i,j));
    i=j;
  }
  return result;
}

int main(){
  string line;
  getline(cin,line);
  vector<string> result = split(line);
  for(auto s: result){
    cout<<s<<endl;
  }
}

为什么使用 int 作为分隔符,而且为什么是 int delimiter(int) 中的 (int) - Ela782
2
@Ela782 这是一个函数指针参数,一个接受 int 参数并返回 int 的函数。默认值是 isspace 函数。 - Fsmv

3
#include <string>
#include <vector>
#include <sstream>

inline vector<string> split(const string& s) {
    vector<string> result;
    istringstream iss(s);
    for (string w; iss >> w; )
        result.push_back(w);
    return result;
}

1
我认为这篇帖子:https://dev59.com/CGgt5IYBdhLWcg3w7BoE 更好地使用istringstream实现。 - eminemence
将第二个s重命名为w后,它运行得很好。请更新答案以便在任何地方都可以编译。 - Carlos Pinzón
我认为为了性能,你可以写成: result.emplace_back(std::move(w)); - Кое Кто

2
这是一个仅使用std::string::find()的C++11解决方案,可以处理任意长度的分隔符。通过输出迭代器(通常在我的代码中为std::back_inserter)输出解析的标记。
我没有使用UTF-8进行测试,但只要输入和分隔符都是有效的UTF-8字符串,它应该可以工作。
#include <string>

template<class Iter>
Iter splitStrings(const std::string &s, const std::string &delim, Iter out)
{
    if (delim.empty()) {
        *out++ = s;
        return out;
    }
    size_t a = 0, b = s.find(delim);
    for ( ; b != std::string::npos;
          a = b + delim.length(), b = s.find(delim, a))
    {
        *out++ = std::move(s.substr(a, b - a));
    }
    *out++ = std::move(s.substr(a, s.length() - a));
    return out;
}

一些测试用例:

void test()
{
    std::vector<std::string> out;
    size_t counter;

    std::cout << "Empty input:" << std::endl;        
    out.clear();
    splitStrings("", ",", std::back_inserter(out));
    counter = 0;        
    for (auto i = out.begin(); i != out.end(); ++i, ++counter) {
        std::cout << counter << ": " << *i << std::endl;
    }

    std::cout << "Non-empty input, empty delimiter:" << std::endl;        
    out.clear();
    splitStrings("Hello, world!", "", std::back_inserter(out));
    counter = 0;        
    for (auto i = out.begin(); i != out.end(); ++i, ++counter) {
        std::cout << counter << ": " << *i << std::endl;
    }

    std::cout << "Non-empty input, non-empty delimiter"
                 ", no delimiter in string:" << std::endl;        
    out.clear();
    splitStrings("abxycdxyxydefxya", "xyz", std::back_inserter(out));
    counter = 0;        
    for (auto i = out.begin(); i != out.end(); ++i, ++counter) {
        std::cout << counter << ": " << *i << std::endl;
    }

    std::cout << "Non-empty input, non-empty delimiter"
                 ", delimiter exists string:" << std::endl;        
    out.clear();
    splitStrings("abxycdxy!!xydefxya", "xy", std::back_inserter(out));
    counter = 0;        
    for (auto i = out.begin(); i != out.end(); ++i, ++counter) {
        std::cout << counter << ": " << *i << std::endl;
    }

    std::cout << "Non-empty input, non-empty delimiter"
                 ", delimiter exists string"
                 ", input contains blank token:" << std::endl;        
    out.clear();
    splitStrings("abxycdxyxydefxya", "xy", std::back_inserter(out));
    counter = 0;        
    for (auto i = out.begin(); i != out.end(); ++i, ++counter) {
        std::cout << counter << ": " << *i << std::endl;
    }

    std::cout << "Non-empty input, non-empty delimiter"
                 ", delimiter exists string"
                 ", nothing after last delimiter:" << std::endl;        
    out.clear();
    splitStrings("abxycdxyxydefxy", "xy", std::back_inserter(out));
    counter = 0;        
    for (auto i = out.begin(); i != out.end(); ++i, ++counter) {
        std::cout << counter << ": " << *i << std::endl;
    }

    std::cout << "Non-empty input, non-empty delimiter"
                 ", only delimiter exists string:" << std::endl;        
    out.clear();
    splitStrings("xy", "xy", std::back_inserter(out));
    counter = 0;        
    for (auto i = out.begin(); i != out.end(); ++i, ++counter) {
        std::cout << counter << ": " << *i << std::endl;
    }
}

预期输出:

空输入:
0: 
非空输入,空分隔符:
0: 你好,世界!
非空输入,非空分隔符,字符串中没有分隔符:
0: abxycdxyxydefxya
非空输入,非空分隔符,在字符串中存在分隔符:
0: ab
1: cd
2: !!
3: def
4: a
非空输入,非空分隔符,在字符串中存在分隔符,输入包含空标记:
0: ab
1: cd
2: 
3: def
4: a
非空输入,非空分隔符,在字符串中存在分隔符,最后一个分隔符之后没有内容:
0: ab
1: cd
2: 
3: def
4: 
非空输入,非空分隔符,只有分隔符在字符串中存在:
0: 
1: 

2

这是我的答案。冗长、易读且高效。

std::vector<std::string> tokenize(const std::string& s, char c) {
    auto end = s.cend();
    auto start = end;

    std::vector<std::string> v;
    for( auto it = s.cbegin(); it != end; ++it ) {
        if( *it != c ) {
            if( start == end )
                start = it;
            continue;
        }
        if( start != end ) {
            v.emplace_back(start, it);
            start = end;
        }
    }
    if( start != end )
        v.emplace_back(start, end);
    return v;
}

1
除非有人想使用UTF8或多个字符。 - v010dya
如果允许使用strchr,它可能有助于您的实现。请参考http://www.cplusplus.com/reference/cstring/strchr/。 - phoad

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