如何迭代一个字符串中的单词?

3346

如何迭代由空格分隔的单词组成的字符串?请注意,我不感兴趣的是 C 字符串函数或那种字符操作/访问。我更喜欢优雅而不是效率。我的当前解决方案:

#include <iostream>
#include <sstream>
#include <string>

using namespace std;

int main() {
    string s = "Somewhere down the road";
    istringstream iss(s);

    do {
        string subs;
        iss >> subs;
        cout << "Substring: " << subs << endl;
    } while (iss);
}

687
伙计,我觉得“优雅”只是一个花哨的说法,意思就是“看起来漂亮的效率”。不要因为某些方法不在模板中,就回避使用C函数和快速方法来完成任何任务。;) - user19302
19
当(iss)时,获取子串并将其存入subs中,然后输出"Substring:"和sub的值,并换行输出。 - isekaijin
28
@Eduardo:那也是错误的...你需要测试iss在尝试流另一个值和使用该值之间的状态,即string sub; while (iss >> sub) cout << "Substring: " << sub << '\n'; - Tony Delroy
14
C++中默认的各种选项:http://www.cplusplus.com/faq/sequences/strings/split/ - hB0
27
优雅不仅仅是漂亮的高效,还包括低代码量和高易读性。在我看来,优雅并不是效率的替代品,而是可维护性的体现。 - Matt
显示剩余3条评论
84个回答

2581

我使用这个函数通过指定分隔符来切割字符串。第一个函数将结果放在一个预先构建的向量中,而第二个函数返回一个新的向量。

#include <string>
#include <sstream>
#include <vector>
#include <iterator>

template <typename Out>
void split(const std::string &s, char delim, Out result) {
    std::istringstream iss(s);
    std::string item;
    while (std::getline(iss, item, delim)) {
        *result++ = item;
    }
}

std::vector<std::string> split(const std::string &s, char delim) {
    std::vector<std::string> elems;
    split(s, delim, std::back_inserter(elems));
    return elems;
}
请注意,此解决方案不会跳过空令牌,因此以下代码会找到4个元素之一为空:
std::vector<std::string> x = split("one:two::three", ':');

96
为了避免跳过空标记,需要进行 empty() 检查:if (!item.empty()) elems.push_back(item)。该代码意思是,如果 item 不为空,则将其添加到 elems 的末尾。 - David G
13
delim包含两个字符,如->,该怎么办? - herohuyongtao
9
@herohuyongtao,这个解决方案只适用于单字符分隔符。 - Evan Teran
4
@JeshwanthKumarNK,这不是必要的,但是如果您想要将结果直接传递给一个函数,例如 f(split(s, d, v)),同时仍然享受到预先分配的 vector 的好处,那么这就是一个好方法。请注意,这并非必需。 - Evan Teran
10
注意:split("one:two::three", ':') 和 split("one:two::three:", ':') 返回相同的值。 - dshin
显示剩余22条评论

1507

说句实话,这里有另一种从输入字符串中提取标记的方法,仅依赖于标准库设施。它展示了STL设计背后的强大和优美。

#include <iostream>
#include <string>
#include <sstream>
#include <algorithm>
#include <iterator>

int main() {
    using namespace std;
    string sentence = "And I feel fine...";
    istringstream iss(sentence);
    copy(istream_iterator<string>(iss),
         istream_iterator<string>(),
         ostream_iterator<string>(cout, "\n"));
}

可以使用相同的通用copy算法,将提取的令牌插入到容器中,而不是将它们复制到输出流中。

vector<string> tokens;
copy(istream_iterator<string>(iss),
     istream_iterator<string>(),
     back_inserter(tokens));

... 或直接创建 vector

vector<string> tokens{istream_iterator<string>{iss},
                      istream_iterator<string>{}};

177
可以指定一个分隔符吗?比如按逗号进行分割? - l3dx
17
在这种情况下,“\n”不是分隔符,而是用于将内容输出到cout的定界符。 - huy
802
这不是一个好的解决方案,因为它并没有考虑到其他分隔符,所以不具有可扩展性和可维护性。 - ABCD
44
实际上,这也可以使用其他分隔符完美地工作(尽管有些操作可能会有些丑陋)。您可以创建一个 ctype facet ,将所需的分隔符分类为空格,并创建一个包含该 facet 的 locale ,然后在提取字符串之前将 stringstream 注入该 locale 。 - Jerry Coffin
67
“该字符串可以假定由用空格分隔的单词组成” - 嗯,听起来似乎不是这个问题的不良解决方案。 “不可扩展且不可维护” - 哈哈,说得好。 - Christian Rau
显示剩余23条评论

