在C/C++中,使用空格作为分隔符将字符串拆分为字符串数组的更好方法

14
抱歉,我的C/C++不太好,但是下面的现有代码即使对我来说也看起来像垃圾。它还有一个bug-当str =“07/02/2010”以'\0'结尾时会失败。我认为,与其修复错误,不如重写它。在Python中,它只是'kas\nhjkfh kjsdjkasf'.split()。我知道这是C风格的代码,但拆分字符串肯定不会那么复杂!如何在保持相同签名的情况下且不使用额外的库来改进它-使它简短而易懂?我可以说出这段代码存在问题,例如由于最后的else子句而导致的异味。

失败的行:

_tcsncpy_s(
    s.GetBuffer((int) (nIndex-nLast)),
    nIndex-nLast,
    psz+nLast,
    (size_t) (nIndex-nLast)
);

使用字符串 "07/02/2010" 并以 '\0' 结束时,它将尝试将 11 个字符写入一个只有 10 个字符长的缓冲区。

完整函数:

#define 

// This will return the text string as a string array
// This function is called from SetControlText to parse the
// text string into an array of CStrings that the control
// Gadgets will attempt to interpret

BOOL CLVGridDateTimeCtrl::ParseTextWithCurrentFormat(const CString& str, const CGXStyle* pOldStyle, CStringArray& strArray )
{
    // Unused:
    pOldStyle;

    // we assume that the significant segments are seperated by space

    // Please change m_strDelim to add other delimiters

    CString s;

    LPCTSTR psz = (LPCTSTR) str;

    BOOL bLastCharSpace = FALSE;
    DWORD size = str.GetLength()+1;

    // (newline will start a new row, tab delimiter will
    // move to the next column).
    // parse buffer (DBCS aware)
    for (DWORD nIndex = 0, nLast = 0; nIndex < size; nIndex += _tclen(psz+nIndex))
    {
        // check for a delimiter
        if (psz[nIndex] == _T('\0') || _tcschr(_T("\r\n"), psz[nIndex]) || _tcschr(_T(" "), psz[nIndex])
            ||!_tcscspn(&psz[nIndex], (LPCTSTR)m_strDelim))
        {
            s.ReleaseBuffer();
            s.Empty();
            // abort parsing the string if next char
            // is an end-of-string
            if (psz[nIndex] == _T('\0'))
            {
                if (psz[nIndex] == _T('\r') && psz[nIndex+1] == _T('\n'))
                    nIndex++;

                _tcsncpy_s(s.GetBuffer((int) (nIndex-nLast)),
                    nIndex-nLast,
                            psz+nLast,
                            (size_t) (nIndex-nLast));
                CString temStr = s;
                strArray.Add(temStr);
                temStr.Empty();
                break;
            }

            else if (_tcscspn(&psz[nIndex], (LPCTSTR)m_strDelim) == 0 && !bLastCharSpace)
            {
                if (psz[nIndex] == _T('\r') && psz[nIndex+1] == _T('\n'))
                    nIndex++;

                _tcsncpy_s(s.GetBuffer((int) (nIndex-nLast)),
                    nIndex-nLast,
                            psz+nLast,
                            (size_t) (nIndex-nLast));
                CString temStr = s;
                strArray.Add(temStr);
                temStr.Empty();
                bLastCharSpace = TRUE;
                // abort parsing the string if next char
                // is an end-of-string
                if (psz[nIndex+1] == _T('\0'))
                    break;

            }
            // Now, that the value has been copied to the cell,
            // let's check if we should jump to a new row.
            else if (_tcschr(_T(" "), psz[nIndex]) && !bLastCharSpace)
            {
                if (psz[nIndex] == _T('\r') && psz[nIndex+1] == _T('\n'))
                    nIndex++;

                _tcsncpy_s(s.GetBuffer((int) (nIndex-nLast)),
                    nIndex-nLast,
                            psz+nLast,
                            (size_t) (nIndex-nLast));
                CString temStr = s;
                strArray.Add(temStr);
                temStr.Empty();
                bLastCharSpace = TRUE;
                // abort parsing the string if next char
                // is an end-of-string
                if (psz[nIndex+1] == _T('\0'))
                    break;
            }

            nLast = nIndex + _tclen(psz+nIndex);


        }
        else
        {   
            // nLast = nIndex + _tclen(psz+nIndex);
            bLastCharSpace = FALSE;
        }
    }
    if (strArray.GetSize())
        return TRUE;
    else
        return FALSE;
}
编辑: m_strDelim = _T(",");这个成员变量仅在这个函数中使用。我现在明白分词的意义了——它试图解析日期和时间……等等!下面是调用这个函数的代码,请帮助我改进一下。我的一些同事声称C#对他们的生产力没有比C++更有帮助。我曾经觉得自己不如他们,感到很愚蠢。
// SetControlText will attempt to convert the text to a valid date first with
// the help of COleDateTime and then with the help of the Date control and the
// current format

