转载自 https://blog.csdn.net/astrotycoon/article/details/50813959

为什么写本文

最近工作中经常需要解析字符串,并且这些字符串都有一个共同的特点,那就是它们由一个或者多个分隔符(delimiter)隔开,而我要做的就是提取出由这些分隔符隔开的诸多子串。

我们来看一个例子,假设DHCP服务器返回的信息如下格式:

网络地址:子网掩码:默认网关:DNS地址1:DNS地址2

这里为了简单化问题,字符串中只有一种分隔符“:”,当然我相信这是现实中最常见的一种情况。现在的任务就是正确地提取出网络地址、子网掩码、默认网关以及两个DNS地址。需要指出的是,因为有时DHCP服务器会发生异常,导致返回的信息不完整,例如192.168.6.138:255.255.255.0::202.38.64.1:114.114.114.114,看到没有? 默认网关没有正确获取到,因此返回的字符串中有连续的两个分隔符“:”,彼此之间是没有空格的。

我首先想到的办法是使用sscanf函数,如下:

const char *dhcpargs = "192.168.6.138:255.255.255.0::202.38.64.1:114.114.114.114";
char ip[32], netmask[32], gateway[32], dns[2][32];
int ret = sscanf(dhcpargs, "%[^:]:%[^:]:%[^:]:%[^:]:%s", ip, netmask, gateway, dns[0], dns[1]);
结果发现不可行,sscanf函数返回值为2,网络地址和子网掩码正确解析出来了,但是DNS地址都没有解析出来,也就是说sscanf无法正确解析空字符串,如果遇到空字符串就停止后续的解析 -- 确切的说,是说明符[ ]无法匹配空字符串。

后来了解到C语言中有函数来分割字符串,分别为strtok和strsep函数。strtok是标准C库函数,strsep不是,但是现如今的C库几乎全部有strsep的实现,就连linux内核也已经很早开始使用strsep,而放弃使用strtok了。

在使用过程中,我发现strtok不能胜任以上的情况,最终选择了strsep函数。还发现了这两个函数的诸多相同点和不同点,以及使用这两个函数容易犯的错误,本文就结合这两个函数的源码来简单分析下这两个函数的异同点。

函数源码

代码来自glibc-2.24,strtok源码如下:

/* Copyright (C) 1991-2016 Free Software Foundation, Inc.
   This file is part of the GNU C Library.
   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.
   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.
   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, see
   <http://www.gnu.org/licenses/>.  */
 
#include <string.h>
 
 
static char *olds;
 
#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;
 
  /* 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
    {
      /* Terminate the token and make OLDS point past it.  */
      *s = '\0';
      olds = s + 1;
    }
  return token;
}
strtok代码整体流程如下:

(1)判断参数s是否为NULL。不为NULL则就以s为起始点开始分割;如果是NULL,证明不是第一次调用,则从上一次olds保存的位置处开始分割。

(2)跳过开始位置的所有分隔字符,直到遇到第一个非分割字符。函数strspn用来计算开始处有多少个连续分割字符。

(3)如果一走来就遇到了'\0'字符,证明字符串已经分割完成,因此返回NULL。不然则继续。

(4)先保存待分割字符串的首地址,也就是本次待分割出的子串的首地址。接着调用函数strpbrk寻找后续的第一个分隔符:如果返回值为NULL,证明后续的所有字符中没有分隔符了,那么使olds指向字符串的末尾,即'\0',函数返回s。如果找到了,那么将其覆盖写为了'\0',并且使old指向后面的第一个字符,函数返回s。

再来看看strsep的源码如下:

/* Copyright (C) 1992-2016 Free Software Foundation, Inc.
   This file is part of the GNU C Library.
   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.
   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.
   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, see
   <http://www.gnu.org/licenses/>.  */
 
#include <string.h>
 
#undef __strsep
#undef strsep
 
