C语言源码剖析与实现——strtok()系列函数实现

Posted C_YCBX Py_YYDS

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言源码剖析与实现——strtok()系列函数实现相关的知识,希望对你有一定的参考价值。

源码剖析与实践

strtok() 源代码实现

由于是用的static实现的全局变量存储地址,所以只需要传一次,后面传NULL,即可对之前的字符串进行操作。

  • 但同样也造成了一个问题:如果有两个线程都用了这一个函数,由于是全局变量,所以他们会共享这个变量,如果两个线程处理的是不同的两个字符串,而它们的地址存储都是用的同一个变量,这会导致原本应该处理的是本身这个线程所处理的字符串而反而去去处理了另一个线程的字符串了。
    这就是所谓的线程不安全。
#include<string.h>

static char *_strtok = NULL; //用全局变量缓存上一次的结果

char *
strtok(char *s, char const *ct) {
    char *sbegin, *send;

    sbegin = s ? s : _strtok;
    if (!sbegin) {
        return NULL;
    }
    sbegin += strspn(sbegin, ct);
    if (*sbegin == '\\0') {
        _strtok = NULL;
        return (NULL);
    }
    send = strpbrk(sbegin, ct);
    if (send && *send != '\\0')
        *send++ = '\\0';
    _strtok = send;
    return (sbegin);
}

源代码用的另外几个函数 strspn() 、 strpbrk() 、strcspn()

strspn()

strspn的使用

第二个参数形成一个集合,返回第一个字符串参数中不在集合里的第一个字符下标位置。

/* strspn example */
#include <stdio.h>
#include <string.h>

int main ()
{
  int i;
  char strtext[] = "129th";
  char cset[] = "1234567890";

  i = strspn (strtext,cset);
  printf ("The initial number has %d digits.\\n",i);
  return 0;
}

strspn实现(个人实现版本)

int strspn(const char* s1,const char* s2){
    if(s1==NULL || s2==NULL)//处理特殊情况
        return 0;
    bool cset[256]={0};
    while((*s2)!='\\0'){	//创建set
        cset[*s2] = 1;
        ++s2;
    }
    int idx = 0;
    while(s1[idx] != '\\0'){//得到正确的idx位置
        if(cset[s1[idx]])
        	idx++;
        else
            break;
    }
    return idx;
}

strcspn()

原型:

size_t strcspn(const char *str1, const char *str2)

作用和strspn类似,只不过多了一个c意为constant,连续的。

所以它是找出从str1开始,连续的不在str2字符集合中的长度。

代码实现(个人功能实现)

int strspn(const char* s1,const char* s2){
    if(s1==NULL || s2==NULL)//处理特殊情况
        return 0;
    bool cset[256]={0};
    while((*s2)!='\\0'){	//创建set
        cset[*s2] = 1;
        ++s2;
    }
    int len = 0;
    while(s1[idx] != '\\0'){//得到正确的idx位置
        if(!cset[s1[len]])
        	len++;
        else
            break;
    }
    return len;
}

strpbrk()

strpbrk的使用

同样也是先建立字符的set,返回第一个字符串中在这个集合内的字符的第一个位置的地址(指针)。

/* strpbrk example */
#include <stdio.h>
#include <string.h>

int main ()
{
  char str[] = "This is a sample string";
  char key[] = "aeiou";
  char * pch;
  printf ("Vowels in '%s': ",str);
  pch = strpbrk (str, key);
  while (pch != NULL)
  {
    printf ("%c " , *pch);
    pch = strpbrk (pch+1,key);
  }
  printf ("\\n");
  return 0;
}

strpbrk的实现(个人实现版本_空间节省版)

为了节省空间我这边将每一个位都利用了,大家如果看不懂,可以用256个bool(一个字节)变量之间存。

typedef unsigned char uchar;

inline int get_pos(uchar x) {
    return x % 32;
}

char *strpbrk(const char *s1, const char *s2) {
    if (s1 == NULL || s2 == NULL)
        return NULL;
    uchar cset[32] = {0};                        //用32个unsigned char对每个位进行bool运算可以更节省内存
    while ((*s2) != '\\0') {                      //创建set,%32决定放入哪个盒子中,/32决定放入8位的哪个位
        uchar t = (uchar) *s2++;
        cset[get_pos(t)] |= 1 << (t / 32);       //由于最大值255所以t/32为0~31
    }
    while ((*s1) != '\\0') {
        uchar t = (uchar) *s1;
        if (cset[get_pos(t)] & (1 << (t / 32))) {//bitmap的查找方式
            return (char *) s1;
        } else {
            ++s1;
        }
    }
    return NULL;
}

strtok_r() 源代码实现

函数原型:

char * strtok_r (char *s, const char *delim, char **save_ptr);

由于没有用到static全局变量,所以需要从外面传入一个指针变量用于存储当前操作的字符串。

