如何在C ++中标记字符串?


414

Java有一个方便的split方法:

String str = "The quick brown fox";
String[] results = str.split(" ");

有没有一种简单的方法可以在C ++中做到这一点?


172
简直

6
在C ++中,它并不令人头疼-有多种方法可以实现它。程序员对C ++的了解不如C#-有关营销和投资的知识……请参见以下有关实现相同目标的各种C ++选项的信息:cplusplus.com/faq/sequences/strings/split
hB0

9
@ hB0经历了许多问题的答案,但仍未决定方法是令人头疼的事情。在一个需求的图书馆,另一种是只为空格,其他不..处理空间
PASCHALIS


2
为什么C ++中的所有内容都必须挣扎?
Wael Assaf

Answers:


145

C ++标准库算法非常普遍地基于迭代器而不是具体容器。不幸的是,这使得很难split在C ++标准库中提供类似Java的功能,即使没有人认为这样做会很方便。但是它的返回类型是什么?std::vector<std::basic_string<…>>?也许可以,但是后来我们被迫执行(可能是多余的和昂贵的)分配。

取而代之的是,C ++提供了多种方法来基于任意复杂的定界符来分割字符串,但是它们都没有像其他语言那样被很好地封装。大量方法可以填充整个博客文章

最简单的说,您可以迭代使用std::string::find直到您点击std::string::npos,然后使用提取内容std::string::substr

用于在空格上分割的更流畅(和惯用但基本的)版本应使用std::istringstream

auto iss = std::istringstream{"The quick brown fox"};
auto str = std::string{};

while (iss >> str) {
    process(str);
}

使用std::istream_iterators,还可以使用其迭代器范围构造函数将字符串流的内容复制到向量中。

多个库(例如Boost.Tokenizer)提供特定的标记器。

更高级的拆分需要正则表达式。C ++ std::regex_token_iterator为此特别提供了:

auto const str = "The quick brown fox"s;
auto const re = std::regex{R"(\s+)"};
auto const vec = std::vector<std::string>(
    std::sregex_token_iterator{begin(str), end(str), re, -1},
    std::sregex_token_iterator{}
);

53
可悲的是,boost并非总是适用于所有项目。我将不得不寻找一个非助推的答案。
FuzzyBunnySlippers 2013年

36
并非每个项目都对“开源”开放。我在严格管制的行业中工作。真的,这不是问题。这只是生活中的事实。Boost并非随处可用。
FuzzyBunnySlippers 2013年

5
@NonlinearIdeas另一个问题/答案根本与开源项目无关。对于任何项目都是如此。就是说,我当然了解诸如MISRA C之类的受限制的标准,但是后来了解到,无论如何,您都是从头开始构建所有内容的(除非您碰巧找到了一个兼容的库-很少)。无论如何,要点几乎不是“无法提供升压”,而是您有特殊的要求,几乎所有通用答案都不适合。
Konrad Rudolph

1
以@NonlinearIdeas为例,其他非Boost答案也不符合MISRA。
Konrad Rudolph

3
@Dmitry什么是“ STL barf”?整个社区都非常赞成替换C预处理器-实际上,有建议这样做。但是,您建议使用PHP或其他语言代替它会倒退一大步。
康拉德·鲁道夫,

188

升压标记生成器类可以使这种相当简单的事情:

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

using namespace std;
using namespace boost;