char *
__strsep (char **stringp, const char *delim)
{
  char *begin, *end;
 
  begin = *stringp;
  if (begin == NULL)
    return NULL;
 
  /* A frequent case is when the delimiter string contains only one
     character.  Here we don't need to call the expensive `strpbrk'
     function and instead work using `strchr'.  */
  if (delim[0] == '\0' || delim[1] == '\0')
    {
      char ch = delim[0];
 
      if (ch == '\0')
    end = NULL;
      else
    {
      if (*begin == ch)
        end = begin;
      else if (*begin == '\0')
        end = NULL;
      else
        end = strchr (begin + 1, ch);
    }
    }
  else
    /* Find the end of the token.  */
    end = strpbrk (begin, delim);
 
  if (end)
    {
      /* Terminate the token and set *STRINGP past NUL character.  */
      *end++ = '\0';
      *stringp = end;
    }
  else
    /* No more delimiters; this is the last token.  */
    *stringp = NULL;
 
  return begin;
}
weak_alias (__strsep, strsep)
strong_alias (__strsep, __strsep_g)
libc_hidden_def (__strsep_g)
个人认为strsep的源码比strtok的写的要好,主要体现在函数内局部变量的命名上,简单明了。strsep代码的整体流程如下:

(1)begin指向参数stringp,即传递进来的字符串首地址。如果首地址为NULL,则什么也不做,返回NULL;否则继续。

(2)获取后续字符中第一个分割字符的位置,如果没有获取到,设置*stringp为NULL,返回begin。否则覆盖写找到的分隔符为'\0',并且是*stringp指向后续的第一个字符。

相同点

好了,看完了源码,想必已经看出了些许两者的相同之处了吧?总结如下:

(1)两者都会修改原字符串,因此这个字符串可千万不能存储在只读内存区域里(也就是大家常说的string literal,中文一般叫做字符串字面值),否则运行时会发生断错误。可以是字符数组(存储在数据段.data,或者栈上),或者是存储在动态申请的内存(堆)里的字符串。

(2)如果处理过程中没有找到分隔符,则返回的就是传递进去的字符串的首地址。

不同点

(1)strtok函数使用了全局静态变量,这就使得它不是线程安全函数或者说是不可重入函数。而strsep通过二级指针用来替代strtok中的全局指针的功能,因此它是线程安全函数,glibc中有个strtok_r函数,它同样是通过提供二级指针的功能来保证线程安全的,在不考虑移植的情况下,应尽量使用strtok_r函数。

(2)strtok函数在扫描一个字符串时,会忽略掉一开始的分隔符,而strsep不会忽略,而是覆盖写成'\0',并且返回一个空字符串。因此strtok的返回值只有两种情况:非空字符串首地址和NULL;strsep的返回值有三种可能:空字符串首地址,非空字符串首地址以及NULL。这点尤其重要,会导致strtok和strsep的行为不同,也是最迷惑人的地方。

(3)调用方式的区别:strtok函数第一次调用时第一个参数必须提供待分割字符串的首地址,而后续调用必须设置成NULL。strsep的调用方式一直不变。

实际例子

先来说下为什么文章一开始我说最终选择了strsep函数,根据上面的不同点(2)可知,strsep在处理网关时会返回空字符串,这是我想要的,为空就知道是DHCP服务器发生异常导致的,而strtok却直接跳过去解析DNS,这就导致把DNS地址当成网关了,这完全不是我想要的结果嘛!

好了,来看个比较典型并且简单的例子,重点是向读者展示两个函数扫描字符串的不同方式!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(int argc, const char *argv[])
{
    char buf[] = "abadcbaf";
    char *result;
 
#if 0
    for (result = strtok(buf, "ab"); result != NULL; result = strtok(NULL, "ab")) {
        printf("result = %s\n", result);
    }
#else
    char *pbuf = buf;
    while ((result = strsep(&pbuf, "ab")) != NULL) {
        printf("result = %s\n", result);
    }
#endif
 
    exit(EXIT_SUCCESS);
}
接下来我以图形方式向读者展示两个函数的扫描过程,你会发现两个函数的扫描次数是不同的。

