關於函數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]);
}

0_1301297567kvMs.gif


執行的結果是,僅僅提取出了第一個人的信息。看來程序的執行並沒有按照我們的預想。原因是什麼?

原因是:在第一次外循環中,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]);
}

0_13012975748Bli.gif


程序雖然可以達到理想的結果,但不是一個太好解決方案。程序要求你在提取之前必須要知道一個結構體中究竟包含了幾個數據成員。明顯不如雙重循環那樣直觀。

倘若一定要採用二重循環那種結構提取,有沒有合適的函數能夠代替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(). Thesaveptr argument is a pointer to a char * variable that is used internally bystrtok_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 ofsaveptr is ignored. In subsequent calls, str should be NULL, andsaveptr 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 saveptr arguments.

一系列不同的字符串可能會同時連續調用strtok_r進行提取,要爲不同的調用傳遞不同的saveptr參數。

The strtok() function uses a static buffer while parsing, so it's not thread safe. Usestrtok_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]);
}

0_1301297719x8j3.gif

調用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的誤解比較深,網上很少有對於其非常詳細的討論,因此總結一份比較全面的材料,是有必要的;二來這也是自己不斷學習的一個過程,總結會得到遠比兩個函數重要很多的信息。


原文來自

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章