关于函数strtok和strtok_r的使用要点和实现原理(二)
http://www.cnblogs.com/stemon/p/4013264.html已经介绍了使用strtok函数的一些注意事项,本篇将介绍strtok的一个应用并引出strtok_r函数。
1.一个应用实例
网络上一个比较经典的例子是将字符串切分,存入结构体中。如,现有结构体
1 typedef struct person{ 2 char name[25]; 3 char sex[10]; 4 char age[4]; 5 }Person;
需从字符串 char buffer[INFO_MAX_SZ]="Fred male 25,John male 62,Anna female 16"; 中提取出人名、性别以及年龄。
一种可行的思路是设置两层循环。外循环,先以 ',’ (逗号) 为分界符,将三个人的信息分开,然后对于每一个子串,再以 ' ’(空格) 为分界符分别得到人名、性别和年龄。
按照这个思路,理应能够实现所要的功能。为了简化步骤,我们调用strtok,先将子串先一一保存到字符串指针数组中,程序末尾打印指针数组中保存的所有子串,验证程序的正确性。得到的程序应该如下:
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并不能通过两层循环的办法,解决提取多人信息的问题。有没有其他办法呢? 显然,是有其他途径的。
我给出了一种解决办法。同时以 ',’ (逗号) 和 ' ’(空格) 为分界符,一层循环解决问题。
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,以双重循环的形式提取出每个人的信息。
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的源代码。
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给保存起来,对调用者不可见。其代码如下:
1 char *strtok(char *s, const char *delim) 2 { 3 static char *last; 4 5 return strtok_r(s, delim, &last); 6 }
有了上述两个函数的实现代码,再理解(一)(二)中所讲的一些要点也就不困难了。
花那么多篇幅总结这两个函数,一来是因为很对人对于strtok的误解比较深,网上很少有对于其非常详细的讨论,因此总结一份比较全面的材料,是有必要的;二来这也是自己不断学习的一个过程,总结会得到远比两个函数重要很多的信息。