BOOL CLVGridDateTimeCtrl::ConvertControlTextToValue(CString& str, ROWCOL nRow, ROWCOL nCol, const CGXStyle* pOldStyle)
{
    CGXStyle* pStyle = NULL;
    BOOL bSuccess = FALSE;

    if (pOldStyle == NULL)
    {
        pStyle = Grid()->CreateStyle();
        Grid()->ComposeStyleRowCol(nRow, nCol, pStyle);
        pOldStyle = pStyle;
    }

    // allow only valid input
    {
        // First do this
        CLVDateTime dt;

        if (str.IsEmpty())
        {
            ;
            // if (Grid()->IsCurrentCell(nRow, nCol))
            //  Reset();
            bSuccess = TRUE;
        }
        else if (dt.ParseDateTime(str,CLVGlobals::IsUSDateFormat()) && (DATE) dt != 0)
        {
            SetDateTime(dt);
            if (m_bDateValueAsNumber)
                str.Format(_T("%g"), (DATE) dt);
            else
                str = dt.Format();
            bSuccess = TRUE;
        }
        else
        {
            // parse the string using the current format
            CStringArray strArray;
            if (!ParseTextWithCurrentFormat(str, pOldStyle, strArray))
                return FALSE;

            UpdateNullStatus(m_TextCtrlWnd);

            SetFormat(m_TextCtrlWnd, *pOldStyle);

            int nArrIndex = 0;
            for(int i=0; i<m_TextCtrlWnd.m_gadgets.GetSize(); i++)
            {
                int val = m_TextCtrlWnd.m_gadgets[i]->GetValue();   
                // s.Empty();
                if(m_TextCtrlWnd.m_gadgets[i]->IsKindOf(RUNTIME_CLASS(SECDTNumericGadget)))
                {
                    // TRACE(_T("The value %s\n"), strArray[nArrIndex]);
                    ((CLVDTNumericGadget*)m_TextCtrlWnd.m_gadgets[i])->m_nNewValue = _ttoi(strArray[nArrIndex]);    
                    nArrIndex++;
                    if (nArrIndex>strArray.GetUpperBound())
                            break;
                }
                else if(m_TextCtrlWnd.m_gadgets[i]->IsKindOf(RUNTIME_CLASS(SECDTListGadget)) && val!=-1)
                {
                    int nIndex = ((CLVDTListGadget*)m_TextCtrlWnd.m_gadgets[i])->FindMatch(strArray[nArrIndex], ((CLVDTListGadget*)m_TextCtrlWnd.m_gadgets[i])->GetValue()+1);
                    if (nIndex!=-1)
                    {
                        // TRACE(_T("The value %s\n"), strArray[nArrIndex]);
                        ((CLVDTListGadget*)m_TextCtrlWnd.m_gadgets[i])->SetValue(nIndex);
                        nArrIndex++;
                        if (nArrIndex>strArray.GetUpperBound())
                            break;
                    }

                }

                CLVDBValue dbDate = m_TextCtrlWnd.GetDateTime();
                if (dbDate.IsNull())
                    str = _T("");
                else
                {
                    CLVDateTime dt = (CLVDateTime)dbDate;
                    if (m_bDateValueAsNumber)
                        str.Format(_T("%g"), (DATE) dt);
                    else
                        str = dt.Format();
                }
            }
            bSuccess = TRUE;
        }
    }

    if (pStyle)
        Grid()->RecycleStyle(pStyle);

    return bSuccess;
}
7个回答

14

String Toolkit Library (Strtk)提供了以下解决方案:

