C ++中的Strtok给了我意想不到的行为

时间:2017-10-04 01:34:25

标签: c++

我有这个功能:

void simple_shell::parse_command(char* cmd, char** cmdTokens) {
  // TODO: tokenize the command string into arguments
  char *token = strtok(cmd, " ");
  int count = 0;

  while (token != NULL) {
      count = count + 1;
      cout << token << endl;
      token = strtok(NULL, " ");
  }

  char *secondToken = strtok(cmd, " ");
  *cmdTokens = new char[count];
  count = 0;

  while (secondToken != NULL) {
      cmdTokens[count] = secondToken;
      cout << secondToken << endl;
      secondToken = strtok(NULL, " ");
      count = count + 1;
  }
}

我基本上使用cmd并将其作为分隔符与空格分开。然后,在while循环中,我打印每个标记。

第一个while循环和第一个标记可以正常工作。如果字符串是“hello world”,那么我在两个单独的行上得到“hello”和“world”,这正是我想要的。

然而,在第二个循环中,出现了问题,我只得到“你好”,而没有别的。这是怎么回事?

1 个答案:

答案 0 :(得分:2)

strtok()对以null结尾的字符串作为输入进行操作,但修改字符串。当它在字符串中找到分隔符时,它会在分隔符所在的位置注入一个空字符,并返回指向注入的nul前面的标记的指针。在下一次使用NULL输入指针调用时,搜索在先前注入的nul。

之后开始

因此,当您运行第一个循环时,所有内容都正确输出,但strtok()已更改cmd的内容以使用nul字符替换所有空格字符。因此,当你运行第二个循环时,它不能再在cmd中找到任何空格字符,并且在第一个标记到达后的nul字符处停止查看,即使它不是真正的最后一个在字符串中。

所以,你有几个选择。

您可以在每个循环上复制cmd。您还必须复制存储在输出数组中的每个标记,因为它们将指向临时内存:

int simple_shell::parse_command(char* cmd, char*** cmdTokens)
{
    *cmdTokens = NULL;

    char *dup = strdup(cmd);
    if (!dup)
        return -1;

    int count = 0;

    char *token = strtok(dup, " ");
    while (token)
    {
        ++count;
        token = strtok(NULL, " ");
    }

    free(dup);

    try {
        *cmdTokens = new char*[count];
    }
    catch (const std::bad_alloc&) {
        return -1;
    }

    if (count > 0)
    {
        dup = strdup(cmd);
        if (!dup)
        {
            delete[] *cmdTokens;
            return -1;
        }

        token = strtok(dup, " ");
        count = 0;

        while (token)
        {
            cmdTokens[count] = strdup(token);
            if (!cmdTokens[count])
            {
                for(int i = 0; i < count; ++i)
                    free(cmdTokens[i]);
                delete[] *cmdTokens;
                return -1;
            }

            ++count;
            token = strtok(NULL, " ");
        }
    }

    return count;
}

char** tokens;
int numTokens = shell.parse_command("hello world", &tokens);
if (numTokens != -1)
{
    for (int i = 0; i < numTokens; ++i)
    {
        std::cout << tokens[i] << std::endl;
        free(tokens[i]);
    }
    delete[] tokens;
}

或者,您可以停止使用strtok()并使用破坏性较小的方法来提取子字符串。但是,您仍然需要复制单个标记,因为不再有cmd注入任何空字符:

const char* nextToken(const char *str)
{
    if (!str)
        return NULL;

    while (*str == ' ')
        ++str;

    if (*str == '\0')
        return NULL;

    return str;
}

const char* endOfToken(const char *str)
{
    if (!str)
        return NULL;

    while ((*str != ' ') && (*str != '\0'))
        ++str;

    return ptr;
}

int simple_shell::parse_command(char* cmd, char*** cmdTokens)
{
    *cmdTokens = NULL;

    char *ptr = cmd;
    int count = 0;

    const char *token = nextToken(ptr);
    while (token)
    {
        ++count;
        token = nextToken(endOfToken(token));
    }

    try {
        *cmdTokens = new char*[count];
    }
    catch (const std::bad_alloc&) {
        return -1;
    }

    if (count > 0)
    {
        ptr = cmd;
        count = 0;

        token = nextToken(ptr);
        while (token)
        {
            const char *end = endOfToken(token);
            int len = (end-token);

            try {
                cmdTokens[count] = new char[len+1];
            }
            catch (const std::bad_alloc&) {
                for(int i = 0; i < count; ++i)
                    delete[] cmdTokens[i];
                return -1;
            }

            memcpy(cmdTokens[count], token, len);
            cmdTokens[count][len] = '\0';
            ++count;

            token = nextToken(end);
        }
    }

    return count;
}

char** tokens;
int numTokens = shell.parse_command("hello world", &tokens);
if (numTokens != -1)
{
    for (int i = 0; i < numTokens; ++i)
    {
        std::cout << tokens[i] << std::endl;
        delete[] tokens[i];
    }
    delete[] tokens;
}

或者,如果你不想复制令牌,只需返回指向它们的指针,以及它们的长度:

struct token_info
{
    char* token;
    int length;
}

const char* nextToken(const char *str)
{
    if (!str)
        return NULL;

    while (*str == ' ')
        ++str;

    if (*str == '\0')
        return NULL;

    return str;
}

const char* endOfToken(const char *str)
{
    if (!str)
        return NULL;

    while ((*str != ' ') && (*str != '\0'))
        ++str;

    return ptr;
}

int simple_shell::parse_command(char* cmd, token_info** cmdTokens)
{
    *cmdTokens = NULL;

    char *ptr = cmd;
    int count = 0;

    const char *token = nextToken(ptr);
    while (token)
    {
        ++count;
        token = nextToken(endOfToken(token));
    }

    try {
        *cmdTokens = new token_info[count];
    }
    catch (const std:bad_alloc&) {
        return -1;
    }

    if (count > 0)
    {
        ptr = cmd;
        count = 0;

        token = nextToken(ptr);
        while (token)
        {
            const char *end = endOfToken(token);

            cmdTokens[count].token = const_cast<char*>(token);
            cmdTokens[count].length = (end-token);
            ++count;

            token = nextToken(end);
        }
    }

    return count;
}

token_info* tokens;
int numTokens = shell.parse_command("hello world", &tokens);
if (numTokens != -1)
{
    for (int i = 0; i < numTokens; ++i)
        std::cout << std::string(tokens[i].token, tokens[i].length) << std::endl;
    delete[] tokens;
}

但是,由于你显然正在使用C ++,你应该停止使用这种古老的C风格方法,而是使用更现代的C ++实践,例如:

std::vector<std::string> simple_shell::parse_command(const std::string &cmd)
{
    std::istringstream iss(cmd);
    return std::vector<std::string>(
        std::istream_iterator<std::string>(iss),
        std::istream_iterator<std::string>()
    );
}

std::vector<std::string> tokens = shell.parse_command("hello world");
for (auto &token: tokens)
    std::cout << token << std::endl;