1.7. 分割字符串
很多文件格式或协议格式中会规定一些分隔符或者叫界定符(Delimiter),例如/etc/passwd文件中保存着系统的帐号信息:$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
...
每条记录占一行,也就是说记录之间的分隔符是换行符,每条记录又由若干个字段组成,这些字段包括用户名、密码、用户id、组id、个人信息、主目录、登录Shell,字段之间的分隔符是:号。解析这样的字符串需要根据分隔符把字符串分割成几段,C标准库提供的strtok函数可以很方便地完成分割字符串的操作。tok是Token的缩写,分割出来的每一段字符串称为一个Token。#include char *strtok(char *str, const char *delim);
char *strtok_r(char *str, const char *delim, char **saveptr);
返回值:返回指向下一个Token的指针,如果没有下一个Token了就返回NULL
参数str是待分割的字符串,delim是分隔符,可以指定一个或多个分隔符,strtok遇到其中任何一个分隔符就会分割字符串。看下面的例子。
例 25.2. strtok
#include #include int main(void)
{
char str[] = "root:x::0:root:/root:/bin/bash:";
char *token;
token = strtok(str, ":");
printf("%s\n", token);
while ( (token = strtok(NULL, ":")) != NULL)
printf("%s\n", token);
return 0;
}$ ./a.out
root
x
0
root
/root
/bin/bash
结合这个例子,strtok的行为可以这样理解:冒号是分隔符,把"root:x::0:root:/root:/bin/bash:"这个字符串分隔成"root"、"x"、""、"0"、"root"、"/root"、"/bin/bash"、""等几个Token,但空字符串的Token被忽略。第一次调用要把字符串首地址传给strtok的第一个参数,以后每次调用第一个参数只要传NULL就可以了,strtok函数自己会记住上次处理到字符串的什么位置(显然这是通过strtok函数中的一个静态指针变量记住的)。
用gdb跟踪这个程序,会发现str字符串被strtok不断修改,每次调用strtok把str中的一个分隔符改成'\0',分割出一个小字符串,并返回这个小字符串的首地址。(gdb) start
Breakpoint 1 at 0x8048415: file main.c, line 5.
Starting program: /home/akaedu/a.out
main () at main.c:5
5{
(gdb) n
6char str[] = "root:x::0:root:/root:/bin/bash:";
(gdb)
9token = strtok(str, ":");
(gdb) display str
1: str = "root:x::0:root:/root:/bin/bash:"
(gdb) n
10printf("%s\n", token);
1: str = "root\000x::0:root:/root:/bin/bash:"
(gdb)
root
11while ( (token = strtok(NULL, ":")) != NULL)
1: str = "root\000x::0:root:/root:/bin/bash:"
(gdb)
12printf("%s\n", token);
1: str = "root\000x\000:0:root:/root:/bin/bash:"
(gdb)
x
11while ( (token = strtok(NULL, ":")) != NULL)
1: str = "root\000x\000:0:root:/root:/bin/bash:"
刚才提到在strtok函数中应该有一个静态指针变量记住上次处理到字符串中的什么位置,所以不需要每次调用时都把字符串中的当前处理位置传给strtok,但是在函数中使用静态变量是不好的,以后会讲到这样的函数是不可重入的。strtok_r函数则不存在这个问题,它的内部没有静态变量,调用者需要自己分配一个指针变量来维护字符串中的当前处理位置,每次调用时把这个指针变量的地址传给strtok_r的第三个参数,告诉strtok_r从哪里开始处理,strtok_r返回时再把新的处理位置写回到这个指针变量中(这是一个Value-result参数)。strtok_r末尾的r就表示可重入(Reentrant),这个函数不属于C标准库,是在POSIX标准中定义的。关于strtok_r的用法Man Page上有一个很好的例子:
例 25.3. strtok_r
#include #include #include int main(int argc, char *argv[])
{
char *str1, *str2, *token, *subtoken;
char *saveptr1, *saveptr2;
int j;
if (argc != 4) {
fprintf(stderr, "Usage: %s string delim subdelim\n",
argv[0]);
exit(EXIT_FAILURE);
}
for (j = 1, str1 = argv[1]; ; j++, str1 = NULL) {
token = strtok_r(str1, argv[2], &saveptr1);
if (token == NULL)
break;
printf("%d: %s\n", j, token);
for (str2 = token; ; str2 = NULL) {
subtoken = strtok_r(str2, argv[3], &saveptr2);
if (subtoken == NULL)
break;
printf(" --> %s\n", subtoken);
}
}
exit(EXIT_SUCCESS);
}$ ./a.out 'a/bbb///cc;xxx:yyy:' ':;' '/'
1: a/bbb///cc
--> a
--> bbb
--> cc
2: xxx
--> xxx
3: yyy
--> yyy
a/bbb///cc;xxx:yyy:这个字符串有两级分隔符,一级分隔符是:号或;号,把这个字符串分割成a/bbb///cc、xxx、yyy三个子串,二级分隔符是/,只有第一个子串中有二级分隔符,它被进一步分割成a、bbb、cc三个子串。由于strtok_r不使用静态变量,而是要求调用者自己保存字符串的当前处理位置,所以这个例子可以在按一级分隔符分割整个字符串的过程中穿插着用二级分隔符分割其中的每个子串。建议读者用gdb的display命令跟踪argv[1]、saveptr1和saveptr2,以理解strtok_r函数的工作方式。
Man Page的BUGS部分指出了用strtok和strtok_r函数需要注意的问题:
这两个函数要改写字符串以达到分割的效果
这两个函数不能用于常量字符串,因为试图改写.rodata段会产生段错误
在做了分割之后,字符串中的分隔符就被'\0'覆盖了
strtok函数使用了静态变量,它不是线程安全的,必要时应该用可重入的strtok_r函数,以后再详细介绍“可重入”和“线程安全”这两个概念
习题
1、出于练习的目的,strtok和strtok_r函数非常值得自己动手实现一遍,在这个过程中不仅可以更深刻地理解这两个函数的工作原理,也为以后理解“可重入”和“线程安全”这两个重要概念打下基础。
2、解析URL中的路径和查询字符串。动态网页的URL末尾通常带有查询,例如:
比如上面第一个例子,是路径部分,?号后面的complete=1&hl=zh-CN&ie=GB2312&q=linux&meta=是查询字符串,由五个“key=value”形式的键值对(Key-value Pair)组成,以&隔开,有些键对应的值可能是空字符串,比如这个例子中的键meta。
现在要求实现一个函数,传入一个带查询字符串的URL,首先检查输入格式的合法性,然后对URL进行切分,将路径部分和各键值对分别传出,请仔细设计函数接口以便传出这些字符串。如果函数中有动态分配内存的操作,还要另外实现一个释放内存的函数。完成之后,为自己设计的函数写一个Man Page。