到处是“坑”的strtok()—解读strtok()的隐含特性

在用C/C++实现字符串处理逻辑时,strtok函数的使用非常广泛,其主要作用是按照给定的字符集分隔字符串,并返回各子字符串。由于该函数的使用有诸多限制,如果使用不当就会造成很多“坑”,因此本文首先介绍那些经常误踩的坑,然后通过分析源代码,解读该函数的诸多隐含特性,以便对该函数有个全面的理解,不再被坑。

那些年一起踩过的坑

TOP1 不可重入

目前大部分程序都是在多线程环境下运行的,因此函数的可重入性就显得尤为重要。下面实例的本意是,先按照;分隔句子并输出,然后再按照空格分隔单词并输出,可结果呢?

#include <stdio.h>
#include <string.h>

int main(void)
{
    char  szTest[]  = "Hello world;I'm Pele"; /* 以;作为句子的分隔符,以空格作为单词的分隔符 */
    char *pSentence = NULL;
    char *pWord     = NULL;

    /* 先分隔句子 */
    pSentence = strtok(szTest, ";");
    while (NULL != pSentence)
    {
        printf("The sentence is %s.\n", pSentence);

        /* 再分隔单词 */
        pWord = strtok(pSentence, " ");
        while (NULL != pWord)
        {
            printf("The word is %s.\n", pWord);
            pWord = strtok(NULL, " ");
        }

        pSentence = strtok(NULL, ";");
    }

    return 0;
}

预期结果如下:

$ gcc test_strtok.c
$ ./a.out
The sentence is Hello world.
The word is Hello.
The word is world.
The sentence is I'm Pele.
The word is I'm.
The word is Pele.

可实际结果如下:

$ gcc test_strtok.c
$ ./a.out
The sentence is Hello world.
The word is Hello.
The word is world.

实际告诉我们:strtok不可重入

TOP2 源字符串会被修改

如果一个字符串在我们的视线之外被修改了,那么可能会发生一些列诡异的事,而我们却全然不知。请看如下案例:

#include <stdio.h>
#include <string.h>

int main(void)
{
    char  szTest[]  = "Hello world;I'm Pele";
    char *pSentence = NULL;

    printf("The original string is %s.\n", szTest);

    pSentence = strtok(szTest, ";");
    while (NULL != pSentence)
    {
        pSentence = strtok(NULL, ";");
    }

    printf("The final string is %s.\n", szTest);

    return 0;
}

预期结果如下:

$ gcc test_strtok.c
$ ./a.out
The original string is Hello world;I'm Pele.
The final string is Hello world;I'm Pele.

可实际结果如下:

$ gcc test_strtok.c
$ ./a.out
The original string is Hello world;I'm Pele.
The final string is Hello world.

实际告诉我们:你传入的字符串会被strtok修改

TOP3 连续的分隔符被当做一个分隔符处理

如果两个分隔符连续出现,那么在分隔的时候,你是希望分隔出一个空字符串,还是希望strtok忽略掉多余的分隔符呢?请看strtok给我们的答案:

#include <stdio.h>
#include <string.h>

int main(void)
{
    char  szTest[]  = "Hello world;;I'm Pele";  /* 连续使用两个;分隔语句 */
    char *pSentence = NULL;

    pSentence = strtok(szTest, ";");
    while (NULL != pSentence)
    {
        printf("The sentence is %s.\n", pSentence);
        pSentence = strtok(NULL, ";");
    }

    return 0;
}

实际结果如下:

$ gcc test_strtok.c
$ ./a.out
The sentence is Hello world.
The sentence is I'm Pele.

实际告诉我们:连续出现的分隔符只被处理一次

TOP4 字符串首尾的分隔符会被忽略

你希望将字符串首尾的分隔符忽略掉吗?如果不希望,那么请慎用strtok,请看:

#include <stdio.h>
#include <string.h>

int main(void)
{
    char  szTest[]  = ";Hello world;I'm Pele;;";
    char *pSentence = NULL;

    pSentence = strtok(szTest, ";");
    while (NULL != pSentence)
    {
        printf("The sentence is %s.\n", pSentence);
        pSentence = strtok(NULL, ";");
    }

    return 0;
}

实际结果如下:

$ gcc test_strtok.c
$ ./a.out
The sentence is Hello world.
The sentence is I'm Pele.

实际告诉我们:字符串首尾的分隔符会被忽略

真相只有一个

下面的代码取自glibc-2.20的strtok.c文件,未做任何删改,中文注释为笔者添加,用于说明造成以上坑的原因。

#include <string.h>


static char *olds; /* 使用了全局变量,因此该函数不可重入--TOP1坑 */

#undef strtok

#ifndef STRTOK
# define STRTOK strtok
#endif

/* Parse S into tokens separated by characters in DELIM.
   If S is NULL, the last string strtok() was called with is
   used.  For example:
    char s[] = "-abc-=-def";
    x = strtok(s, "-");     // x = "abc"
    x = strtok(NULL, "-=");     // x = "def"
    x = strtok(NULL, "=");      // x = NULL
        // s = "abc\0=-def\0"
*/
char *
STRTOK (char *s, const char *delim)
{
  char *token;

  if (s == NULL)
    s = olds;

  /* 跳过了字符串前面的分隔符,如果字符串只剩下尾部的分隔符,跳过前导符相当于忽略尾部的分隔符--TOP4坑 */
  /* Scan leading delimiters.  */  
  s += strspn (s, delim);
  if (*s == '\0')
    {
      olds = s;
      return NULL;
    }

  /* Find the end of the token.  */
  token = s;
  s = strpbrk (token, delim);  
  if (s == NULL)
    /* This token finishes the string.  */
    olds = __rawmemchr (token, '\0');
  else /* 找到一个分隔符就返回,下次进入该函数会跳过前导分隔符,此为TOP3坑 */
    {      
      /* Terminate the token and make OLDS point past it.  */
      *s = '\0';        /* 将分隔符所在位置置0,此为TOP2坑 */
      olds = s + 1;
    }
  return token;
}

总结

  • 尽量使用可重入版的strtok,Windows平台下为strtok_s,Linux平台下为strtok_r
  • 牢记strtok函数族的分隔规则:忽略字符串前后的分隔符,连续的分隔符被当做一个处理
  • 在使用strtok前,请对源字符串进行备份,除非你可以接受字符串被修改这一事实。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值