strtok函数处理过程:

(1)初始状态

(2)第一次分割后(忽略前三个分隔符,覆盖写找到的第一个分割符b为'\0',olds指向后续字符a,返回字符串"dc")

(3)第二次分割后(忽略分割字符a,olds执行字符串尾部,返回字符串"f")

(4)最后一次(第三次)分割后(函数返回NULL,olds指向字符串尾部)

接下来看strsep的处理过程:

(1)初始状态

(2)第一次分割(覆盖写字符a为'\0', stringp指向字符b,返回空字符串)

(3)第二次分割(覆盖写字符b为'\0',stringp指向后续的字符a,返回空字符串)

(4)第三次分割(覆盖写字符a为'\0',stringp指向后续字符d,返回空字符串)

(5)第四次分割(覆盖写字符b为'\0',stringp指向后续字符a,返回字符串"dc")

(6)第五次分割(覆盖写字符a为'\0',stringp指向后续的字符f,返回空字符串)

(7)第六次分割(stringp指向NULL,返回字符串"f")

(8)第七次分割(因为stringp为NULL, 什么都不做,函数返回NULL)

通过对比发现:strtok函数实际分割了2次,strsep实际分割了6次。

总结

对strtok和strsep这两个函数,网上有不少文章讲解,我发信很多存在错误,究其原因是他们没有结合源码来分析,甚至有猜测的成分在里面,导致对它们的理解不够透彻。而strtok和strsep的源码我们又是唾手可得的,那还不如直接看源码,这样来的痛快,也不会存在理解上的模棱两可。

参考链接:

《Linux C函数strtok解析》

《关于函数strtok和strtok_r的使用要点和实现原理(一)》

《What are the differences between strtok and strsep inC》

补充:

今天简单看了下VS下strtok的实现,发现它的实现跟GNU的实现方式不大一样,因此简单分析如下。

先看源码如下:

/***
*strtok.c - tokenize a string with given delimiters
*
*       Copyright (c) Microsoft Corporation. All rights reserved.
*
*Purpose:
*       defines strtok() - breaks string into series of token
*       via repeated calls.
*
*******************************************************************************/
 
#include <cruntime.h>
#include <string.h>
#ifdef _SECURE_VERSION
#include <internal.h>
#else  /* _SECURE_VERSION */
#include <mtdll.h>
#endif  /* _SECURE_VERSION */
 
/***
*char *strtok(string, control) - tokenize string with delimiter in control
*
*Purpose:
*       strtok considers the string to consist of a sequence of zero or more
*       text tokens separated by spans of one or more control chars. the first
*       call, with string specified, returns a pointer to the first char of the
*       first token, and will write a null char into string immediately
*       following the returned token. subsequent calls with zero for the first
*       argument (string) will work thru the string until no tokens remain. the
*       control string may be different from call to call. when no tokens remain
*       in string a NULL pointer is returned. remember the control chars with a
*       bit map, one bit per ascii char. the null char is always a control char.
*
*Entry:
*       char *string - string to tokenize, or NULL to get next token
*       char *control - string of characters to use as delimiters
*
*Exit:
*       returns pointer to first token in string, or if string
*       was NULL, to next token
*       returns NULL when no more tokens remain.
*
*Uses:
*
*Exceptions:
*
*******************************************************************************/
 
#ifdef _SECURE_VERSION
#define _TOKEN *context
#else  /* _SECURE_VERSION */
#define _TOKEN ptd->_token
#endif  /* _SECURE_VERSION */
 
#ifdef _SECURE_VERSION
char * __cdecl strtok_s (
        char * string,
        const char * control,
        char ** context
        )
#else  /* _SECURE_VERSION */
char * __cdecl strtok (
        char * string,
        const char * control
        )
