如何遍历字符串中的单词?

我正在尝试遍历字符串中的单词。

可以假定字符串由空格分隔的单词组成。

请注意,我对 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);
}

有没有更优雅的方法可以做到这一点?

答案

我用它用定界符分割字符串。第一个将结果放入预先构造的向量中,第二个返回一个新向量。

#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", ':');

值得的是,这是另一种仅依靠标准库工具从输入字符串中提取令牌的方法。这是 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>{}};

使用 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)。

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

#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;
}

对于那些不愿意为了代码大小而牺牲所有效率并且将 “效率” 视为一种优雅的人来说,以下内容应该是一个不错的选择(我认为模板容器类是非常出色的添加):

template < class ContainerT >
void tokenize(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.push_back(value_type(str.data()+lastPos,
               (size_type)pos-lastPos ));

      lastPos = pos + 1;
   }
}

我通常选择使用std::vector<std::string>类型作为我的第二个参数( ContainerT )... 但是对于不需要直接访问的情况, list<>vector<>快得多,甚至可以创建您自己的字符串类,并使用类似std::list<subString>类的东西,其中subString不执行任何复制以实现惊人的速度提高。

它是此页面上最快的标记化速度的两倍以上,几乎是其他一些标记速度的 5 倍。同样,使用理想的参数类型,您可以消除所有字符串和列表副本,从而进一步提高速度。

此外,它不会返回结果(效率极低),而是将令牌作为参考传递,因此,如果您愿意,还可以使用多个调用来构建令牌。

最后,它允许您指定是否通过最后一个可选参数从结果中修剪空标记。

它需要的只是std::string ... 其余都是可选的。它不使用流或 boost 库,但足够灵活以能够自然地接受其中一些外部类型。

这是另一个解决方案。它紧凑且相当有效:

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;
}

这是我最喜欢的遍历字符串的方式。您可以按单词做任何您想做的事情。

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

istringstream iss(line, istringstream::in);

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

这类似于堆栈溢出问题, 如何在 C ++ 中标记字符串?

#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;
    }
}

我喜欢以下内容,因为它将结果放入向量中,支持字符串作为 delim 并控制保留空值。但是,那时看起来还不怎么样。

#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()效果很好。

STL 还没有可用的这种方法。

但是,您可以通过使用std::string::c_str()成员来使用 C 的strtok()函数,也可以编写自己的函数。这是经过快速 Google 搜索( “STL 字符串拆分” )后发现的代码示例:

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

如果您对代码示例有疑问,请发表评论,我会解释。

仅仅因为它没有实现称为迭代器的typedef或重载<<操作符,并不意味着它是错误的代码。我经常使用 C 函数。例如, printfscanf都比std::cinstd::cout都快(明显), fopen语法对二进制类型友好得多,并且它们也倾向于生成更小的 EXE。

不要因为这项“绩效上的优雅”交易而被出售。