本文转载自:http://astute11.blog.51cto.com/4404646/1334199

(一)中已经介绍了使用strtok函数的一些注意事项,本篇将介绍strtok的一个应用并引出strtok_r函数。

1.一个应用实例

网络上一个比较经典的例子是将字符串切分,存入结构体中。如,现有结构体

1
2
3
4
5
typedef struct person{
    char name[25];
    char sex[10];
    char age[4];
}Person;

需从字符串 char buffer[INFO_MAX_SZ]="Fred male 25,John male 62,Anna female 16"; 中提取出人名、性别以及年龄。

一种可行的思路是设置两层循环。外循环,先以 ',’ (逗号) 为分界符,将三个人的信息分开,然后对于每一个子串,再以 ' ’(空格) 为分界符分别得到人名、性别和年龄。

按照这个思路,理应能够实现所要的功能。为了简化步骤,我们调用strtok,先将子串先一一保存到字符串指针数组中,程序末尾打印指针数组中保存的所有子串,验证程序的正确性。得到的程序应该如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int in=0;
char buffer[INFO_MAX_SZ]="Fred male 25,John male 62,Anna female 16";  
char *p[20];
char *buf = buffer;
while((p[in]=strtok(buf,","))!=NULL)
{
    buf=p[in];
    while((p[in]=strtok(buf," "))!=NULL)
    {
        in++;
        buf=NULL;
    }
    buf=NULL;
}
printf("Here we have %d strings/n"in);
for (int j=0; j<in; j++)
    printf(">%s</n",p[j]);
}

执行的结果是,仅仅提取出了第一个人的信息。看来程序的执行并没有按照我们的预想。原因是什么?

原因是:在第一次外循环中,strtok将"Fred male 25,"后的这个逗号,改为了'\0’,这时strtok内部的this指针指向的是逗号的后一个字符'J’经过第一次的内循环,分别提取出了“Fred” “male” “25”。提取完"25”之后,函数内部的this指针被修改指向了"25”后面的'\0’内循环结束后(内循环实际执行了4次),开始第二次的外循环,由于函数第一个参数被设定为NULL,strtok将以this指针指向的位置作为分解起始位置。很遗憾,此时this指针指向的是'\0’,strtok对一个空串无法切分,返回NULL。外循环结束。所以,我们只得到了如图所示的第一个人的信息。

看来使用strtok并不能通过两层循环的办法,解决提取多人信息的问题。有没有其他办法呢? 显然,是有其他途径的。

我给出了一种解决办法。同时以 ',’ (逗号) 和 ' ’(空格) 为分界符,一层循环解决问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
in 0;
while ((p[in] = strtok(buf, " ,")) != NULL)
{
    switch (in 3)
    {
    case 0:
        printf("第%d个人:Name!/n"in/3+1);
        break;
    case 1:
        printf("第%d个人:Sex!/n"in/3+1);
        break;
    case 2:
        printf("第%d个人:Age!/n"in/3+1);
        break;
    }
    in++;
    buf = NULL;
}
printf("Here we have %d strings/n"in);
for (int j=0; j<in; j++)
    printf(">%s</n",p[j]);
}

程序虽然可以达到理想的结果,但不是一个太好解决方案。程序要求你在提取之前必须要知道一个结构体中究竟包含了几个数据成员。明显不如双重循环那样直观。

倘若一定要采用二重循环那种结构提取,有没有合适的函数能够代替strtok呢? 有的,它就是strtok_r。

2.strtok_r及其使用

strtok_r是linux平台下的strtok函数的线程安全版。windows的string.h中并不包含它。要想使用这个函数,上网搜其linux下的实现源码,复制到你的程序中即可。别的方式应该也有,比如使用GNU C Library。我下载了GNU C Library,在其源代码中找到了strtok_r的实现代码,复制过来。可以看作是第一种方法和第二种方法的结合。

strtok的函数原型为 char *strtok_r(char *str, const char *delim, char **saveptr);