由于用到的是外部的变量进行缓存,不会产生线程安全问题!

以下为使用测试:

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

void func() {
    char chSrc[] = "Can I help you";//注意不能直接用常量,需要有内存
    char *save = NULL;          //用于缓存
    char *pchSrc = chSrc;
    while (NULL != (pchSrc = strtok_s(pchSrc, " ", &save))) {
        printf("\\npchSrc: %s\\nsave: %s\\n", pchSrc, save);
        pchSrc = NULL;
    }
}

int main() {
    func();
    return 0;
}

strtok_r() 源代码:

char *
strtok_r(char *s, const char *delim, char **save_ptr) {
    char *end;
    if (s == NULL)
        s = *save_ptr;
    if (*s == '\\0') {
        *save_ptr = s;
        return NULL;
    }
    /* Scan leading delimiters.  */
    s += strspn(s, delim);
    if (*s == '\\0') {
        *save_ptr = s;
        return NULL;
    }
    /* Find the end of the token.  */
    end = s + strcspn(s, delim);
    if (*end == '\\0') {
        *save_ptr = end;
        return s;
    }
    /* Terminate the token and make *SAVE_PTR point past it.  */
    *end = '\\0';
    *save_ptr = end + 1;
    return s;
}

(不依赖其他函数库)完全自己实现strtok()

设计方案

设计方案:

  1. 功能:将字符串根据分隔符分割
  2. 原理:全局变量状态实现缓存
  3. 关键:由于缺少strspn()和strpbrk(),根据自己实现了strspn()、strcspn()和strpbrk()后,发现就是建立集合进行判断移动指针的过程。
  • 为了节省空间我这边将每一个位都利用了,大家如果看不懂,可以用256个bool(一个字节)变量之间存。

还是怕有人看不懂,问为什么需要256个位置进行映射,为什么需要uchar?在这里进行解答:因为一个字符为1个字节,所以如果不考虑符号,则最多表示0~255,由于我们是用数组下标对字符的数值进行映射,所以需要为非负数,故转uchar,而我typedef为uchar只是单纯的觉得unsigned char太长了而已🤣

代码实现

typedef unsigned char uchar;

inline int get_pos(uchar x) {
    return x % 32;
}

char *strtok(char *src, const char *delimiters) {
    char *sbegin, *send;
    static char *ssave = NULL;
    sbegin = src ? src : ssave;           //如果src为NULL就继续上一次的缓存
    uchar cset[32] = {0};                 //用32个unsigned char对每个位进行bool运算可以更节省内存
    while ((*delimiters) != '\\0') {       //更新set
        uchar t = (uchar) *delimiters++;
        cset[get_pos(t)] |= 1 << (t / 32);//由于最大值255所以t/32为0~7
    }
    //让sbegin指向不在set中的位置
    while (*sbegin != '\\0' && (cset[get_pos(*sbegin)] & (1 << (((uchar) *sbegin) / 32)))) {
        ++sbegin;
    }
    if (*sbegin == '\\0') {
        ssave = NULL;
        return NULL;
    }
    int idx = 0;
    //相当于strcspn的功能了
    while (sbegin[idx] != '\\0' && !(cset[get_pos(sbegin[idx])] & (1 << ((uchar) sbegin[idx] / 32)))) {
        ++idx;
    }
    send = sbegin + idx;
    if (*send != '\\0')
        *send++ = '\\0';                   //画上终止符
    ssave = send;                         //更新下一次处理的缓存位置
    return sbegin;
}

性能分析

问题规模N为字符串的长度,由于设计strtok采用的是集合存储方式,而不是暴力枚举,所以时间复杂度占优为 O(n) !而用于存储集合元素的数组也通过位运算实现了空间优化,空间不随问题规模改变,所以 O(1) 的空间复杂度。

时间复杂度: O ( N ) O(N) O(N)

空间复杂度: O ( 1 ) O(1) O(1)

测试用例

测试的main函数

测试的说明:以 ' ' ',' , '.' '-' 为分隔符对整个字符串进行分割。

int main() {
    char str[] = "- This 3234d s3242- -d-, a sample string.";
    char *pch;
    printf("Splitting string \\"%s\\" into tokens:\\n", str);
    pch = strtok(str, " ,.-");
    while (pch != NULL) {
        printf("%s\\n", pch);
        pch = strtok(NULL, " ,.-");
    }
    return 0;
}

测试结果:

很好非常perfect!!!

以上是关于C语言源码剖析与实现——strtok()系列函数实现的主要内容,如果未能解决你的问题,请参考以下文章

c语言的split字符串分割(函数strtok)

c语言的split字符串分割(函数strtok)

求C语言标准函数库的源代码

WorldWind源码剖析系列:WorldWind如何确定与视点相关的地形数据的LOD层级与范围

C语言字符串相关函数使用示例 strtok_r strstr strtok atoi

C语言———字符与字符串