#endif  /* _SECURE_VERSION */
{
        unsigned char *str;
        const unsigned char *ctrl = control;
 
        unsigned char map[32];
        int count;
 
#ifdef _SECURE_VERSION
 
        /* validation section */
        _VALIDATE_RETURN(context != NULL, EINVAL, NULL);
        _VALIDATE_RETURN(string != NULL || *context != NULL, EINVAL, NULL);
        _VALIDATE_RETURN(control != NULL, EINVAL, NULL);
 
        /* no static storage is needed for the secure version */
 
#else  /* _SECURE_VERSION */
 
        _ptiddata ptd = _getptd();
 
#endif  /* _SECURE_VERSION */
 
        /* Clear control map */
        for (count = 0; count < 32; count++)
                map[count] = 0;
 
        /* Set bits in delimiter table */
        do {
                map[*ctrl >> 3] |= (1 << (*ctrl & 7));
        } while (*ctrl++);
 
        /* Initialize str */
 
        /* If string is NULL, set str to the saved
         * pointer (i.e., continue breaking tokens out of the string
         * from the last strtok call) */
        if (string)
                str = string;
        else
                str = _TOKEN;
 
        /* Find beginning of token (skip over leading delimiters). Note that
         * there is no token iff this loop sets str to point to the terminal
         * null (*str == '\0') */
        while ( (map[*str >> 3] & (1 << (*str & 7))) && *str )
                str++;
 
        string = str;
 
        /* Find the end of the token. If it is not the end of the string,
         * put a null there. */
        for ( ; *str ; str++ )
                if ( map[*str >> 3] & (1 << (*str & 7)) ) {
                        *str++ = '\0';
                        break;
                }
 
        /* Update nextoken (or the corresponding field in the per-thread data
         * structure */
        _TOKEN = str;
 
        /* Determine if a token has been found. */
        if ( string == str )
                return NULL;
        else
                return string;
}
可能最让人迷惑的就是这个数组map,它的用途是什么? 又为什么它的大小是32呢?

不难看出,map数组是用于记录分隔符的,但是又不是单纯的记录,确切地说,它是一个位集,每个字节8个bit,因此32*8共256,足以表示所有的ascii。

因此map数组的每个元素代表8个ascii,如果对应的ascii字符存在,对应的bit设置为1,否则为0。

我们可以看一个具体的例子,在ascii表中从字符@到字符G的十进制值为64~71,正好是数组map的第9个元素,即map[8] -- 也就是说map[8]字节中的每一个bit的状态代表分隔符总是否存在字符@到G。

这样就比较清楚了。其实右移3位,相当于除以8,&7相当于取8的模。

/* Set bits in delimiter table */
        do {
                map[*ctrl >> 3] |= (1 << (*ctrl & 7));
        } while (*ctrl++);
这段代码就是讲分隔符映射到map数组中。
        /* Find beginning of token (skip over leading delimiters). Note that
         * there is no token iff this loop sets str to point to the terminal
         * null (*str == '\0') */
        while ( (map[*str >> 3] & (1 << (*str & 7))) && *str )
                str++;
这段代码即忽略前缀的分隔符。
好,差不多了明白了吧。

参考链接:

《 strtok源码剖析 位操作与空间压缩》

《杭电水题--排序 关于strtok的一些问题》
--------------------- 
作者:astrotycoon 
来源:CSDN 
原文:https://blog.csdn.net/astrotycoon/article/details/50813959 
版权声明:本文为博主原创文章,转载请附上博文链接!

