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 = " ",即空格為分界符。
上圖的代碼會產生這樣的結果:
首先,buffer發生了變化。如果此時打印buffer的值,會顯示“,Fred”,而后面" male 25…16”不翼而飛了。實際上,strtok函數根據delim中的分界符,找到其首次出現的位置,即Fred后面那個空格(buffer[5]),將其修改成了'/0’。其余位置不變。這就很好解釋為什么打印buffer的值只能出現“,Fred”,而非buffer中的全部內容了。因此,使用strtok時一定要慎重,以防止源字符串被修改。
理解了buffer的變化,就很好解釋函數的返回值了。返回值buf為分界符之前的子串(其實這個說法並不確切,詳見"3”中對於返回值的詳細說明)。注意,由變量的地址可知,buf依然指向源字符串。
分界符delim沒有發生變化,就不再截圖了。
2.若要在第一次提取子串完畢之后,繼續對源字符串s進行提取,應在其后(第二次,第三次。。。第n次)的調用中將strtok的第一個參數賦為空值NULL。
第一次調用的結果如前文所述,提取出了",Fred”。我們還想繼續以空格為分界,提取出后面的"male”等。由上圖可以看到,第一次之后的調用我們都給strtok的第一個參數傳遞了空值NULL(表示函數繼續從上一次調用隱式保存的位置,繼續分解字符串;對於上述的第二次調用來說,第一次調用結束前用一個this指針指向了分界符的下一位,即'm’所在的位置),這樣可依次提取出
至於為什么要賦空值,要么你就記住結論,要么去查strtok的源代碼。本文的最后會有一些介紹。
當然也有部分愛鑽牛角尖的人,非不按套路出牌,要看看不賦空值繼續賦值為buffer會有什么結果。其實,答案想也能想的到。再一次傳遞buffer,相當於還從字符串的開頭查找分界符delim,而且此時buffer已經被修改(可見的部分只剩下",Fred”),因此,其結果必然是找不到分界符delim。
3.關於函數返回值的探討
由"1”中所述,在提取到子串的情況下,strtok的返回值(假設返回值賦給了指針buf)是提取出的子串的指針。這個指針指向的是子串在源字符串中的起始位置。子串末尾的下一個字符在提取前為分隔符,提取后被修改成了'/0’。因此,若打印buf的值,可以成功的輸出子串的內容。
在沒有提取到子串的情況下,函數會返回什么值呢?
由上圖可以看到buffer中並不包含分界符delim。調用strtok后buf的值為
因為沒有找到,源字符串buffer沒有發生改變,buf指向源字符串的首地址,打印輸出的值為整個字符串的完整值。
什么時候函數的返回值為空值NULL呢?
百度百科上說,“當沒有被分割的串時則返回NULL。”這是一個很模棱兩可的說法。如果想要確切的了解清楚這個問題,可能需要看一下strtok的實現原理。這里先以實驗說明。
第一次調用strtok,毫無疑問,buf指向",Fred”。
第二次調用strtok,由於第一個參數為NULL,表示函數繼續以上次調用所保存的this指針的位置開始分解,即對"male 25”分解。分解完畢后,buf指向"male”。
第三次調用strtok,參數繼續設定為NULL,此時即對第二次保存的this指針的位置開始分解,即對"25”分解。因為無法找到包含分隔符delim的子串,所以buf指向"25”。
第四次調用,參數仍為NULL,此時第三次調用保存的this指針已指向字符串的末尾'/0’,已無法再進行分解。因此函數返回NULL,這也就是百度百科中所提到的“當沒有被分割的串時函數返回NULL。”
4.參數 分隔符delim的探討(delim是分隔符的集合)
很多人在使用strtok的時候,都想當然的以為函數在分割字符串時完整匹配分隔符delim,比如delim=”ab”,則對於"acdab”這個字符串,函數提取出的是"acd”。至少我在第一次使用的時候也是這么認為的。其實我們都錯了,我是在看函數的源代碼時才發現這個問題的,且看下面的例子。
源字符串為buffer,分隔符delim為 逗號和空格,按照一般的想法我們會以為調用函數后,buf的值為"Fred,male,25”,結果是這樣么?
第一次調用之后的結果竟然是"Fred”,而非我們所想的結果。這是為什么呢?
我們回到GNU C Library中對strtok的功能定義:“Parse S into tokens separated by characters in DELIM”。也就是說包含在delim中的字符均可以作為分隔符,而非嚴格匹配。可以把delim理解為分隔符的集合。這一點是非常重要的~
當然,我們在分解字符串的時候,很少使用多個分隔符。這也導致,很多人在寫例子的時候只討論了一個分隔符的情況。有更多的人在看例子的時候也就錯誤的認識了delim的作用。
5.待分解的字符串,首字符就為分隔符
首字符為分隔符不能算作一個很特殊的情況。按照常規的分解思路也能正確分解字符串。
我想說明的是,strtok對於這種情況采用了比常規處理更快的方式。
如上圖例子所示。僅用一次調用就可以得到以逗號分隔的字符串"Fred male 25”,而F前面的','被忽略了。由此可見,strtok在調用的時候忽略了起始位置開始的分隔符。這一點,可以從strtok的源代碼得到證實。
6.不能向第一個參數傳遞字符串常量!
本文中所舉的例子都將源字符串保存為字符串數組變量。若你將源字符串定義成字符串常量,可想而知,程序會因為strtok函數試圖修改源字符串的值,而拋出異常。
好了,本文詳細介紹了使用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,先將子串先一一保存到字符串指針數組中,程序末尾打印指針數組中保存的所有子串,驗證程序的正確性。得到的程序應該如下:
- 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並不能通過兩層循環的辦法,解決提取多人信息的問題。有沒有其他辦法呢? 顯然,是有其他途徑的。
我給出了一種解決辦法。同時以 ',’ (逗號) 和 ' ’(空格) 為分界符,一層循環解決問題。
- 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(). 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,以雙重循環的形式提取出每個人的信息。
- 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的源代碼。
- /* 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給保存起來,對調用者不可見。其代碼如下:
- char *strtok(char *s, const char *delim)
- {
- static char *last;
- return strtok_r(s, delim, &last);
- }
有了上述兩個函數的實現代碼,再理解(一)(二)中所講的一些要點也就不困難了。
花那么多篇幅總結這兩個函數,一來是因為很對人對於strtok的誤解比較深,網上很少有對於其非常詳細的討論,因此總結一份比較全面的材料,是有必要的;二來這也是自己不斷學習的一個過程,總結會得到遠比兩個函數重要很多的信息。