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

 

strtok函数的使用是一个老生常谈的问题了。该函数的作用很大,争议也很大。以下的表述可能与一些资料有区别或者说与你原来的认识有差异,因此,我尽量以实验为证。交代一下实验环境是必要的,winxp+vc6.0,一个极端平民化的实验环境。本文中使用的源代码大部分来自于网络,我稍加修改作为例证。当然,本人水平有限,有不妥之处在所难免,各位见谅的同时不妨多做实验,以实验为证。

strtok的函数原型为char *strtok(char *s, char *delim),功能为“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. ” 翻译成汉语就是:作用于字符串s,以包含在delim中的字符为分界符,将s切分成一个个子串;如果,s为空值NULL,则函数保存的指针SAVE_PTR在下一次调用中将作为起始位置。

函数的返回值为从指向被分割的子串的指针。

这个定义和国内一些网站上的说法有一些差别,正是这些差别导致很多人对strtok没有一个正确的认识。希望读者在调用一些函数前,最好能够读一读官方的文档(多半都是英文的),而非看一些以讹传讹的资料。

 

使用strtok需要注意的有以下几点:

 

1.函数的作用是分解字符串,所谓分解,即没有生成新串,只是在s所指向的内容上做了些手脚而已。因此,源字符串s发生了变化!

设源字符串s为 char buffer[INFO_MAX_SZ]=",Fred male 25,John male 62,Anna female 16";  过滤字符串delim为 char *delim = " ",即空格为分界符。

 image

上图的代码会产生这样的结果:

image

首先,buffer发生了变化。如果此时打印buffer的值,会显示“,Fred”,而后面" male 25…16”不翼而飞了。实际上,strtok函数根据delim中的分界符,找到其首次出现的位置,即Fred后面那个空格(buffer[5]),将其修改成了'/0’。其余位置不变。这就很好解释为什么打印buffer的值只能出现“,Fred”,而非buffer中的全部内容了。因此,使用strtok时一定要慎重,以防止源字符串被修改。 

