strtok函数和strtok_r函数的区别

2024-06-22 09:58
文章标签 函数 区别 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,先将子串先一一保存到字符串指针数组中,程序末尾打印指针数组中保存的所有子串,验证程序的正确性。得到的程序应该如下:

  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 differentsaveptr arguments.

一系列不同的字符串可能会同时连续调用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. /* 
  2.  * strtok_r.c: 
  3.  * Implementation of strtok_r for systems which don't have it. 
  4.  * 
  5.  * This is taken from the GNU C library and is distributed under the terms of 
  6.  * the LGPL. See copyright notice below. 
  7.  * 
  8.  */  
  9.   
  10. #ifdef HAVE_CONFIG_H   
  11. #include "configuration.h"   
  12. #endif /* HAVE_CONFIG_H */   
  13.   
  14. #ifndef HAVE_STRTOK_R   
  15.   
  16. static const char rcsid[] = "$Id: strtok_r.c,v 1.1 2001/04/24 14:25:34 chris Exp $";  
  17.   
  18. #include <string.h>   
  19.   
  20. #undef strtok_r   
  21.   
  22. /* Parse S into tokens separated by characters in DELIM. 
  23.    If S is NULL, the saved pointer in SAVE_PTR is used as 
  24.    the next starting point.  For example: 
  25.         char s[] = "-abc-=-def"; 
  26.         char *sp; 
  27.         x = strtok_r(s, "-", &sp);      // x = "abc", sp = "=-def" 
  28.         x = strtok_r(NULL, "-=", &sp);  // x = "def", sp = NULL 
  29.         x = strtok_r(NULL, "=", &sp);   // x = NULL 
  30.                 // s = "abc/0-def/0" 
  31. */  
  32. char *strtok_r(char *s, const char *delim, char **save_ptr) {  
  33.     char *token;  
  34.   
  35.     if (s == NULL) s = *save_ptr;  
  36.   
  37.     /* Scan leading delimiters.  */  
  38.     s += strspn(s, delim);  
  39.     if (*s == '/0')   
  40.         return NULL;  
  41.   
  42.     /* Find the end of the token.  */  
  43.     token = s;  
  44.     s = strpbrk(token, delim);  
  45.     if (s == NULL)  
  46.         /* This token finishes the string.  */  
  47.         *save_ptr = strchr(token, '/0');  
  48.     else {  
  49.         /* Terminate the token and make *SAVE_PTR point past it.  */  
  50.         *s = '/0';  
  51.         *save_ptr = s + 1;  
  52.     }  
  53.   
  54.     return token;  
  55. }   

代码整体的流程如下:

(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函数和strtok_r函数的区别的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1083969

相关文章

Vue和React受控组件的区别小结

《Vue和React受控组件的区别小结》本文主要介绍了Vue和React受控组件的区别小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录背景React 的实现vue3 的实现写法一:直接修改事件参数写法二:通过ref引用 DOMVu

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

GO语言中函数命名返回值的使用

《GO语言中函数命名返回值的使用》在Go语言中,函数可以为其返回值指定名称,这被称为命名返回值或命名返回参数,这种特性可以使代码更清晰,特别是在返回多个值时,感兴趣的可以了解一下... 目录基本语法函数命名返回特点代码示例命名特点基本语法func functionName(parameters) (nam

Python Counter 函数使用案例

《PythonCounter函数使用案例》Counter是collections模块中的一个类,专门用于对可迭代对象中的元素进行计数,接下来通过本文给大家介绍PythonCounter函数使用案例... 目录一、Counter函数概述二、基本使用案例(一)列表元素计数(二)字符串字符计数(三)元组计数三、C

Go之errors.New和fmt.Errorf 的区别小结

《Go之errors.New和fmt.Errorf的区别小结》本文主要介绍了Go之errors.New和fmt.Errorf的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考... 目录error的基本用法1. 获取错误信息2. 在条件判断中使用基本区别1.函数签名2.使用场景详细对

Redis中哨兵机制和集群的区别及说明

《Redis中哨兵机制和集群的区别及说明》Redis哨兵通过主从复制实现高可用,适用于中小规模数据;集群采用分布式分片,支持动态扩展,适合大规模数据,哨兵管理简单但扩展性弱,集群性能更强但架构复杂,根... 目录一、架构设计与节点角色1. 哨兵机制(Sentinel)2. 集群(Cluster)二、数据分片

Python中的filter() 函数的工作原理及应用技巧

《Python中的filter()函数的工作原理及应用技巧》Python的filter()函数用于筛选序列元素,返回迭代器,适合函数式编程,相比列表推导式,内存更优,尤其适用于大数据集,结合lamb... 目录前言一、基本概念基本语法二、使用方式1. 使用 lambda 函数2. 使用普通函数3. 使用 N

MySQL中REPLACE函数与语句举例详解

《MySQL中REPLACE函数与语句举例详解》在MySQL中REPLACE函数是一个用于处理字符串的强大工具,它的主要功能是替换字符串中的某些子字符串,:本文主要介绍MySQL中REPLACE函... 目录一、REPLACE()函数语法:参数说明:功能说明:示例:二、REPLACE INTO语句语法:参数

python中update()函数的用法和一些例子

《python中update()函数的用法和一些例子》update()方法是字典对象的方法,用于将一个字典中的键值对更新到另一个字典中,:本文主要介绍python中update()函数的用法和一些... 目录前言用法注意事项示例示例 1: 使用另一个字典来更新示例 2: 使用可迭代对象来更新示例 3: 使用

一文带你迅速搞懂路由器/交换机/光猫三者概念区别

《一文带你迅速搞懂路由器/交换机/光猫三者概念区别》讨论网络设备时,常提及路由器、交换机及光猫等词汇,日常生活、工作中,这些设备至关重要,居家上网、企业内部沟通乃至互联网冲浪皆无法脱离其影响力,本文将... 当谈论网络设备时,我们常常会听到路由器、交换机和光猫这几个名词。它们是构建现代网络基础设施的关键组成