int main(int, char**)
{
    string text = "token, test   string";

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

针对C ++ 11更新:

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

using namespace std;
using namespace boost;

int main(int, char**)
{
    string text = "token, test   string";

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

1
好东西,我最近利用了这个。我的Visual Studio编译器有点奇怪,直到我使用空格将标记(text,sep)位之前的两个“>”字符分开:(错误C2947:期望'>'终止模板参数列表,找到'> >')
AndyUK

@AndyUK是的,没有空格,编译器会将其解析为提取运算符,而不是两个封闭模板。
EnabrenTane 2011年

理论上这是被固定在的C ++ 0x
大卫南风

3
注意char_separator构造函数的第三个参数(drop_empty_tokens默认设置,替代值为keep_empty_tokens)。
Benoit

5
@puk-这是C ++头文件的常用后缀。(例如.hC标头)
Ferruccio

167

这是一个真正简单的:

#include <vector>
#include <string>
using namespace std;

vector<string> split(const char *str, char c = ' ')
{
    vector<string> result;

    do
    {
        const char *begin = str;

        while(*str != c && *str)
            str++;

        result.push_back(string(begin, str));
    } while (0 != *str++);

    return result;
}

我需要在.h文件中为此方法添加原型吗?
Suhrob Samiev 2011年

5
这并不是完全“最佳”的答案,因为它仍然使用字符串文字,即纯C常量字符数组。我相信发问者正在询问他是否可以标记由后者引入的“字符串”类型的C ++字符串。
维杰·库玛·坎塔

这需要一个新的答案,因为我强烈怀疑C ++ 11中包含正则表达式已经改变了最佳答案。
全天

113

使用strtok。我认为,除非strtok不能为您提供所需的内容,否则无需围绕标记化构建类。可能不是,但是在用C和C ++编写各种解析代码的15年以上的时间里,我一直使用strtok。这是一个例子

char myString[] = "The quick brown fox";
char *p = strtok(myString, " ");
while (p) {
    printf ("Token: %s\n", p);
    p = strtok(NULL, " ");
}

一些警告(可能不适合您的需求)。该字符串在此过程中被“销毁”,这意味着EOS字符被内联放置在除臭点中。正确的用法可能要求您制作字符串的非常量版本。您还可以更改中间解析的定界符列表。

我个人认为,上述代码比编写一个单独的类要简单得多,而且易于使用。对我来说,这是该语言提供的功能之一,它可以干净利落地完成。它只是一个“基于C”的解决方案。这很合适,很容易,而且您不必编写很多额外的代码:-)


42
并不是我不喜欢C,但是strtok并不是线程安全的,并且需要确定发送给它的字符串包含空字符,以避免可能的缓冲区溢出。
tloach

11
有strtok_r,但这是一个C ++问题。
Falken教授的合同

3
@tloach:在MS C ++编译器中,strtok是线程安全的,因为内部静态变量是在TLS(线程本地存储)上创建的(实际上是编译器所依赖的)
Ahmed Said

3
@ahmed:线程安全不仅意味着能够在不同线程中两次运行该函数。在这种情况下,如果在strtok运行时修改了线程,则可能会在整个strtok运行期间使字符串有效,但是strtok仍然会混乱,因为字符串已更改,现在已经超过了null字符,并且它将继续读取内存,直到它遇到安全漏洞或找到空字符。这是原始C字符串函数的问题,如果您未在某处指定长度,则会遇到问题。
10年

4
strtok需要一个指向非const以null终止的char数组的指针,这不是在c ++代码中找到的常见动物……从std :: string转换为此字符串的最喜欢的方法是什么?
FuzzyTew13年

105

另一种快速的方法是使用getline。就像是:

stringstream ss("bla bla");
string s;

while (getline(ss, s, ' ')) {
 cout << s << endl;
}

如果需要,可以创建一个简单的split()方法来返回vector<string>,这真的很有用。


2
我在使用0x0A字符的字符串中使用此技术时遇到问题,这使得while循环过早退出。否则,这是一个很好的简单快速的解决方案。
Ryan H.

4
这样做很好,但是必须记住,这样做不会考虑默认的定界符'\ n'。这个例子将工作,但如果您使用的是这样的:而(函数getline(INFILE,字,”“))这里的infile是包含多行,你会得到funnny结果.. ifstream的对象
hackrock

getline返回的是流而不是字符串,这太糟糕了,使其无法在没有临时存储的初始化列表中使用
FuzzyTew

1
凉!没有boost和C ++ 11,这是对那些旧项目的良好解决方案!
德庆2014年

1
这就是答案,该函数的名称有点尴尬。
尼尔斯

82

您可以使用流,迭代器和复制算法直接进行此操作。

#include <string>
#include <vector>
#include <iostream>
#include <istream>
#include <ostream>
#include <iterator>
#include <sstream>
#include <algorithm>

int main()
{
  std::string str = "The quick brown fox";

  // construct a stream from the string
  std::stringstream strstr(str);

  // use stream iterators to copy the stream to the vector as whitespace separated strings
  std::istream_iterator<std::string> it(strstr);
  std::istream_iterator<std::string> end;
  std::vector<std::string> results(it, end);

  // send the vector to stdout.
  std::ostream_iterator<std::string> oit(std::cout);
  std::copy(results.begin(), results.end(), oit);
}

17
我发现那些std ::令人讨厌阅读..为什么不使用“ using”?
user35978

80
@Vadi:因为编辑其他人的帖子是很麻烦的。@pheze:我更喜欢这样让std我知道我的对象来自哪里,这仅是样式问题。
Matthieu M.

7
我理解您的原因,并且认为对您有用,这实际上是一个不错的选择,但是从教学的角度来看,我实际上同意“冒昧”。更容易阅读和理解这样一个完全陌生的示例,它的顶部带有“ using namespace std”,因为它不需要花费很多精力来解释以下几行……尤其是在这种情况下,因为所有内容都来自标准库。您可以通过一系列“使用std :: string;”使它易于阅读并清楚地表明对象的来源。等等,特别是因为功能太短了。
cheshirekow

61
尽管前缀“ std ::”令人讨厌或丑陋,但最好将它们包含在示例代码中,以便完全清楚这些函数的来源。如果它们打扰了您,那么在您窃取该示例并将其声明为您自己的示例之后,将它们替换为“ using”是很简单的。
dlchambers 2012年

20
是的!他说什么!最佳做法是使用std前缀。毫无疑问,任何大型代码库都将拥有自己的库和名称空间,当开始引起名称空间冲突时,使用“使用名称空间标准”将使您头疼。
Miek 2012年

48

没有进攻的乡亲,但对于这样一个简单的问题,你在做事情的方式太复杂了。有很多原因使用Boost。但是对于这种简单的事情,这就像用20号雪橇击中苍蝇。

void
split( vector<string> & theStringVector,  /* Altered/returned value */
       const  string  & theString,
       const  string  & theDelimiter)
{
    UASSERT( theDelimiter.size(), >, 0); // My own ASSERT macro.

    size_t  start = 0, end = 0;

    while ( end != string::npos)
    {
        end = theString.find( theDelimiter, start);

        // If at end, use length=maxLength.  Else use length=end-start.
        theStringVector.push_back( theString.substr( start,
                       (end == string::npos) ? string::npos : end - start));

        // If at end, use start=maxSize.  Else use start=end+delimiter.
        start = (   ( end > (string::npos - theDelimiter.size()) )
                  ?  string::npos  :  end + theDelimiter.size());
    }
}

例如(以道格为例),

#define SHOW(I,X)   cout << "[" << (I) << "]\t " # X " = \"" << (X) << "\"" << endl

int
main()
{
    vector<string> v;

    split( v, "A:PEP:909:Inventory Item", ":" );

    for (unsigned int i = 0;  i < v.size();   i++)
        SHOW( i, v[i] );
}

是的,我们可以让split()返回一个新向量,而不是传入一个向量。包装和重载都是微不足道的。但是根据我在做什么,我经常发现重用已有的对象比总创建新的对象要好。(只要我不忘记将它们之间的向量清空!)

参考:http : //www.cplusplus.com/reference/string/string/

(我最初是对Doug的问题写的一个答案:基于分隔符的C ++字符串修改和提取


12
为什么要定义只在一个地方使用的宏。而且您的UASSERT会比标准断言更好吗?将比较结果分成3个标记,除了需要比您需要的逗号更多的内容,别无其他。
2011年

1
也许UASSERT宏显示(在错误消息中)两个比较值(及其值)之间的实际关系?恕我直言,这实际上是一个很好的主意。
GhassanPL 2012年

10
gh,为什么std::string类不包含split()函数?
Shickadance先生2012年

我认为while循环的最后一行应该是start = ((end > (theString.size() - theDelimiter.size())) ? string::npos : end + theDelimiter.size());,while循环应该是while (start != string::npos)。另外,在将子字符串插入向量之前,我要检查它是否为空。
约翰K

@JohnK如果输入具有两个连续的定界符,则显然它们之间的字符串为空,应将其插入向量中。如果空值对于特定目的是不可接受的,那是另一回事,但是恕我直言,此类约束应在此类非常通用的函数之外实施。
Lauri Nurmi 2013年

46

使用regex_token_iterators 的解决方案:

#include <iostream>
#include <regex>
#include <string>

using namespace std;

int main()
{
    string str("The quick brown fox");

    regex reg("\\s+");

    sregex_token_iterator iter(str.begin(), str.end(), reg, -1);
    sregex_token_iterator end;

    vector<string> vec(iter, end);

    for (auto a : vec)
    {
        cout << a << endl;
    }
}

5
这应该是排名最高的答案。这是在C ++> = 11中执行此操作的正确方法
。– Omnifarious

1
我很高兴我一直滚动到这个答案(目前只有9个投票)。这正是此任务的C ++ 11代码外观!
YePhIcK

不依赖外部库而是使用现有库的出色答案
Andrew

1
很好的答案,使定界符具有最大的灵活性。请注意以下几点:使用\ s + regex可以避免在文本中间出现空标记,但是如果文本以空格开头,则会给出一个空的第一个标记。另外,正则表达式似乎很慢:在我的笔记本电脑上,对于20 MB的随机文本,它需要0.6秒,而使用str.find_first_of的strtok,strsep或Parham的答案为0.014秒,对于Perl则为0.027秒,对于Python为0.021秒。对于短文本,速度可能不是问题。
马克·盖茨

2
好的,也许看起来很酷,但这显然是对正则表达式的过度使用。仅当您不关心性能时才是合理的。
Marek R

35

Boost具有强大的拆分功能:boost :: algorithm :: split

示例程序:

#include <vector>
#include <boost/algorithm/string.hpp>

int main() {
    auto s = "a,b, c ,,e,f,";
    std::vector<std::string> fields;
    boost::split(fields, s, boost::is_any_of(","));
    for (const auto& field : fields)
        std::cout << "\"" << field << "\"\n";
    return 0;
}

输出:

"a"
"b"
" c "
""
"e"
"f"
""

26

我知道您要求使用C ++解决方案,但是您可能会认为这有帮助:

t

#include <QString>

...

QString str = "The quick brown fox"; 
QStringList results = str.split(" "); 

在本示例中,与Boost相比的优势在于,它是直接映射到您的帖子代码。

Qt文档中查看更多


22

这是一个示例标记器类,可能会做您想要的

//Header file
class Tokenizer 
{
    public:
        static const std::string DELIMITERS;
        Tokenizer(const std::string& str);
        Tokenizer(const std::string& str, const std::string& delimiters);
        bool NextToken();
        bool NextToken(const std::string& delimiters);
        const std::string GetToken() const;
        void Reset();
    protected:
        size_t m_offset;
        const std::string m_string;
        std::string m_token;
        std::string m_delimiters;
};

//CPP file
const std::string Tokenizer::DELIMITERS(" \t\n\r");

Tokenizer::Tokenizer(const std::string& s) :
    m_string(s), 
    m_offset(0), 
    m_delimiters(DELIMITERS) {}

Tokenizer::Tokenizer(const std::string& s, const std::string& delimiters) :
    m_string(s), 
    m_offset(0), 
    m_delimiters(delimiters) {}

bool Tokenizer::NextToken() 
{
    return NextToken(m_delimiters);
}

bool Tokenizer::NextToken(const std::string& delimiters) 
{
    size_t i = m_string.find_first_not_of(delimiters, m_offset);
    if (std::string::npos == i) 
    {
        m_offset = m_string.length();
        return false;
    }

    size_t j = m_string.find_first_of(delimiters, i);
    if (std::string::npos == j) 
    {
        m_token = m_string.substr(i);
        m_offset = m_string.length();
        return true;
    }

    m_token = m_string.substr(i, j - i);
    m_offset = j;
    return true;
}

例:

std::vector <std::string> v;
Tokenizer s("split this string", " ");
while (s.NextToken())
{
    v.push_back(s.GetToken());
}

19

这是一个简单的仅使用STL的解决方案(〜5行!)std::findstd::find_first_not_of它使用和处理分隔符的重复(例如空格或句点),以及前导和尾随定界符:

#include <string>
#include <vector>

void tokenize(std::string str, std::vector<string> &token_v){
    size_t start = str.find_first_not_of(DELIMITER), end=start;

    while (start != std::string::npos){
        // Find next occurence of delimiter
        end = str.find(DELIMITER, start);
        // Push back the token found into vector
        token_v.push_back(str.substr(start, end-start));
        // Skip all occurences of the delimiter to find new start
        start = str.find_first_not_of(DELIMITER, end);
    }
}

现场尝试!


3
这是一个很好的选择,但我认为您需要使用find_first_of()而不是find()才能与多个定界符一起正常工作。

2
使用find_first_not_of查找起始位置时,会跳过@ user755921多个定界符。
初级

16

pystring是一个小型库,它实现了一堆Python的字符串函数,包括split方法:

#include <string>
#include <vector>
#include "pystring.h"

std::vector<std::string> chunks;
pystring::split("this string", chunks);

// also can specify a separator
pystring::split("this-string", chunks, "-");

3
哇,您已经回答了我的紧迫问题以及以后的许多问题。我知道c ++功能强大。但是,当拆分字符串会产生类似于上述答案的源代码时,这显然令人沮丧。我很想知道其他像这样的库,它们会拉低高级语言的便利性。
罗斯

哇,你真的让我开心了!!不知道pystring。这将为我节省很多时间!
2015年

11

我针对类似问题发布了此答案。
不要重新发明轮子。我使用了许多库,遇到的最快,最灵活的是: C ++ String Toolkit库

这是我已在stackoverflow上其他地方发布的如何使用它的示例。

#include <iostream>
#include <vector>
#include <string>
#include <strtk.hpp>

const char *whitespace  = " \t\r\n\f";
const char *whitespace_and_punctuation  = " \t\r\n\f;,=";

int main()
{
    {   // normal parsing of a string into a vector of strings
       std::string s("Somewhere down the road");
       std::vector<std::string> result;
       if( strtk::parse( s, whitespace, result ) )
       {
           for(size_t i = 0; i < result.size(); ++i )
            std::cout << result[i] << std::endl;
       }
    }

    {  // parsing a string into a vector of floats with other separators
       // besides spaces

       std::string s("3.0, 3.14; 4.0");
       std::vector<float> values;
       if( strtk::parse( s, whitespace_and_punctuation, values ) )
       {
           for(size_t i = 0; i < values.size(); ++i )
            std::cout << values[i] << std::endl;
       }
    }

    {  // parsing a string into specific variables

       std::string s("angle = 45; radius = 9.9");
       std::string w1, w2;
       float v1, v2;
       if( strtk::parse( s, whitespace_and_punctuation, w1, v1, w2, v2) )
       {
           std::cout << "word " << w1 << ", value " << v1 << std::endl;
           std::cout << "word " << w2 << ", value " << v2 << std::endl;
       }
    }

    return 0;
}

8

检查这个例子。它可能会帮助您。

#include <iostream>
#include <sstream>

using namespace std;

int main ()
{
    string tmps;
    istringstream is ("the dellimiter is the space");
    while (is.good ()) {
        is >> tmps;
        cout << tmps << "\n";
    }
    return 0;
}

1
我会做while ( is >> tmps ) { std::cout << tmps << "\n"; }
jordix '16

6

MFC / ATL有一个非常好的标记器。从MSDN:

CAtlString str( "%First Second#Third" );
CAtlString resToken;
int curPos= 0;

resToken= str.Tokenize("% #",curPos);
while (resToken != "")
{
   printf("Resulting token: %s\n", resToken);
   resToken= str.Tokenize("% #",curPos);
};

Output

Resulting Token: First
Resulting Token: Second
Resulting Token: Third

1
此Tokenize()函数将跳过空令牌,例如,如果主字符串中有子字符串“ %%”,则不返回空令牌。它被跳过。
Sheen

4

如果您愿意使用C,则可以使用strtok函数。使用它时,应注意多线程问题。


3
请注意,strtok修改了您要检查的字符串,因此您不能在没有复制的情况下在const char *字符串上使用它。
Graeme Perrow

9
多线程问题是strtok使用全局变量来跟踪其位置,因此,如果您有两个都使用strtok的线程,则会得到未定义的行为。
JohnMcG

@JohnMcG或只使用strtok_s基本上strtok带有显式状态传递的方法。
Matthias

4

对于简单的东西,我只使用以下内容:

unsigned TokenizeString(const std::string& i_source,
                        const std::string& i_seperators,
                        bool i_discard_empty_tokens,
                        std::vector<std::string>& o_tokens)
{
    unsigned prev_pos = 0;
    unsigned pos = 0;
    unsigned number_of_tokens = 0;
    o_tokens.clear();
    pos = i_source.find_first_of(i_seperators, pos);
    while (pos != std::string::npos)
    {
        std::string token = i_source.substr(prev_pos, pos - prev_pos);
        if (!i_discard_empty_tokens || token != "")
        {
            o_tokens.push_back(i_source.substr(prev_pos, pos - prev_pos));
            number_of_tokens++;
        }

        pos++;
        prev_pos = pos;
        pos = i_source.find_first_of(i_seperators, pos);
    }

    if (prev_pos < i_source.length())
    {
        o_tokens.push_back(i_source.substr(prev_pos));
        number_of_tokens++;
    }

    return number_of_tokens;
}

怯弱的免责声明:我编写了实时数据处理软件,其中的数据通过二进制文件,套接字或某些API调用(I / O卡,相机的)传入。与启动时读取外部配置文件相比,我从未将此功能用于更复杂或时间紧迫的事情。


4

您可以简单地使用正则表达式库并使用正则表达式解决该问题。

使用表达式(\ w +)和\ 1中的变量(或$ 1,取决于正则表达式的库实现)。


+1表示正则表达式,如果您不需要经线速度,它是最灵活的解决方案,尚无处支持,但随着时间的流逝,它将变得不那么重要。
odinthenerd 2014年

从我+1,只是在c ++ 11中尝试过<regex>。如此简单而优雅
StahlRat 2014年

4

这里有很多过于复杂的建议。试试这个简单的std :: string解决方案:

using namespace std;

string someText = ...

string::size_type tokenOff = 0, sepOff = tokenOff;
while (sepOff != string::npos)
{
    sepOff = someText.find(' ', sepOff);
    string::size_type tokenLen = (sepOff == string::npos) ? sepOff : sepOff++ - tokenOff;
    string token = someText.substr(tokenOff, tokenLen);
    if (!token.empty())
        /* do something with token */;
    tokenOff = sepOff;
}


4

亚当·皮尔斯(Adam Pierce)的答案提供了一个手动旋转的分词器const char*。使用迭代器会有更多问题,因为增加一个stringend迭代器是不确定的。也就是说,鉴于string str{ "The quick brown fox" }我们当然可以做到这一点:

auto start = find(cbegin(str), cend(str), ' ');
vector<string> tokens{ string(cbegin(str), start) };

while (start != cend(str)) {
    const auto finish = find(++start, cend(str), ' ');

    tokens.push_back(string(start, finish));
    start = finish;
}

Live Example


如果您希望通过使用标准功能来抽象复杂性,那么正如On Freund所建议的那样,这 strtok是一个简单的选择:

vector<string> tokens;

for (auto i = strtok(data(str), " "); i != nullptr; i = strtok(nullptr, " ")) tokens.push_back(i);

如果您无权使用C ++ 17,则需要替换data(str)为以下示例: http //ideone.com/8kAGoa

尽管在示例中未演示,但strtok不必为每个标记使用相同的定界符。尽管具有此优点,但有几个缺点:

  1. strtok不能同时使用在多个对象strings上:nullptr必须传递a来继续对当前标记进行标记化,string或者char*必须传递一个新的标记化标记(但是,某些非标准实现确实支持此标记,例如:strtok_s
  2. 由于相同的原因strtok,不能同时在多个线程上使用(但是,这可以定义为实现,例如:Visual Studio的实现是线程安全的
  3. 调用strtok会修改string它正在操作的对象,因此不能在const strings,const char*s或文字字符串上使用它,以将其中的任何一个标记化strtok或对string需要保留其内容的某人进行操作,str必须对其进行复制,然后该副本才能被操作

为我们提供了split_view以非破坏性方式标记字符串的方法:https ://topanswers.xyz/cplusplus?q=749#a874


先前的方法无法vector就地生成标记化的符号,这意味着没有将它们抽象为无法初始化的辅助函数const vector<string> tokens。可以使用来利用该功能接受任何空格分隔符的功能istream_iterator。给定的例子:const string str{ "The quick \tbrown \nfox" }我们可以这样做:

istringstream is{ str };
const vector<string> tokens{ istream_iterator<string>(is), istream_iterator<string>() };

Live Example

istringstream此选项所需的a构造比前两个选项具有更高的成本,但是,此成本通常隐藏在string分配费用中。


如果以上选项都不具有足够的regex_token_iterator灵活性来满足标记化需求,那么最灵活的选项当然是使用,当然,这种灵活性会带来更大的费用,但这又很可能隐藏在string分配成本中。例如,假设输入以下内容,我们希望基于未转义的逗号进行标记化,并且也占用空白:const string str{ "The ,qu\\,ick ,\tbrown, fox" }我们可以这样做:

const regex re{ "\\s*((?:[^\\\\,]|\\\\.)*?)\\s*(?:,|$)" };
const vector<string> tokens{ sregex_token_iterator(cbegin(str), cend(str), re, 1), sregex_token_iterator() };

Live Example


strtok_s顺便说一下,它是C11标准。strtok_r是POSIX2001标准。在这两者之间strtok,大多数平台都有一个标准的可重入版本。
安东·科尔曼

@ AndonM.Coleman但这是一个c ++问题,在C ++中#include <cstring>仅包含c99版本的strtok。所以我的假设是,您只是将此注释作为辅助材料提供,说明strtok扩展的特定于实现的可用性?
乔纳森·梅

1
仅仅是它不像人们原本认为的那样不合标准。strtok_s由C11提供,并且作为Microsoft C运行时中的独立扩展提供。微软的_s功能成为C标准在这里有一段奇怪的历史。
安东·科尔曼

@ AndonM.Coleman对,我和你在一起。显然,如果它在C11标准中,则接口和实现受到约束,它们要求独立于平台的相同行为。现在唯一的问题是确保C11功能可跨平台使用。希望C11标准将成为C ++ 17或C ++ 20选择的标准。
乔纳森·米

3

我知道这个问题已经回答,但我想贡献自己的力量。也许我的解决方案有点简单,但这是我想到的:

vector<string> get_words(string const& text, string const& separator)
{
    vector<string> result;
    string tmp = text;

    size_t first_pos = 0;
    size_t second_pos = tmp.find(separator);

    while (second_pos != string::npos)
    {
        if (first_pos != second_pos)
        {
            string word = tmp.substr(first_pos, second_pos - first_pos);
            result.push_back(word);
        }
        tmp = tmp.substr(second_pos + separator.length());
        second_pos = tmp.find(separator);
    }

    result.push_back(tmp);

    return result;
}

如果在我的代码中有更好的方法或出现错误,请发表评论。

更新:添加了通用分隔符


从人群中使用您的解决方案:)我可以修改您的代码以添加任何分隔符吗?
扎克

1
@Zac很高兴您喜欢它,并且可以对其进行修改...只需在我的答案中添加一个加粗的更新部分...
NutCracker

2

这是一种允许您控制是否包含空令牌(如strsep)或排除空令牌(如strtok)的方法。

#include <string.h> // for strchr and strlen

/*
 * want_empty_tokens==true  : include empty tokens, like strsep()
 * want_empty_tokens==false : exclude empty tokens, like strtok()
 */
std::vector<std::string> tokenize(const char* src,
                                  char delim,
                                  bool want_empty_tokens)
{
  std::vector<std::string> tokens;

  if (src and *src != '\0') // defensive
    while( true )  {
      const char* d = strchr(src, delim);
      size_t len = (d)? d-src : strlen(src);

      if (len or want_empty_tokens)
        tokens.push_back( std::string(src, len) ); // capture token

      if (d) src += len+1; else break;
    }

  return tokens;
}

2

令我感到奇怪的是,在这里,我们所有人都对速度敏感,因此没有人提出过使用编译时生成的查找表作为分隔符的版本(示例实现进一步向下)。使用查找表和迭代器应该可以在效率上击败std :: regex,如果您不需要击败regex,只需使用它,它是C ++ 11的标准版本,并且非常灵活。

有些人已经建议使用正则表达式,但是对于菜鸟来说,这是一个打包的示例,应该完全符合OP的期望:

std::vector<std::string> split(std::string::const_iterator it, std::string::const_iterator end, std::regex e = std::regex{"\\w+"}){
    std::smatch m{};
    std::vector<std::string> ret{};
    while (std::regex_search (it,end,m,e)) {
        ret.emplace_back(m.str());              
        std::advance(it, m.position() + m.length()); //next start position = match position + match length
    }
    return ret;
}
std::vector<std::string> split(const std::string &s, std::regex e = std::regex{"\\w+"}){  //comfort version calls flexible version
    return split(s.cbegin(), s.cend(), std::move(e));
}
int main ()
{
    std::string str {"Some people, excluding those present, have been compile time constants - since puberty."};
    auto v = split(str);
    for(const auto&s:v){
        std::cout << s << std::endl;
    }
    std::cout << "crazy version:" << std::endl;
    v = split(str, std::regex{"[^e]+"});  //using e as delim shows flexibility
    for(const auto&s:v){
        std::cout << s << std::endl;
    }
    return 0;
}

如果我们需要更快的速度并接受所有字符必须为8位的约束,我们可以在编译时使用元编程制作一个查询表:

template<bool...> struct BoolSequence{};        //just here to hold bools
template<char...> struct CharSequence{};        //just here to hold chars
template<typename T, char C> struct Contains;   //generic
template<char First, char... Cs, char Match>    //not first specialization
struct Contains<CharSequence<First, Cs...>,Match> :
    Contains<CharSequence<Cs...>, Match>{};     //strip first and increase index
template<char First, char... Cs>                //is first specialization
struct Contains<CharSequence<First, Cs...>,First>: std::true_type {}; 
template<char Match>                            //not found specialization
struct Contains<CharSequence<>,Match>: std::false_type{};

template<int I, typename T, typename U> 
struct MakeSequence;                            //generic
template<int I, bool... Bs, typename U> 
struct MakeSequence<I,BoolSequence<Bs...>, U>:  //not last
    MakeSequence<I-1, BoolSequence<Contains<U,I-1>::value,Bs...>, U>{};
template<bool... Bs, typename U> 
struct MakeSequence<0,BoolSequence<Bs...>,U>{   //last  
    using Type = BoolSequence<Bs...>;
};
template<typename T> struct BoolASCIITable;
template<bool... Bs> struct BoolASCIITable<BoolSequence<Bs...>>{
    /* could be made constexpr but not yet supported by MSVC */
    static bool isDelim(const char c){
        static const bool table[256] = {Bs...};
        return table[static_cast<int>(c)];
    }   
};
using Delims = CharSequence<'.',',',' ',':','\n'>;  //list your custom delimiters here
using Table = BoolASCIITable<typename MakeSequence<256,BoolSequence<>,Delims>::Type>;

有了这个getNextToken功能,就很容易了:

template<typename T_It>
std::pair<T_It,T_It> getNextToken(T_It begin,T_It end){
    begin = std::find_if(begin,end,std::not1(Table{})); //find first non delim or end
    auto second = std::find_if(begin,end,Table{});      //find first delim or end
    return std::make_pair(begin,second);
}

使用它也很容易:

int main() {
    std::string s{"Some people, excluding those present, have been compile time constants - since puberty."};
    auto it = std::begin(s);
    auto end = std::end(s);
    while(it != std::end(s)){
        auto token = getNextToken(it,end);
        std::cout << std::string(token.first,token.second) << std::endl;
        it = token.second;
    }
    return 0;
}

这是一个实时示例:http : //ideone.com/GKtkLQ


1
是否可以使用String分隔符进行标记?
Galigator 2014年

此版本仅针对单字符定界符进行了优化,使用查找表不适用于多字符(字符串)定界符,因此其效率很难超过正则表达式。
odinthenerd 2014年

1

您可以利用boost :: make_find_iterator。类似于以下内容:

template<typename CH>
inline vector< basic_string<CH> > tokenize(
    const basic_string<CH> &Input,
    const basic_string<CH> &Delimiter,
    bool remove_empty_token
    ) {

    typedef typename basic_string<CH>::const_iterator string_iterator_t;
    typedef boost::find_iterator< string_iterator_t > string_find_iterator_t;

    vector< basic_string<CH> > Result;
    string_iterator_t it = Input.begin();
    string_iterator_t it_end = Input.end();
    for(string_find_iterator_t i = boost::make_find_iterator(Input, boost::first_finder(Delimiter, boost::is_equal()));
        i != string_find_iterator_t();
        ++i) {
        if(remove_empty_token){
            if(it != i->begin())
                Result.push_back(basic_string<CH>(it,i->begin()));
        }
        else
            Result.push_back(basic_string<CH>(it,i->begin()));
        it = i->end();
    }
    if(it != it_end)
        Result.push_back(basic_string<CH>(it,it_end));

    return Result;
}

1

这是我的Swiss®折线机军刀,用于按空格分割字符串,说明单引号和双引号引起来的字符串以及从结果中剥离这些字符。我使用RegexBuddy 4.x生成了大部分代码片段,但是我添加了自定义处理以去除引号和其他一些东西。

#include <string>
#include <locale>
#include <regex>

std::vector<std::wstring> tokenize_string(std::wstring string_to_tokenize) {
    std::vector<std::wstring> tokens;

    std::wregex re(LR"(("[^"]*"|'[^']*'|[^"' ]+))", std::regex_constants::collate);

    std::wsregex_iterator next( string_to_tokenize.begin(),
                                string_to_tokenize.end(),
                                re,
                                std::regex_constants::match_not_null );

    std::wsregex_iterator end;
    const wchar_t single_quote = L'\'';
    const wchar_t double_quote = L'\"';
    while ( next != end ) {
        std::wsmatch match = *next;
        const std::wstring token = match.str( 0 );
        next++;

        if (token.length() > 2 && (token.front() == double_quote || token.front() == single_quote))
            tokens.emplace_back( std::wstring(token.begin()+1, token.begin()+token.length()-1) );
        else
            tokens.emplace_back(token);
    }
    return tokens;
}

1
(Down)投票可以和upvotes一样具有建设性,但当您不对原因发表评论时就不会如此……
kayleeFrye_onDeck

1
我使您失望了,但这可能是因为代码对于程序员搜索“如何分割字符串”而言显得相当艰巨,尤其是在没有文档的情况下
mattshu

谢谢@mattshu!是使正则表达式令人生畏的细分还是其他东西?
kayleeFrye_onDeck

0

如果知道要分词的输入字符串的最大长度,则可以利用它并实现非常快的版本。我正在草绘下面的基本思想,该思想受strtok()和“后缀数组”的影响-数据结构描述了Jon Bentley的“ Programming Perls”第二版,第15章。在这种情况下,C ++类仅提供了一些组织和便利性使用。可以轻松扩展所示的实现,以删除令牌中的前导和尾随空白字符。

基本上,可以使用以字符串结尾的'\ 0'字符替换分隔符,并使用修改后的字符串设置指向标记的指针。在极端情况下,当字符串仅包含分隔符时,一个将得到字符串长度加1的空标记。复制要修改的字符串是实用的。

头文件:

class TextLineSplitter
{
public:

    TextLineSplitter( const size_t max_line_len );

    ~TextLineSplitter();

    void            SplitLine( const char *line,
                               const char sep_char = ',',
                             );

    inline size_t   NumTokens( void ) const
    {
        return mNumTokens;
    }

    const char *    GetToken( const size_t token_idx ) const
    {
        assert( token_idx < mNumTokens );
        return mTokens[ token_idx ];
    }

private:
    const size_t    mStorageSize;

    char           *mBuff;
    char          **mTokens;
    size_t          mNumTokens;

    inline void     ResetContent( void )
    {
        memset( mBuff, 0, mStorageSize );
        // mark all items as empty:
        memset( mTokens, 0, mStorageSize * sizeof( char* ) );
        // reset counter for found items:
        mNumTokens = 0L;
    }
};

实施文件:

TextLineSplitter::TextLineSplitter( const size_t max_line_len ):
    mStorageSize ( max_line_len + 1L )
{
    // allocate memory
    mBuff   = new char  [ mStorageSize ];
    mTokens = new char* [ mStorageSize ];

    ResetContent();
}

TextLineSplitter::~TextLineSplitter()
{
    delete [] mBuff;
    delete [] mTokens;
}


void TextLineSplitter::SplitLine( const char *line,
                                  const char sep_char   /* = ',' */,
                                )
{
    assert( sep_char != '\0' );

    ResetContent();
    strncpy( mBuff, line, mMaxLineLen );

    size_t idx       = 0L; // running index for characters

    do
    {
        assert( idx < mStorageSize );

        const char chr = line[ idx ]; // retrieve current character

        if( mTokens[ mNumTokens ] == NULL )
        {
            mTokens[ mNumTokens ] = &mBuff[ idx ];
        } // if

        if( chr == sep_char || chr == '\0' )
        { // item or line finished
            // overwrite separator with a 0-terminating character:
            mBuff[ idx ] = '\0';
            // count-up items:
            mNumTokens ++;
        } // if

    } while( line[ idx++ ] );
}

使用情况是:

// create an instance capable of splitting strings up to 1000 chars long:
TextLineSplitter spl( 1000 );
spl.SplitLine( "Item1,,Item2,Item3" );
for( size_t i = 0; i < spl.NumTokens(); i++ )
{
    printf( "%s\n", spl.GetToken( i ) );
}

输出:

Item1

Item2
Item3

0

boost::tokenizer是您的朋友,但是请考虑使用wstring/ wchar_t代替旧版string/ char类型,从而使您的代码可参考国际化(i18n)问题进行移植。

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

using namespace std;
using namespace boost;

typedef tokenizer<char_separator<wchar_t>,
                  wstring::const_iterator, wstring> Tok;

int main()
{
  wstring s;
  while (getline(wcin, s)) {
    char_separator<wchar_t> sep(L" "); // list of separator characters
    Tok tok(s, sep);
    for (Tok::iterator beg = tok.begin(); beg != tok.end(); ++beg) {
      wcout << *beg << L"\t"; // output (or store in vector)
    }
    wcout << L"\n";
  }
  return 0;
}

“传统”绝对是不正确的,并且wchar_t是一种依赖于实现的可怕类型,除非绝对必要,否则任何人都不应使用。
CoffeeandCode 2014年

使用wchar_t不能以某种方式自动解决任何i18n问题。您可以使用编码来解决该问题。如果用定界符分割字符串,则暗示定界符不会与字符串中任何标记的编码内容发生冲突。可能需要转义,等等。wchar_t并非对此的神奇解决方案。
yonil 2015年

0

简单的C ++代码(标准C ++ 98),接受多个定界符(在std :: string中指定),仅使用向量,字符串和迭代器。

#include <iostream>
#include <vector>
#include <string>
#include <stdexcept> 

std::vector<std::string> 
split(const std::string& str, const std::string& delim){
    std::vector<std::string> result;
    if (str.empty())
        throw std::runtime_error("Can not tokenize an empty string!");
    std::string::const_iterator begin, str_it;
    begin = str_it = str.begin(); 
    do {
        while (delim.find(*str_it) == std::string::npos && str_it != str.end())
            str_it++; // find the position of the first delimiter in str
        std::string token = std::string(begin, str_it); // grab the token
        if (!token.empty()) // empty token only when str starts with a delimiter
            result.push_back(token); // push the token into a vector<string>
        while (delim.find(*str_it) != std::string::npos && str_it != str.end())
            str_it++; // ignore the additional consecutive delimiters
        begin = str_it; // process the remaining tokens
        } while (str_it != str.end());
    return result;
}

int main() {
    std::string test_string = ".this is.a.../.simple;;test;;;END";
    std::string delim = "; ./"; // string containing the delimiters
    std::vector<std::string> tokens = split(test_string, delim);           
    for (std::vector<std::string>::const_iterator it = tokens.begin(); 
        it != tokens.end(); it++)
            std::cout << *it << std::endl;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.