本文轉載自:http://astute11.blog.51cto.com/4404646/1334199
(一)中已經介紹了使用strtok函數的一些注意事項,本篇將介紹strtok的一個應用並引出strtok_r函數。
1.一個應用實例
網絡上一個比較經典的例子是將字符串切分,存入結構體中。如,現有結構體
1
2
3
4
5
|
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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並不能通過兩層循環的辦法,解決提取多人信息的問題。有沒有其他辦法呢? 顯然,是有其他途徑的。
我給出了一種解決辦法。同時以 ',’ (逗號) 和 ' ’(空格) 為分界符,一層循環解決問題。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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(). 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 ofsaveptris 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,以雙重循環的形式提取出每個人的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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的源代碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
/* 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給保存起來,對調用者不可見。其代碼如下:
1
2
3
4
5
|
char *strtok(char *s,
const
char *delim)
{
static
char *last;
return
strtok_r(s, delim, &last);
}
|
有了上述兩個函數的實現代碼,再理解(一)(二)中所講的一些要點也就不困難了。
花那么多篇幅總結這兩個函數,一來是因為很多人對於strtok的誤解比較深,網上很少有對於其非常詳細的討論,因此總結一份比較全面的材料,是有必要的;二來這也是自己不斷學習的一個過程,總結會得到遠比兩個函數重要很多的信息。