下面对strtok的英文说明摘自http://www.linuxhowtos.org/manpages/3/strtok_r.htm,译文是由我给出的。

The strtok_r() function is a reentrant version strtok(). Thesaveptr argument is a pointer to a char *variable that is used internally bystrtok_r() in order to maintain context between successive calls that parse the same string.

strtok_r函数是strtok函数的可重入版本。char **saveptr参数是一个指向char *的指针变量,用来在strtok_r内部保存切分时的上下文,以应对连续调用分解相同源字符串。

On the first call to strtok_r(), str should point to the string to be parsed, and the value ofsaveptris ignored. In subsequent calls, str should be NULL, andsaveptr should be unchanged since the previous call.

第一次调用strtok_r时,str参数必须指向待提取的字符串,saveptr参数的值可以忽略。连续调用时,str赋值为NULL,saveptr为上次调用后返回的值,不要修改。

Different strings may be parsed concurrently using sequences of calls to strtok_r() that specify different saveptr arguments.

一系列不同的字符串可能会同时连续调用strtok_r进行提取,要为不同的调用传递不同的saveptr参数。

The strtok() function uses a static buffer while parsing, so it's not thread safe. Usestrtok_r() if this matters to you.

strtok函数在提取字符串时使用了静态缓冲区,因此,它是线程不安全的。如果要顾及到线程的安全性,应该使用strtok_r。

strtok_r实际上就是将strtok内部隐式保存的this指针,以参数的形式与函数外部进行交互。由调用者进行传递、保存甚至是修改。需要调用者在连续切分相同源字符串时,除了将str参数赋值为NULL,还要传递上次切分时保存下的saveptr。

举个例子,还记得前文提到的提取结构体的例子么?我们可以使用strtok_r,以双重循环的形式提取出每个人的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int in=0;
char buffer[INFO_MAX_SZ]="Fred male 25,John male 62,Anna female 16";
char *p[20];
char *buf=buffer;
char *outer_ptr=NULL;
char *inner_ptr=NULL;
while((p[in] = strtok_r(buf, ",", &outer_ptr))!=NULL)
{
    buf=p[in];
    while((p[in]=strtok_r(buf, " ", &inner_ptr))!=NULL)
    {
        in++;
        buf=NULL;
    }
    buf=NULL;
}
printf("Here we have %d strings/n",in);
for (int j=0; j<in; j++)
    printf(">%s</n",p[j]);
}

调用strtok_r的代码比调用strtok的代码多了两个指针,outer_ptr和inner_ptr。outer_ptr用于标记每个人的提取位置,即外循环;inner_ptr用于标记每个人内部每项信息的提取位置,即内循环。具体过程如下:

(1)第1次外循环,outer_ptr忽略,对整个源串提取,提取出"Fred male 25",分隔符',' 被修改为了'\0’,outer_ptr返回指向'J’。

(2)第一次内循环,inner_ptr忽略对第1次外循环的提取结果"Fred male 25"进行提取,提取出了"Fred",分隔符' '被修改为了'\0',inner_ptr返回指向'm'。

(3)第二次内循环,传递第一次内循环返回的inner_ptr,第一个参数为NULL,从inner_ptr指向的位置'm'开始提取,提取出了"male",分隔符  ' '被修改为了'\0',inner_ptr返回指向'2'。

(4)第三次内循环,传递第二次内循环返回的inner_ptr,第一个参数为NULL,从inner_ptr指向的位置'2'开始提取,提取出了"25",因为没有找到' ',inner_ptr返回指向25后的'\0'。

(5)第四次内循环,传递第三次内循环返回的inner_ptr,第一个参数为NULL,因为inner_ptr指向的位置为'\0',无法提取,返回空值。结束内循环。

(6)第2次外循环,传递第1次外循环返回的outer_ptr,第一个参数为NULL,从outer_ptr指向的位置'J'开始提取,提取出"John male 62",分隔符',’被修改为了'\0’,outer_ptr返回指向'A’。(调用strtok则卡死在了这一步