字符串分割函数strtok和strsep使用注意事项相关推荐

  1. 字符串分割函数--strtok与strsep

    在c/c++中,字符串分割函数主要有两种:一是strtok函数,另一个就是strsep函数.下面我们对这两个函数作一个详细解释说明. 1.strtok 原形: char* strtok(char *s ...

  2. c语言strsep,C/C++ 字符串分割: strtok 与 strsep 函数说明(示例代码)

    函数原型: char *strtok(char *s, const char *delim); char *strsep(char **s, const char *delim); 功能:strtok ...

  3. strtok(), strtok_s() 字符串分割函数

    strtok(), strtok_s() 字符串分割函数 //strtok()函数原型/*_Check_return_ _CRT_INSECURE_DEPRECATE(strtok_s) _CRTIM ...

  4. PHP字符串分割函数explode,strtok,str_spli的用法

    PHP字符串函数在PHP网站开发中广泛使用,比如使用PHP字符串函数对字符串分割.截取.匹配.替换等处理.PHP字符串函数对于PHP入门学习者来说必不可少,本文将主要介绍PHP字符串分割函数处理心得, ...

  5. 探索C语言之字符串分割函数:strtok和strsep的区别

    探索C语言之字符串分割函数:strtok和strsep的区别 概述 strsep - extract token from string(linux 下) strtok, strtok_r - ext ...

  6. c语言中字符切割函数split,c 语言 指针 C语言字符串分割函数split实现

    c语言也有类似其他语言的split字符串分割函数,就是strtok使用方法如下://用逗号做分隔符,读出内容//char s[] = "123-sldkf-123ls-343434-dfjd ...

  7. C++中string如何实现字符串分割函数split()——4种方法

    如: string str1 = "This is a test"; string str2 = "This-is-a-test"; string str2 = ...

  8. Split字符串分割函数

    非常非常常用的一个函数Split字符串分割函数. Dim myTest myTest = "aaa/bbb/ccc/ddd/eee/fff/ggg"Dim arrTest arrT ...

  9. C++的字符串分割函数

    转载:http://blog.csdn.net/shanzhizi/article/details/17189267 C++的字符串没有分割函数,因此需要自己写方便使用.而受到开发工具的影响,有很多用 ...

最新文章

  1. [HNOI2016] 大数(莫队)
  2. /etc/sysconfig/i18n文件详解
  3. 使用 Web Tracing Framework 分析富 JS 应用的性能
  4. php 批量更新死锁,php – 在尝试获取锁定时,哪个查询导致死锁;尝试重新启动事务...
  5. python去噪音_python中的噪声是什么意思
  6. php tool pagination,php实现分页工具类分享
  7. lambda 复制数组
  8. 在java编程中会使用汉字字符_在java程序中将中文字符写入文件中或者是将文件中的中文字符读入程序时会出现乱码或者一串“?”,求...
  9. Android平台ROM的定制及精简教程
  10. 【OOP编程思想小感悟】
  11. Rayman的绝顶之路——Leetcode每日一题打卡20
  12. springboot设置局域网访问
  13. spring-cloud-context源码解读
  14. Activiti表结构
  15. 内存的分配与回收实验
  16. 看完嵌入式培训视频的一些感受
  17. 【PC工具】开源绿色好看实用桌面时钟Digital Clock 4
  18. 1030个!电子设计电子竞赛毕业设计产品开发(吐血推荐)
  19. Spring图文详细教学笔记
  20. 用什么软件可以检测苹果耳机芯片_头戴耳机如何支持光学入耳检测?穿透网布、响应速度是难题!...

热门文章

  1. 【转】Linux内核大讲堂 (一) 设备驱动的基石驱动模型(1)
  2. 继滴滴之后,网约车遇行业大洗牌
  3. 安防监控那些知名品牌!
  4. 编写Chrome扩展插件教程
  5. 初学Python的学习笔记2----dist字典,set集合,声明函数,函数参数
  6. 使用冰狐智能辅助实现找图(findImage)
  7. 【1399】甲流病人初筛
  8. parallelstudio license 位置_《看门狗军团》镜头照相坐标文职攻略 镜头位置在哪
  9. 转:2020-21软件测试中的重要趋势及应对措施
  10. android 桌面图标的点击放大效果,Android仿英语流利说取词放大控件的实现方法(附demo源码下载)...