理解了buffer的变化,就很好解释函数的返回值了。返回值buf为分界符之前的子串(其实这个说法并不确切,详见"3”中对于返回值的详细说明)。注意,由变量的地址可知,buf依然指向源字符串。

 image

分界符delim没有发生变化,就不再截图了。

 

2.若要在第一次提取子串完毕之后,继续对源字符串s进行提取,应在其后(第二次,第三次。。。第n次)的调用中将strtok的第一个参数赋为空值NULL。

 image

第一次调用的结果如前文所述,提取出了",Fred”。我们还想继续以空格为分界,提取出后面的"male”等。由上图可以看到,第一次之后的调用我们都给strtok的第一个参数传递了空值NULL(表示函数继续从上一次调用隐式保存的位置,继续分解字符串;对于上述的第二次调用来说,第一次调用结束前用一个this指针指向了分界符的下一位,即'm’所在的位置),这样可依次提取出

image ,image 。。。。以此类推。。。。。

至于为什么要赋空值,要么你就记住结论,要么去查strtok的源代码。本文的最后会有一些介绍。

当然也有部分爱钻牛角尖的人,非不按套路出牌,要看看不赋空值继续赋值为buffer会有什么结果。其实,答案想也能想的到。再一次传递buffer,相当于还从字符串的开头查找分界符delim,而且此时buffer已经被修改(可见的部分只剩下",Fred”),因此,其结果必然是找不到分界符delim。

 

3.关于函数返回值的探讨

由"1”中所述,在提取到子串的情况下,strtok的返回值(假设返回值赋给了指针buf)是提取出的子串的指针。这个指针指向的是子串在源字符串中的起始位置。子串末尾的下一个字符在提取前为分隔符,提取后被修改成了'/0’。因此,若打印buf的值,可以成功的输出子串的内容。

在没有提取到子串的情况下,函数会返回什么值呢?

 image

由上图可以看到buffer中并不包含分界符delim。调用strtok后buf的值为

 image

因为没有找到,源字符串buffer没有发生改变,buf指向源字符串的首地址,打印输出的值为整个字符串的完整值。

什么时候函数的返回值为空值NULL呢?

百度百科上说,“当没有被分割的串时则返回NULL。”这是一个很模棱两可的说法。如果想要确切的了解清楚这个问题,可能需要看一下strtok的实现原理。这里先以实验说明。

 image

第一次调用strtok,毫无疑问,buf指向",Fred”。

第二次调用strtok,由于第一个参数为NULL,表示函数继续以上次调用所保存的this指针的位置开始分解,即对"male 25”分解。分解完毕后,buf指向"male”。

第三次调用strtok,参数继续设定为NULL,此时即对第二次保存的this指针的位置开始分解,即对"25”分解。因为无法找到包含分隔符delim的子串,所以buf指向"25”。

image

第四次调用,参数仍为NULL,此时第三次调用保存的this指针已指向字符串的末尾'/0’,已无法再进行分解。因此函数返回NULL,这也就是百度百科中所提到的“当没有被分割的串时函数返回NULL。”

image

 

4.参数 分隔符delim的探讨(delim是分隔符的集合)

很多人在使用strtok的时候,都想当然的以为函数在分割字符串时完整匹配分隔符delim,比如delim=”ab”,则对于"acdab”这个字符串,函数提取出的是"acd”。至少我在第一次使用的时候也是这么认为的。其实我们都错了,我是在看函数的源代码时才发现这个问题的,且看下面的例子。

image

源字符串为buffer,分隔符delim为 逗号和空格,按照一般的想法我们会以为调用函数后,buf的值为"Fred,male,25”,结果是这样么?

image

第一次调用之后的结果竟然是"Fred”,而非我们所想的结果。这是为什么呢?

我们回到GNU C Library中对strtok的功能定义:“Parse S into tokens separated by characters in DELIM”。也就是说包含在delim中的字符均可以作为分隔符,而非严格匹配。可以把delim理解为分隔符的集合。这一点是非常重要的~

当然,我们在分解字符串的时候,很少使用多个分隔符。这也导致,很多人在写例子的时候只讨论了一个分隔符的情况。有更多的人在看例子的时候也就错误的认识了delim的作用。

 

 

5.待分解的字符串,首字符就为分隔符

首字符为分隔符不能算作一个很特殊的情况。按照常规的分解思路也能正确分解字符串。

我想说明的是,strtok对于这种情况采用了比常规处理更快的方式。

image

如上图例子所示。仅用一次调用就可以得到以逗号分隔的字符串"Fred male 25”,而F前面的','被忽略了。由此可见,strtok在调用的时候忽略了起始位置开始的分隔符。这一点,可以从strtok的源代码得到证实。

 

6.不能向第一个参数传递字符串常量!

本文中所举的例子都将源字符串保存为字符串数组变量。若你将源字符串定义成字符串常量,可想而知,程序会因为strtok函数试图修改源字符串的值,而抛出异常。

image

 

好了,本文详细介绍了使用strtok的注意事项,(二)中我将详细介绍strtok不能实现的一些功能并引出strtok_r函数,最后介绍一下两个函数的实现。

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

 

1.一个应用实例

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

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,先将子串先一一保存到字符串指针数组中,程序末尾打印指针数组中保存的所有子串,验证程序的正确性。得到的程序应该如下:

 

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

 

 

 

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

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

 

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

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

 

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

 

 

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

倘若一定要采用二重循环那种结构提取,有没有合适的函数能够代替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(). The saveptr argument is a pointer to a char * variable that is used internally by strtok_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 of saveptr is ignored. In subsequent calls, str should be NULL, and saveptr 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 saveptrarguments.

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

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

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

 

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

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

 

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

 

调用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的源代码。

 

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



 

代码整体的流程如下:

(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给保存起来,对调用者不可见。其代码如下:

 

[cpp] view plaincopy
 
  1. char *strtok(char *s, const char *delim)  
  2. {  
  3.     static char *last;  
  4.   
  5.     return strtok_r(s, delim, &last);  
  6. }  

 

 

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

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

posted @ 2014-11-17 16:50  周人假的  阅读(17100)  评论(0编辑  收藏  举报