873
一种可能的解决方案是使用Boost库:

#include <boost/algorithm/string.hpp>
std::vector<std::string> strs;
boost::split(strs, "string to split", boost::is_any_of("\t "));

这种方法可能比使用stringstream方法更快。而且,由于这是一个通用的模板函数,它可以用来拆分其他类型的字符串(如wchar、UTF-8等)并使用各种分隔符。

有关详细信息,请参见文档


37
速度在这里无关紧要,因为这两种情况比类似于strtok的函数慢得多。 - Tom
55
对于那些尚未拥有boost库的人来说,bcp需要复制1000多个文件 :) - Roman Starkov
14
警告:当传入空字符串("")时,该方法返回一个包含 "" 字符串的向量。因此在拆分之前添加 "if (!string_to_split.empty())" 条件判断。 - Offirmo
29
嵌入式开发者并不都在使用boost。 - ACK_stoverflow
33
补充一句:我只有在必要的情况下才使用Boost,通常我更喜欢添加到自己的代码库中,这样可以实现小而精确的特定代码,以完成给定的目标。这样,代码是非公开的、高效的、轻量级的和可移植的。Boost 也有它的用武之地,但我认为用它来分词有点过度设计:就像你不会把整个房子运到工程公司去钉一颗墙上的钉子挂画一样……他们可能做得很好,但利弊相比明显不划算。 - GMasucci
显示剩余13条评论

412
#include <vector>
#include <string>
#include <sstream>

int main()
{
    std::string str("Split me by whitespaces");
    std::string buf;                 // Have a buffer string
    std::stringstream ss(str);       // Insert the string into a stream

    std::vector<std::string> tokens; // Create vector to hold our words

    while (ss >> buf)
        tokens.push_back(buf);

    return 0;
}

25
如果您在while条件中使用getline,则还可以根据其他分隔符进行拆分,例如,要按逗号拆分,请使用while(getline(ss, buff, ',')) - Ali
1
我不明白这个为什么有400个赞。这基本上与OQ中的内容相同:使用stringstream和>>。这就是OP甚至在问题历史的第1个修订版中所做的。 - Thomas Weller

201
一个高效、小巧而优雅的解决方案,使用模板函数实现:
template <class ContainerT>
void split(const std::string& str, ContainerT& tokens,
           const std::string& delimiters = " ", bool trimEmpty = false)
{
   std::string::size_type pos, lastPos = 0, length = str.length();
   
   using value_type = typename ContainerT::value_type;
   using size_type = typename ContainerT::size_type;
   
   while (lastPos < length + 1)
   {
      pos = str.find_first_of(delimiters, lastPos);
      if (pos == std::string::npos)
         pos = length;

      if (pos != lastPos || !trimEmpty)
         tokens.emplace_back(value_type(str.data() + lastPos,
               (size_type)pos - lastPos));

      lastPos = pos + 1;
   }
}

我通常选择使用std::vector<std::string>类型作为我的第二个参数(ContainerT)... 但是在某些情况下,可能更喜欢使用list<...>而不是vector<...>
它还允许您通过最后一个可选参数来指定是否修剪结果中的空标记。
它只需要通过<string>包含std::string。它不明确使用流或boost库,但可以接受其中一些类型。
此外,自C++-17以来,您可以使用std::vector<std::string_view>,它比使用std::string更快且更节省内存。以下是修订版本,还支持容器作为返回类型:
#include <vector>
#include <string_view>
#include <utility>
    
template < typename StringT,
           typename DelimiterT = char,
           typename ContainerT = std::vector<std::string_view> >
ContainerT split(StringT const& str, DelimiterT const& delimiters = ' ', bool trimEmpty = true, ContainerT&& tokens = {})
{
    typename StringT::size_type pos, lastPos = 0, length = str.length();

    while (lastPos < length + 1)
    {
        pos = str.find_first_of(delimiters, lastPos);
        if (pos == StringT::npos)
            pos = length;

      if (pos != lastPos || !trimEmpty)
            tokens.emplace_back(str.data() + lastPos, pos - lastPos);

        lastPos = pos + 1;
    }

    return std::forward<ContainerT>(tokens);
}

已经注意到不要制作任何不必要的副本。
这将允许以下两种情况之一:
for (auto const& line : split(str, '\n'))

或者:

auto& lines = split(str, '\n');

返回默认模板容器类型std::vector<std::string_view>
要获取特定的容器类型,或者传递一个现有的容器,请使用tokens输入参数,可以是具有类型的初始容器或现有的容器变量:
auto& lines = split(str, '\n', false, std::vector<std::string>());

或者:

std::vector<std::string> lines;
split(str, '\n', false, lines);