……以此类推,外循环一次提取一个人的全部信息,内循环从外循环的提取结果中,二次提取个人单项信息。

可以看到strtok_r将原内部指针显示化,提供了saveptr这个参数。增加了函数的灵活性和安全性。

3.strtok和strtok_r的源代码

这两个函数的实现,有众多的版本。我strtok_r来自于GNU C Library,strtok则调用了strtok_r。因此先给出strtok_r的源代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* Parse S into tokens separated by characters in DELIM.
   If S is NULL, the saved pointer in SAVE_PTR is used as
   the next starting point.  For example:
        char s[] = "-abc-=-def";
        char *sp;
        x = strtok_r(s, "-", &sp);      // x = "abc", sp = "=-def"
        x = strtok_r(NULL, "-=", &sp);  // x = "def", sp = NULL
        x = strtok_r(NULL, "=", &sp);   // x = NULL
                // s = "abc\0-def\0"
*/
char *strtok_r(char *s, const char *delim, char **save_ptr) {
    char *token;
    if (s == NULL) s = *save_ptr;
    /* Scan leading delimiters.  */
    s += strspn(s, delim);
    if (*s == '\0')
        return NULL;
    /* Find the end of the token.  */
    token = s;
    s = strpbrk(token, delim);
    if (s == NULL)
        /* This token finishes the string.  */
        *save_ptr = strchr(token, '\0');
    else {
        /* Terminate the token and make *SAVE_PTR point past it.  */
        *s = '\0';
        *save_ptr = s + 1;
    }
    return token;
}

代码整体的流程如下:

(1)判断参数s是否为NULL,如果是NULL就以传递进来的save_ptr作为起始分解位置;若不是NULL,则以s开始切分。

(2)跳过待分解字符串开始的所有分界符。

(3)判断当前待分解的位置是否为'\0',若是则返回NULL(联系到(一)中所说对返回值为NULL的解释);不是则继续。

(4)保存当前的待分解串的指针token,调用strpbrk在token中找分界符:如果找不到,则将save_ptr赋值为待分解串尾部'\0'所在的位置,token没有发生变化;若找的到则将分界符所在位置赋值为'\0',token相当于被截断了(提取出来),save_ptr指向分界符的下一位。

(5)函数的最后(无论找到还是没找到)都将返回。

对于函数strtok来说,可以理解为用一个内部的静态变量将strtok_r中的save_ptr给保存起来,对调用者不可见。其代码如下:

1
2
3
4
5
char *strtok(char *s, const char *delim)
{
    static char *last;
    return strtok_r(s, delim, &last);
}

有了上述两个函数的实现代码,再理解(一)(二)中所讲的一些要点也就不困难了。

花那么多篇幅总结这两个函数,一来是因为很多人对于strtok的误解比较深,网上很少有对于其非常详细的讨论,因此总结一份比较全面的材料,是有必要的;二来这也是自己不断学习的一个过程,总结会得到远比两个函数重要很多的信息。