#include <string>
#include <deque>
#include "strtk.hpp"
int main()
{ 
   std::string data("kas\nhjkfh kjsdjkasf");
   std::deque<std::string> str_list;
   strtk::parse(data, ", \r\n", str_list);
   return 0;
}

更多示例可以在这里找到


嗯...我在想是否可以只偷几个头文件和cpp文件而不必安装整个东西。 - Hamish Grubijan
19
@Hamish:你可以随意使用,这些都是CPL下的内容。如果你没有Boost,或者只使用带有STL的原始C ++,你可以注释掉#define ENABLE_LEXICAL_CAST #define ENABLE_RANDOM #define ENABLE_REGEX,它应该可以正常工作,这在readme.txt中都有解释。 - Matthieu N.

6

在C++中,使用stringstream可能是最简单的:

std::istringstream buffer("kas\nhjkfh kjsdjkasf");

std::vector<std::string> strings;

std::copy(std::istream_iterator<std::string>(buffer),
          std::istream_iterator<std::string>(),
          std::back_inserter(strings));

我并没有尝试完全保持相同的签名,因为大部分都是非标准的,所以不适用于C++的一般情况。

另一个可能性是使用Boost::tokenizer,但显然这涉及到另一个库,所以我不会详细介绍它。

我不确定这是否符合“奇异语法”的要求。我可能需要在这个部分上再努力一些......

编辑:我明白了——直接初始化向量:

std::istringstream buffer("kas\nhjkfh kjsdjkasf");

std::vector<std::string> strings(
    (std::istream_iterator<std::string>(buffer)),
    std::istream_iterator<std::string>());

“奇怪”的部分是,如果第一个参数周围没有额外的括号,这将调用“最令人烦恼的解析”,因此它将声明一个函数而不是定义一个向量。:-)
编辑2:就编辑问题而言,直接回答似乎几乎不可能——它依赖于太多既非标准又未经解释的类型(例如CGXStyle、CLVDateTime)。我自己无法详细了解它。一开始让用户输入更或多或少模棱两可的东西,然后再尝试整理这些混乱的东西,看起来像是一个相当糟糕的设计。最好使用一个只允许明确输入的控件,然后您可以直接读取包含日期和时间的一些字段。
编辑3:执行拆分并将逗号视为分隔符的代码可以这样做:
#include <iostream>
#include <locale>
#include <algorithm>
#include <vector>
#include <sstream>

class my_ctype : public std::ctype<char> {
public:
    mask const *get_table() { 
        // this copies the "classic" table used by <ctype.h>:
        static std::vector<std::ctype<char>::mask> 
            table(classic_table(), classic_table()+table_size);

        // Anything we want to separate tokens, we mark its spot in the table as 'space'.
        table[','] = (mask)space;

        // and return a pointer to the table:
        return &table[0];
    }
    my_ctype(size_t refs=0) : std::ctype<char>(get_table(), false, refs) { }
};

int main() { 
    // put our data in a strea:
    std::istringstream buffer("first kas\nhjkfh kjsdjk,asf\tlast");

    // Create a ctype object and tell the stream to use it for parsing tokens:
    my_ctype parser;
    buffer.imbue(std::locale(std::locale(), &parser));

    // separate the stream into tokens:
    std::vector<std::string> strings(
        (std::istream_iterator<std::string>(buffer)),
        std::istream_iterator<std::string>());

    // copy the tokes to cout so we can see what we got:
    std::copy(strings.begin(), strings.end(), 
        std::ostream_iterator<std::string>(std::cout, "\n"));
    return 0;
}

很酷,我们使用VS2010进行编译,所以Boost有点超纲了,但我相信有很多库可用。 - Hamish Grubijan
8
杰瑞,我应该在哪里指定分词的字符列表?请参考上面Beh Tou Cheh的答案。他使用了以下代码:strtk::parse(data, ", \r\n", str_list); - Hamish Grubijan
1
它们是在流使用的“locale”中指定的。默认情况下,它只会是空格,但您可以创建一个具有使用任何内容的“ctype facet”的“locale”。https://dev59.com/lnI-5IYBdhLWcg3wW3Cp#1895584 - Jerry Coffin
Boost.Tokenizer非常适合拆分字符串。对于那些熟悉STL的人来说,迭代器接口也非常容易使用。 - Dr. Watson
Jerry,这个程序对你来说完全编译了吗?我遇到了这个问题:thefile.cpp(1404): error C2228: left of '.imbue' must have class/struct/union以及:thefile.cpp(1412): error C2440: '<function-style-cast>' : cannot convert from 'std::istringstream (__cdecl *)(CStringA)' to 'std::istream_iterator<_Ty>' - Hamish Grubijan
显示剩余2条评论