5
我很喜欢这个,但对于使用g++(和可能的良好实践),任何人都希望使用typedef和typename: typedef ContainerT Base; typedef typename Base::value_type ValueType; typedef typename ValueType::size_type SizeType;然后相应地替换value_type和size_type。 - user199973
13
对于我们中那些对模板内容和第一条评论完全陌生的人来说,一个包含所需头文件的用法示例将会非常有帮助。 - Wes Miller
3
啊,好吧,我想通了。我把aws的C++代码放到tokenize()函数体内,然后编辑tokens.push_back()行,将ContainerT::value_type更改为ValueType,并将(ContainerT::value_type::size_type)更改为(SizeType)。修复了g++一直在抱怨的问题。只需调用tokenize(some_string, some_vector)即可。 - Wes Miller
2
除了在样本数据上运行一些性能测试外,我主要将其简化为尽可能少的指令和尽可能少的内存复制,通过使用一个子字符串类来实现,该类仅引用其他字符串中的偏移量/长度。(我自己编写了这个类,但也有其他实现方法)。不幸的是,在这方面没有太多其他可以改进的地方,但是可以进行渐进式的增加。 - Marius
3
trimEmpty = true 时,这是正确的输出。需要注意的是,在这个答案中,"abo" 不是分隔符,而是分隔符字符的列表。可以简单地修改它,使其接受单个分隔符字符串(我认为 str.find_first_of 应该改为 str.find_first,但我可能有错……无法测试)。 - Marius
显示剩余7条评论

177

下面是另一种解决方案。它简洁且相当高效:

std::vector<std::string> split(const std::string &text, char sep) {
  std::vector<std::string> tokens;
  std::size_t start = 0, end = 0;
  while ((end = text.find(sep, start)) != std::string::npos) {
    tokens.push_back(text.substr(start, end - start));
    start = end + 1;
  }
  tokens.push_back(text.substr(start));
  return tokens;
}

它可以轻松地进行模板化以处理字符串分隔符、宽字符串等。

请注意,将""进行拆分会得到一个空字符串,而将","(即 sep)进行拆分会得到两个空字符串。

还可以轻松扩展以跳过空标记:

std::vector<std::string> split(const std::string &text, char sep) {
    std::vector<std::string> tokens;
    std::size_t start = 0, end = 0;
    while ((end = text.find(sep, start)) != std::string::npos) {
        if (end != start) {
          tokens.push_back(text.substr(start, end - start));
        }
        start = end + 1;
    }
    if (end != start) {
       tokens.push_back(text.substr(start));
    }
    return tokens;
}
如果需要在多个分隔符处拆分字符串并跳过空令牌,则可以使用此版本:
std::vector<std::string> split(const std::string& text, const std::string& delims)
{
    std::vector<std::string> tokens;
    std::size_t start = text.find_first_not_of(delims), end = 0;

    while((end = text.find_first_of(delims, start)) != std::string::npos)
    {
        tokens.push_back(text.substr(start, end - start));
        start = text.find_first_not_of(delims, end);
    }
    if(start != std::string::npos)
        tokens.push_back(text.substr(start));

    return tokens;
}

10
第一个版本很简单,完美地完成了工作。我唯一会做的改变就是直接返回结果,而不是将其作为参数传递。 - gregschlom
7
即使在 C++11 之前,大多数编译器不会通过 NRVO(命名返回值优化)优化掉返回副本的操作吗?(+1,非常简洁) - Marcelo Cantos
13
在所有答案中,这似乎是其中一个最有吸引力和灵活性的。再加上带有定界符的getline,尽管它是一个不太明显的解决方案。C++11标准没有类似的功能吗?现在的C++11支持打孔卡吗? - Spacen Jasset
3
@LearnCocos2D 请不要通过编辑修改帖子的含义。这种行为是有意设计的。它与Python的split操作符具有相同的行为方式。我会添加一条注释来明确这一点。 - Alec Thomas
3
建议使用std::string::size_type而不是int,因为一些编译器可能会出现有符号/无符号警告。 - Pascal Kesseli
显示剩余8条评论

143

这是我最喜欢的遍历字符串的方法。对于每个单词,您可以自由进行任何操作。

string line = "a line of text to iterate through";
string word;

istringstream iss(line, istringstream::in);

while( iss >> word )     
{
    // Do something on `word` here...
}