关于函数strtok和strtok_r的使用要点和实现原理(二)【转】的更多相关文章

  1. 关于函数strtok和strtok_r的使用要点和实现原理(二)

    http://www.cnblogs.com/stemon/p/4013264.html已经介绍了使用strtok函数的一些注意事项,本篇将介绍strtok的一个应用并引出strtok_r函数. 1. ...

  2. 关于函数strtok和strtok_r的使用要点和实现原理

    strtok函数的使用是一个老生常谈的问题了.该函数的作用很大,争议也很大.以下的表述可能与一些资料有区别或者说与你原来的认识有差异,因此,我尽量以实验为证.交代一下实验环境是必要的,winxp+vc ...

  3. 关于函数strtok和strtok_r的使用要点和实现原理(一)【转】

    本文转载自:http://astute11.blog.51cto.com/4404646/1334198 strtok函数的使用是一个老生常谈的问题了.该函数的作用很大,争议也很大.以下的表述可能与一 ...

  4. [转]关于strtok和strtok_r函数的深度研究

    在linux环境下,字符串分割的函数中,大家比较常用的是strtok函数,这个函数用处很大,但也有一些问题,以下将深度挖掘一下这个函数的用法,原理,实现,其次,该函数是不可再入函数,但是在linux ...

  5. 函数strtok

    char* strtok(char *str, const char*delim) char *strtok_r(char *str, const char *delim, char **savept ...

  6. 字符串分割函数 STRTOK & STRTOK_R (转)

    1.一个应用实例 网络上一个比较经典的例子是将字符串切分,存入结构体中.如,现有结构体 typedef struct person{     char name[25];     char sex[1 ...

  7. Java生鲜电商平台-SpringCloud微服务架构中核心要点和实现原理

    Java生鲜电商平台-SpringCloud微服务架构中核心要点和实现原理 说明:Java生鲜电商平台中,我们将进一步理解微服务架构的核心要点和实现原理,为读者的实践提供微服务的设计模式,以期让微服务 ...

  8. (转载)关于Linux C函数strtok的使用要点

    今天遇到了处理字符串的问题,比如分割问题,但是一时间想不起来什么方法,也不想手写一个类似java String中的split函数,于是百度了一下,发现了strtok这个好用的方法,以此作为总结. st ...

  9. strtok和strtok_r

    1.strtok()函数的用法 函数原型:char *strtok(char *s, const char *delim); Function:分解字符串为一组字符串.s为要分解的字符串,delim为 ...

随机推荐

  1. Docker源码分析(五):Docker Server的创建

    1.Docker Server简介 Docker架构中,Docker Server是Docker Daemon的重要组成部分.Docker Server最主要的功能是:接受用户通过Docker Cli ...

  2. BluePrint和ORM

    一.蓝图创建 #引入库文件 from flask import Blueprint,request,jsonify user = Blueprint( "site", __name ...

  3. 【BZOJ3280】小R的烦恼 最小费用最大流

    [BZOJ3280]小R的烦恼 Description 小R最近遇上了大麻烦,他的程序设计挂科了.于是他只好找程设老师求情.善良的程设老师答应不挂他,但是要求小R帮助他一起解决一个难题. 问题是这样的 ...

  4. 【BZOJ3551】[ONTAK2010]Peaks加强版 最小生成树+DFS序+主席树

    [BZOJ3545][ONTAK2010]Peaks Description 在Bytemountains有N座山峰,每座山峰有他的高度h_i.有些山峰之间有双向道路相连,共M条路径,每条路径有一个困 ...

  5. 不同linux下两网卡绑定方法

    记得原来在做性能测试时,为了提高网络吞吐率.必须将两个网卡绑定一起工作.绑定方法如下: 一.CentOS 配置   1.编辑虚拟网络接口配置文件,指定网卡IP: # vi /etc/sysconfig ...

  6. Android自动读取短信验证码

    Android自动读取短信验证码  extends:http://www.cnblogs.com/jiayaguang/p/4366384.html,http://blog.csdn.net/yung ...

  7. 项目中启动另外的一个app

    NSMutableString *webViewContent = [[NSMutableStringalloc] init]; [webViewContent appendString:@" ...

  8. 微信小程序 --- 动态获取input的value

    这里对 input 使用的是 bindinput 方法: <input type="text" bindinput="input"> <but ...

  9. Iterator 和 Iterable 区别和联系

    首先预览下Java源码中的Iterator和Iterable: Iterable接口: public interface Iterable<T> {//这里只摘录接口中的抽象方法 /** ...

  10. mongodb基础应用

    一些概念 一个mongod服务可以有建立多个数据库,每个数据库可以有多张表,这里的表名叫collection,每个collection可以存放多个文档(document),每个文档都以BSON(bin ...