4
最好的方法是使用strtok。该链接应该可以自我解释如何使用它,您还可以使用多个分隔符。非常方便的C函数。

1
+1,但我相信肯定会有人提出疯狂的C++解决方案,包括奇怪的语法,以夺走你的赞数。 - Carl Norum
1
这是一个适用于C语言的好解决方案。如果您想使用C ++,那么利用该语言的优势进行编码并不“疯狂”。 - Cogwheel
11
如果使用strtok函数是正确的答案,通常意味着你问错了问题。 - Jerry Coffin
3
关于strtok值得注意的一个小细节是它不是线程安全的。我不确定这是否真正适用于提问者的问题,但仍值得注意。 - Tom
1
通常情况下,strtok() 修改其输入并没有什么问题 - 在大多数需要使用 strtok() 进行解析的情况下,您不再关心未解析的字符串。字符串字面量的抱怨也是虚假的; 编译时常量字符串的运行时解析绝对是一个边角案例。 - caf
显示剩余12条评论

1

解决这个问题的一种过度的方式是使用Qt库。如果您正在使用KDE,则它们已经安装好了。 QString类有一个成员函数split,其工作方式类似于Python版本。例如:

QString("This is a string").split(" ", QString::SkipEmptyParts)

返回一个QStringList的QString列表:
["This", "is", "a", "string"]

(使用Pythonic语法)。请注意,第二个参数是必需的,否则如果单词由多个空格分隔,则每个单独的空格都将被返回。

通常情况下,我发现借助Qt库的帮助,大多数Python的简单操作,例如简单的字符串解析和列表迭代,都可以轻松处理,并且具有C++的强大功能。


4
我们是一个微软的用户群体,所以我可以使用随同VS2010一起提供的标准库。我喜欢Linux、开源等,但我不能够安装任意的库。不过,如果许可证允许的话,我可以借鉴一些.h和.cpp文件。 - Hamish Grubijan

0
你可以使用 boost::algorithm::split。例如:
std::string myString;
std::vector<std::string> splitStrings;
boost::algorithm::split(splitStrings, myString, boost::is_any_of(" \r\n"));

+1 Boost字符串算法库对字符串操作至关重要! - Matthieu M.
我真的很希望知道为什么人们会对这个进行负投票。 - Billy ONeal
我没有点踩。我想这是因为我的评论(而不是原始帖子)表明我更喜欢只使用标准库... - Hamish Grubijan
@Hamish Grubijan:是的,同意。我本来想删除我的回答,但我看到这个问题得到最多赞的答案是建议使用另一个库... - Billy ONeal
6
@Billy: 我已经点赞了,希望你不会哭出来 :D - Matthieu N.
@Beh: 谢谢 :) 我并没有对downvotes有什么大问题 -- 只是如果有人要downvote这个答案,他们应该至少留下一个评论,说明为什么这个答案有问题。 - Billy ONeal

0
在C/C++中解析字符串很少是一件简单的事情。您发布的方法似乎涉及了相当多的“历史”。例如,您声明要在空格上拆分字符串。但是该方法本身似乎正在使用成员变量m_strDelim作为拆分决策的一部分。简单地替换该方法可能会导致其他意外问题。
使用现有的标记化类库(例如Boost库)可以大大简化事情。

4
在C/C++中解析字符串很少是一件简单的事情。虽然这是一个简单的问题,但解决方案通常涉及逐个字符地处理字符串和循环。也就是说,我想不出类似于Python中的split()函数那样的(标准)解决方案在C++中存在。 - SigTerm

0
比我之前的答案更好的方法是使用TR1的正则表达式功能。这里有一个小教程可以帮助你入门。这个答案是用C++编写的,使用了正则表达式(也许是拆分字符串最好/最容易的方法),我最近自己也用过它,所以我知道它是一个不错的工具。

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