strtok
的赞歌
标记解析(Tokenizing
)是最简单也是最常见的解析问题,也就是根据分隔符把一个字符串分割为几个部分。这个定义覆盖了所有这种类型的任务。根据空白分隔符(例如" \t\n\r"
之一)分割单词。假设有个像"/usr/include:/usr/local/include:."
这样的路径,在冒号处将其分开,形成单独的目录。根据一个简单的换行分隔符"\n"
把一个字符串分割为不同的行。可以使用一个配置文件,包含value = key
格式的行,在这种情况下分隔符就是"="
。在数据文件中以逗号分隔的值当然是以逗号为分隔符。我们可以采取两个层次的分割来分别进行处理。例如读取一个完整的配置文件,首先根据换行符进行分割,然后在每行根据=进行分割。
标记解析出现得非常频繁,有一个C标准库函数strtok
(字符串标记化)专门用于完成这个任务。它是那些虽然小巧但能够干净利落地完成任务的函数之一。strtok
的基本工作方式是对我们所输入的字符串进行迭代,直到遇到第1个分隔符,然后用一个'\0'
覆盖这个分隔符。现在这个输入字符串的第1部分已经是个表示第1个标记的合法字符串,strtok
返回一个指向这个子字符串头部的指针供我们使用。这个函数在内部保存源字符串的信息,因此当我们再次调用strtok
时,它可以搜索到下一个标记的尾部,将之设置为‘\0’
,并以一个合法的字符串的形式返回这个标记。每个子字符串的指针都指向原字符串内的位置,因此标记化操作所执行的数据写入是极少的(只是那些\0),并且不会进行复制操作。它
的直接影响是输入字符串被损坏,由于子字符串是指向源字符串的指针,因此无法释放这个输入字符串,必须等到完成了所有子字符串的使用以后,你才能去释放原始的字符串。(或者,可以使用strdup
复制出这些子字符串。)
strtok
函数通过一个静态的内部指针保存我们第1次输入的字符串的剩余部分,意味着它被限制为一次只能标记化一个字符串(通过一组分隔符),无法在涉及线程的情况下使用。因此目前strtok
已经不推荐使用了。推荐使用strtok_r
或strtok_s
,它们是strtok
的线程友好版本。POSIX
标准提供了strtok_r
,C11
标准提供了strtok_s
。这两个函数的用法都有点笨拙,因为第1次调用与后续的调用在形式上是不一样的。
第1次调用函数时,把需要解析的字符串作为它的第1个参数。在后续的调用中,把第1个参数设置为NULL。最后一个参数是处理过的字符串,我们并不需要在第1次使用时对它进行初始化,在后续的调用中,它将保存到目前为止所解析的字符串。
下面是个行计数器(事实上是非空白行的计数器,参见后面的警告)。在脚本语言中,标记解析往往只需要一行程序,但是以下已经是strtok_r
最精简的用法了。注意用于发送源字符串的if ? then:else
只出现在第1次呼叫时传入字符串。
#include <string.h> //strtok_r
int count_lines(char *instring){
int counter = 0;
char *scratch, *txt, *delimiter = "\n";
while ((txt = strtok_r(!counter ? instring : NULL, delimiter, &scratch
)))
counter++;
return counter;
}
C11
标准的strtok_s
函数的工作方式就像strtok_r
一样,但接受一个额外的参数(第2个),它提供了输入字符串的长度,并在后续的调用过程中不断缩短,表示每次调用时剩余字符串的长度。如果输入字符串并不是以\0分隔的,这个额外的参数就非常实用。我们可以用下面的代码重新完成前面那个例子:
#include <string.h> //strtok_s
//first use
size_t len = strlen(instring);
txt = strtok_s(instring, &len, delimiter, &scratch);
//subsequent use:
txt = strtok_s(NULL, &len, delimiter, &scratch);
如果你的系统装安装的有glibc
参照下方的实现
string_utilities.h
#include <string.h>
#define _GNU_SOURCE //asks stdio.h to include asprintf
#include <stdio.h>
//Safe asprintf macro
#define Sasprintf(write_to, ...) { \
char *tmp_string_for_extend = write_to; \
//asprintf每次调用都会新申请一段内存,所以要讲老的指针释放掉
asprintf(&(write_to), __VA_ARGS__); \
free(tmp_string_for_extend); \
}
char *string_from_file(char const *filename);
//定义一个标记数组用于对字符串进行标记
typedef struct ok_array {
//定义成数组指针应该更好一点,但是这里定义成了指向指针的指针
//因为你事先是无法知道要处理字符串的长度和分割之后字符串的多少的
char **elements;
char *base_string;
int length;
} ok_array;
ok_array *ok_array_new(char *instring, char const *delimiters);
void ok_array_free(ok_array *ok_in);
string_utilities.c[
]()
#include <glib.h>
#include <string.h>
#include "string_utilities.h"
#include <stdio.h>
#include <assert.h>
#include <stdlib.h> //abort
char *string_from_file(char const *filename){
char *out;
GError *e=NULL;
/*尽管这种做法并不在所有场合下都是可行的,但我已经醉心于
一次把一个完整的文本文件读取到内存中,这是一个很好的使程序员绕
过心烦问题的例子。如果预计到需要读取到内存的文件过于巨大,可以
使用mmap(q.v.)来实现相同的效果。*/
GIOChannel *f = g_io_channel_new_file(filename, "r", &e);
if (!f) {
fprintf(stderr, "failed to open file '%s'.\n", filename);
return NULL;
}
if (g_io_channel_read_to_end(f, &out, NULL, &e) != G_IO_STATUS_NORMAL){
fprintf(stderr, "found file '%s' but couldn't read it.\n", filename);
return NULL;
}
return out;
}
/* 这是strtok_r的包装器。如果读者是从头看到这里的,应该已经
熟悉这个while循环,它几乎是必需的。这个函数把从strtok_r所返回的
结果记录到一个ok_array结构。*/
ok_array *ok_array_new(char *instring, char const *delimiters){
ok_array *out= malloc(sizeof(ok_array));
*out = (ok_array){.base_string=instring};
char *scratch = NULL;
char *txt = strtok_r(instring, delimiters, &scratch);
if (!txt) return NULL;
while (txt) {
out->elements = realloc(out->elements, sizeof(char*)*++(out->length));
out->elements[out->length-1] = txt;
txt = strtok_r(NULL, delimiters, &scratch);
}
return out;
}
/* Frees the original string, because strtok_r mangled it, so it
isn't useful for any other purpose. */
void ok_array_free(ok_array *ok_in){
if (ok_in == NULL) return;
free(ok_in->base_string);
free(ok_in->elements);
free(ok_in);
}
#ifdef test_ok_array
int main (){
char *delimiters = " `~!@#$%^&*()_-+={[]}|\\;:\",<>./?\n";
ok_array *o = ok_array_new(strdup("Hello, reader. This is text."), delimiters);
assert(o->length==5);
assert(!strcmp(o->elements[1], "reader"));
assert(!strcmp(o->elements[4], "text"));
ok_array_free(o);
printf("OK.\n");
}
#endif
啥 你的linux
系统中没有glibc
库
要是没有可以参考下面这个精简版的对strtok
进行学习。
加入你有一个字符串Hello, reader. This is text.
,需要按照分隔符`` ~!@#$%^&*()_-+={[]}|\;:",<>./?\n`进行分割,分割之后取出被分割的每个字符串。
char **elements;
数组指针中的各个指针指向被分割后的各个字符串的起始位置。
注意这里的分割,只是将分隔符位置的字符替换为\0
,并不是真的把一个长的字符串截断
#include <string.h>
#include "string_utilities.h"
#include <stdio.h>
#include <assert.h>
#include <stdlib.h> //abort
ok_array *ok_array_new(char *instring, char const *delimiters){
ok_array *out= malloc(sizeof(ok_array));
*out = (ok_array){.base_string=instring};
char *scratch = NULL;
char *txt = strtok_r(instring, delimiters, &scratch);
if (!txt) return NULL;
while (txt) {
// 申请一个数组指针,数组指针里面存储的都是指向分隔符替换成'\0'之后的字符的开头
out->elements = realloc(out->elements, sizeof(char*)*++(out->length));
out->elements[out->length-1] = txt;
txt = strtok_r(NULL, delimiters, &scratch);
}
return out;
}
/* Frees the original string, because strtok_r mangled it, so it
isn't useful for any other purpose. */
void ok_array_free(ok_array *ok_in){
if (ok_in == NULL) return;
free(ok_in->base_string);
free(ok_in->elements);
free(ok_in);
}
int main (){
int i = 0;
char *delimiters = " `~!@#$%^&*()_-+={[]}|\\;:\",<>./?\n";
ok_array *o = ok_array_new(strdup("Hello, reader. This is text."), delimiters);
assert(o->length==5);
assert(!strcmp(o->elements[1], "reader"));
assert(!strcmp(o->elements[4], "text"));
for(i = 0; i < o->length; i++)
{
printf("o->length[%d] = [%s]\n", i, o->elements[i]);
}
ok_array_free(o);
printf("OK.\n");
}
C缺陷与陷阱
链接:https://pan.baidu.com/s/1QfTa-p2ZlnazDDA92ry_cw
提取码:mnxb
复制这段内容后打开百度网盘手机App,操作更方便哦