1. 程式人生 > >函式 —— strtok() 例如:Fred male 25,John male 62,Anna female 16兩層迴圈

函式 —— strtok() 例如:Fred male 25,John male 62,Anna female 16兩層迴圈

//char *strtok(char *str, const char *delim)
/*功能:
 * 函式用來將字串分割成一個個片段*/
/*引數:
 * str -- 要被分解成一組小字串的字串。
 * delim -- 包含分隔符的 C 字串。*/
/*返回值:
 * 該函式返回被分解的最後一個子字串,如果沒有可檢索的字串,則返回一個空指標*/

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


int main()
{
   //const char str[80] = "This is - www.w3cschool.cc - website";
   //strtok函式  對引數1 丟棄了 const限定符
   char str[80] = "This is - www.w3cschool.cc - website";
   const char s[2] = "-";
   char *token;


   /* 獲取第一個子字串 */
   token = strtok(str, s);


   /* 繼續獲取其他的子字串 */
   while( token != NULL )
   {
      printf( " %s\n", token );


      token = strtok(NULL, s);
   }


   return(0);
}

問題描述:strtok()函式使用後會對原先的字串進行破壞,如何使得分割後再重新拼接,放回原先的字串定義的變數中

程式碼部分:

#include <string.h>
#include <stdio.h>
int main()
{
	int i = 1;
	char str[80] = "This is - aa bb - cc - dd";
	const char s[2] = "-";
	char cont[80]=  {0};
	char ss[80]=  {0};
	char *token = (strtok(str,s));
	while(token)
	{
		sprintf(cont,"%s",token);
		printf("第%d次分割後,字串為:%s\n", i, token);
		token = strtok(NULL,s);
		strcat(ss,cont);
		i++;
	}
	printf("拼接成的字串:%s\n",ss);
	printf("原變數str中的字串:%s\n",str);
	memset(str,0,strlen(str));
	strcat(str,ss);
	printf("給原先的變數str賦值:%s\n",str);
	return 0;
}

結果顯示:

第1次分割後,字串為:This is 
第2次分割後,字串為: aa bb 
第3次分割後,字串為: cc 
第4次分割後,字串為: dd
拼接成的字串:This is  aa bb  cc  dd
原變數str中的字串:This is 
給原先的變數str賦值:This is  aa bb  cc  dd

***************************對函式(strtok 與 strtok_t)的深入理解*****************************

***********https://www.cnblogs.com/zhouhbing/p/4103916.html****************************

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,先將子串先一一儲存到字串指標陣列中,程式末尾列印指標陣列中儲存的所有子串,驗證程式的正確性。得到的程式應該如下:

  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);

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.     if (s == NULL) s = *save_ptr;  
  14.     /* Scan leading delimiters.  */  
  15.     s += strspn(s, delim);  
  16.     if (*s == '\0')   
  17.         return NULL;  
  18.     /* Find the end of the token.  */  
  19.     token = s;  
  20.     s = strpbrk(token, delim);  
  21.     if (s == NULL)  
  22.         /* This token finishes the string.  */  
  23.         *save_ptr = strchr(token, '\0');  
  24.     else {  
  25.         /* Terminate the token and make *SAVE_PTR point past it.  */  
  26.         *s = '\0';  
  27.         *save_ptr = s + 1;  
  28.     }  
  29.     return token;  
  30. }  

程式碼整體的流程如下:

(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.     return strtok_r(s, delim, &last);  
  5. }  

有了上述兩個函式的實現程式碼,再理解(一)(二)中所講的一些要點也就不困難了。

花那麼多篇幅總結這兩個函式,一來是因為很對人對於strtok的誤解比較深,網上很少有對於其非常詳細的討論,因此總結一份比較全面的材料,是有必要的;二來這也是自己不斷學習的一個過程,總結會得到遠比兩個函式重要很多的資訊。