1
能否将 word 声明为 char 类型? - abatishchev
1
抱歉,abatishchev,C++ 不是我的强项。但我想添加一个内部循环来遍历每个单词中的每个字符应该不难。但现在我认为当前的循环依赖于空格来分隔单词。除非你知道每个空格之间只有一个字符,否则你可以将 "word" 转换为 char... 抱歉我不能提供更多帮助,我一直想重新学习 C++。 - gnomed
12
如果你将单词声明为字符,它将迭代每个非空格字符。很容易尝试一下:stringstream ss("Hello World, this is*@#&$(@ a string"); char c; while(ss >> c) cout << c; - Wayne Werner
1
我不明白这是如何得到了140个赞。这基本上与OQ中的内容相同:使用stringstream和>>从中读取。这正是OP在问题历史的第1个修订版本中所做的。 - Thomas Weller

89

这与 Stack Overflow 上的问题 如何在 C++ 中对字符串进行分词? 相似。需要使用 Boost 外部库。

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

using namespace std;
using namespace boost;

int main(int argc, char** argv)
{
    string text = "token  test\tstring";

    char_separator<char> sep(" \t");
    tokenizer<char_separator<char>> tokens(text, sep);
    for (const string& t : tokens)
    {
        cout << t << "." << endl;
    }
}

这是否实现了所有令牌的副本,还是只保留当前令牌的起始和结束位置? - einpoklum

71

我喜欢以下的方法,因为它将结果存入向量中,并支持字符串作为分隔符,并且可以控制保留空值。但是,这种方法看起来不太好。

#include <ostream>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>
using namespace std;

vector<string> split(const string& s, const string& delim, const bool keep_empty = true) {
    vector<string> result;
    if (delim.empty()) {
        result.push_back(s);
        return result;
    }
    string::const_iterator substart = s.begin(), subend;
    while (true) {
        subend = search(substart, s.end(), delim.begin(), delim.end());
        string temp(substart, subend);
        if (keep_empty || !temp.empty()) {
            result.push_back(temp);
        }
        if (subend == s.end()) {
            break;
        }
        substart = subend + delim.size();
    }
    return result;
}

int main() {
    const vector<string> words = split("So close no matter how far", " ");
    copy(words.begin(), words.end(), ostream_iterator<string>(cout, "\n"));
}
当然,Boost有一个名为 split() 的函数部分地实现了这个功能。如果你真的指的是任何类型的空格符,那么使用 Boost 的 split 和 is_any_of() 就非常好用。

终于有一个解决方案可以正确处理字符串两侧的空令牌了。 - fmuecke

59

STL没有提供这样的方法。

但是,您可以使用C语言的strtok()函数,通过使用std::string::c_str()成员,或者您可以编写自己的函数。这里是我在快速谷歌搜索后找到的一个代码示例("STL string split"):

void Tokenize(const string& str,
              vector<string>& tokens,
              const string& delimiters = " ")
{
    // Skip delimiters at beginning.
    string::size_type lastPos = str.find_first_not_of(delimiters, 0);
    // Find first "non-delimiter".
    string::size_type pos     = str.find_first_of(delimiters, lastPos);

    while (string::npos != pos || string::npos != lastPos)
    {
        // Found a token, add it to the vector.
        tokens.push_back(str.substr(lastPos, pos - lastPos));
        // Skip delimiters.  Note the "not_of"
        lastPos = str.find_first_not_of(delimiters, pos);
        // Find next "non-delimiter"
        pos = str.find_first_of(delimiters, lastPos);
    }
}

摘自:http://oopweb.com/CPP/Documents/CPPHOWTO/Volume/C++Programming-HOWTO-7.html

如果您对代码示例有疑问,请留言,我会进行解释。

仅仅因为它没有实现名为iterator的typedef或重载<<运算符并不意味着它是糟糕的代码。我经常使用C函数。例如,printfscanf都比std::cinstd::cout(显著)更快,fopen语法对于二进制类型更加友好,而且它们也倾向于产生更小的EXE文件。

不要被这个"优雅胜过性能"的说法所迷惑。


11
让我猜猜:是因为strtok不可重入吗? - paercebal
44
@Nelson 切记不要将 string.c_str() 传递给 strtok 函数!strtok 会破坏输入的字符串(用 '\0' 字符替换每个找到的分隔符),而 c_str() 返回一个不可修改的字符串。 - Evan Teran
3
@Nelson:在你最后一条评论中,那个数组需要的大小是str.size() + 1。但我同意你的观点,为了“美观”的理由而避免使用C函数是愚蠢的。 - j_random_hacker
例如,printf和scanf都比cin和cout快,因为默认情况下启用了同步。 - paulm
2
@paulm:不,C++流的慢速是由facet(面向对象编程中的一种概念)引起的。即使在禁用同步的情况下(对于无法同步的stringstreams也是如此),它们仍然比stdio.h函数慢。 - Ben Voigt
显示剩余6条评论

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