
在 C 语言开发中,字符串处理是高频需求,而字符串分割(将一个完整字符串按指定分隔符拆分为多个子串)更是其中的核心场景 —— 小到命令行参数解析,大到配置文件读取、日志数据清洗,都离不开高效的分割工具。strtok()作为 C 标准库(string.h)中提供的经典分割函数,凭借简洁的接口和高效的实现,成为很多开发者的首选,但它的 “静态变量依赖”“原字符串修改” 等特性也暗藏坑点。
strtok()(全称 “string tokenize”,即 “字符串令牌化”)是 C 标准库(C89 及后续标准)中定义的字符串分割函数,其核心作用是按照指定的 “分隔符集合”,将目标字符串拆分为多个连续的 “子串(令牌 token)”。
核心特性速览:
头文件依赖:
使用strtok()前必须包含标准字符串头文件:
#include <string.h>strtok()的原型非常简洁,但每个参数的用法都有讲究,尤其是第一个参数的NULL传递逻辑,是初学者容易混淆的点。
完整原型:
char *strtok(char *str, const char *delim);参数详解:
参数名 | 类型 | 含义与用法 |
|---|---|---|
str | char * | 目标字符串: 1. 首次调用:传入需要分割的原始字符串(非const,因会被修改); 2. 后续调用:传入NULL,表示 “继续分割上一次未完成的字符串”; 3. 若传入新的非NULL字符串,会重置分割状态,开始分割新字符串。 |
delim | const char * | 分隔符集合:一个字符串,其中每个字符都视为 “合法分隔符”(如","表示逗号是分隔符," ,;"表示空格、逗号、分号都是分隔符)。 |
返回值:
关键逻辑示例:
假设要分割字符串"hello,world!how are you",分隔符为" ,!"(空格、逗号、感叹号),调用流程如下:
要真正理解strtok(),必须搞懂其内部工作机制 —— 核心是 “静态变量保存状态” 和 “分隔符替换为'\0'”。下面通过伪代码还原其核心逻辑。
核心原理拆解:
伪代码实现:
// 静态变量:保存上次分割的结束位置(跨函数调用保留状态)
static char *last_ptr = NULL;
char *strtok(char *str, const char *delim) {
char *current_start; // 当前子串的起始地址
char *delim_pos; // 找到的分隔符位置
// 1. 初始化:首次调用传入str非NULL,重置last_ptr;后续调用用last_ptr
if (str != NULL) {
last_ptr = str;
} else {
// 若str为NULL且last_ptr已到末尾,返回NULL
if (last_ptr == NULL || *last_ptr == '\0') {
return NULL;
}
}
// 2. 跳过当前位置的所有分隔符(处理连续分隔符/开头分隔符)
while (*last_ptr != '\0') {
// 检查当前字符是否在delim中
int is_delim = 0;
delim_pos = (char *)delim;
while (*delim_pos != '\0') {
if (*last_ptr == *delim_pos) {
is_delim = 1;
break;
}
delim_pos++;
}
if (!is_delim) {
break; // 找到非分隔符,停止跳过
}
last_ptr++; // 是分隔符,继续向后跳
}
// 3. 若跳过分隔符后已到字符串末尾,返回NULL
if (*last_ptr == '\0') {
last_ptr = NULL; // 重置状态,避免下次调用出错
return NULL;
}
// 4. 标记当前子串的起始位置,寻找下一个分隔符
current_start = last_ptr;
while (*last_ptr != '\0') {
// 检查当前字符是否是分隔符
delim_pos = (char *)delim;
int is_delim = 0;
while (*delim_pos != '\0') {
if (*last_ptr == *delim_pos) {
is_delim = 1;
break;
}
delim_pos++;
}
if (is_delim) {
// 5. 找到分隔符,替换为'\0',标记子串结束
*last_ptr = '\0';
last_ptr++; // 更新last_ptr到下一个位置
return current_start; // 返回当前子串
}
last_ptr++; // 不是分隔符,继续向后找
}
// 6. 若遍历到字符串末尾(无更多分隔符),返回最后一个子串
return current_start;
}工作流程示意图:

strtok()的设计定位是 “轻量级字符串分割”,因此更适合分隔符固定、无需保留原字符串、单线程的简单场景。以下是几个典型应用场景:
场景 1:命令行参数解析(模拟)
在命令行程序中,用户输入的命令(如"copy src.txt dest.txt -v")以空格为分隔符,可通过strtok()拆分命令、源文件、目标文件、选项等参数。
示例逻辑:
// 模拟命令行输入字符串
char cmd[] = "copy src.txt dest.txt -v";
// 以空格为分隔符拆分
char *token = strtok(cmd, " ");
while (token != NULL) {
if (strcmp(token, "copy") == 0) {
printf("命令类型:复制文件\n");
} else if (strcmp(token, "-v") == 0) {
printf("选项:显示详细日志\n");
} else if (src_path == NULL) {
src_path = token; // 第一个非命令/选项参数:源文件路径
} else {
dest_path = token; // 第二个非命令/选项参数:目标文件路径
}
token = strtok(NULL, " ");
}
// 输出结果:
// 命令类型:复制文件
// 源文件路径:src.txt
// 目标文件路径:dest.txt
// 选项:显示详细日志场景 2:简单配置文件解析
对于格式简单的配置文件(如key=value格式,无嵌套、无引号),可通过strtok()先按行分割,再按 “=” 分割键值对。
示例配置文件内容(config.ini):
max_conn=100
timeout=30
log_path=/var/log/app.log解析逻辑:
FILE *fp = fopen("config.ini", "r");
if (fp == NULL) { perror("fopen"); return -1; }
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
// 去除行尾的换行符(fgets会读取\n)
line[strcspn(line, "\n")] = '\0';
// 按“=”分割key和value
char *key = strtok(line, "=");
char *value = strtok(NULL, "=");
if (key != NULL && value != NULL) {
printf("配置项:%s → %s\n", key, value);
}
}
fclose(fp);
// 输出结果:
// 配置项:max_conn → 100
// 配置项:timeout → 30
// 配置项:log_path → /var/log/app.log场景 3:日志数据清洗(简单分割)
对于无复杂格式的日志(如"2024-05-20 14:30:00 [INFO] user login"),可通过strtok()按空格、中括号等分隔符拆分 “时间、日志级别、内容”。
示例日志行:
2024-05-20 14:30:00 [INFO] user login解析逻辑:
char log[] = "2024-05-20 14:30:00 [INFO] user login";
// 分隔符:空格、[、]
char *token = strtok(log, " []");
int idx = 0;
while (token != NULL) {
switch (idx) {
case 0: printf("日期:%s\n", token); break;
case 1: printf("时间:%s\n", token); break;
case 2: printf("日志级别:%s\n", token); break;
case 3: printf("日志内容:%s ", token); break;
default: printf("%s ", token); break;
}
token = strtok(NULL, " []");
idx++;
}
// 输出结果:
// 日期:2024-05-20
// 时间:14:30:00
// 日志级别:INFO
// 日志内容:user login场景 4:不适合的场景(避坑提醒)
strtok()并非万能,以下场景需避免使用:
strtok()的特性决定了它有很多 “隐性坑”,稍有不慎就会导致程序崩溃或逻辑错误。以下是必须掌握的 6 个注意事项,每个都搭配示例说明:
注意 1:原字符串会被修改,且不能是 const 类型
strtok()会将分隔符替换为'\0',因此必须传入非 const 的可修改字符串(若传入const char *,编译器会报错,运行时可能崩溃);同时,若后续需要原字符串,必须先复制。
错误示例(const 字符串):
// 错误:str是const类型,不可修改
const char str[] = "hello,world";
char *token = strtok(str, ","); // 编译器报错:传递参数 1 时将丢弃指针目标类型的限定符正确示例(复制原字符串):
const char original[] = "hello,world";
// 先复制原字符串到可修改的缓冲区
char str[256];
strcpy(str, original);
// 再分割
char *token = strtok(str, ",");
printf("子串1:%s\n", token); // 输出:hello
printf("原字符串(original):%s\n", original); // 输出:hello,world(未修改)
printf("复制后字符串(str):%s\n", str); // 输出:hello(因第二个字符被改为'\0')注意 2:线程不安全,多线程需用 strtok_r ()
strtok()使用静态变量last_ptr保存状态,若多个线程同时调用strtok()分割不同字符串,会导致last_ptr被交叉修改,出现 “分割混乱”。
线程不安全示例(简化):
#include <pthread.h>
#include <stdio.h>
#include <string.h>
// 线程1:分割字符串"a,b,c"
void *thread1(void *arg) {
char str[] = "a,b,c";
char *token = strtok(str, ",");
while (token != NULL) {
printf("线程1:%s\n", token);
token = strtok(NULL, ",");
// 模拟耗时操作,让线程2有机会介入
sleep(1);
}
return NULL;
}
// 线程2:分割字符串"x;y;z"
void *thread2(void *arg) {
char str[] = "x;y;z";
char *token = strtok(str, ";");
while (token != NULL) {
printf("线程2:%s\n", token);
token = strtok(NULL, ";");
sleep(1);
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, thread1, NULL);
pthread_create(&t2, NULL, thread2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}预期输出:线程 1 输出 a→b→c,线程 2 输出 x→y→z;
实际输出(混乱):
线程1:a 线程2:x 线程1:y // 错误:线程1读取了线程2的last_ptr状态 线程2:z 线程1:NULL // 提前结束
解决方案:用strtok_r()(r表示 “reentrant,可重入”),它通过用户传入的指针保存状态,而非静态变量,线程安全。
注意 3:连续分隔符会被跳过,需确认业务是否允许
strtok()会自动跳过连续的分隔符(如 “a,,b” 分割后得到 “a” 和 “b”,中间的空串被丢弃)。若业务需要保留空串(如 CSV 文件中 “a,,b” 表示三个字段:a、空、b),则strtok()不适用,需用strsep()或自定义逻辑。
示例:
char str[] = "a,,b;c;;d";
char *token = strtok(str, ",;");
while (token != NULL) {
printf("子串:%s\n", token);
token = strtok(NULL, ",;");
}
// 输出结果(跳过空串):
// 子串:a
// 子串:b
// 子串:c
// 子串:d注意 4:首次调用必须传入非 NULL,后续需传 NULL
错误示例(首次传 NULL):
// 错误:首次调用传入NULL,直接返回NULL
char *token = strtok(NULL, ",");
if (token == NULL) {
printf("分割失败:首次调用必须传入非NULL字符串\n");
}中途切换字符串示例:
char str1[] = "a,b,c";
char str2[] = "x,y,z";
// 首次调用:分割str1
char *token = strtok(str1, ",");
printf("str1子串:%s\n", token); // 输出:a
// 中途传入str2(非NULL),重置状态,开始分割str2
token = strtok(str2, ",");
printf("str2子串:%s\n", token); // 输出:x
// 后续传NULL,继续分割str2(而非str1)
token = strtok(NULL, ",");
printf("str2子串:%s\n", token); // 输出:y
// str1的分割已中断,无法继续获取b、c注意 5:分隔符是 “集合”,而非 “固定字符串”
delim参数是 “分隔符字符集合”,而非 “分隔符字符串”—— 例如delim=" ,!"表示 “空格、逗号、感叹号中的任意一个字符都是分隔符”,而非 “空格 + 逗号 + 感叹号” 的组合。
示例:
// 分隔符:空格、逗号、感叹号(任意一个都生效)
char str[] = "hello world,test!demo";
char *token = strtok(str, " ,!");
while (token != NULL) {
printf("子串:%s\n", token);
token = strtok(NULL, " ,!");
}
// 输出结果:
// 子串:hello
// 子串:world
// 子串:test
// 子串:demo注意 6:分割结束后,建议重置 last_ptr(可选)
strtok()的静态变量last_ptr会一直保留状态,若后续再次调用strtok()分割新字符串,需确保首次调用传入非NULL(会自动重置last_ptr);若担心状态残留,可手动将last_ptr置NULL(需注意:静态变量在函数外部不可直接访问,可通过 “传入空字符串 + 任意分隔符” 间接重置)。
重置示例:
// 间接重置last_ptr:传入空字符串,函数会将last_ptr置NULL
strtok("", ",");以下提供 3 个递进式的示例代码,覆盖strtok()的基础用法、线程安全替代方案(strtok_r())、与其他分割函数的对比,可直接编译运行(GCC 环境:gcc -o strtok_demo strtok_demo.c -lpthread)。
示例 1:基础用法 —— 分割带多种分隔符的字符串
#include <stdio.h>
#include <string.h>
int main() {
// 原字符串:带空格、逗号、感叹号、分号
char str[] = "I love C programming, it's fun! Let's learn; together";
// 分隔符集合:空格、, ! ; '(注意单引号也是分隔符)
const char *delim = " ,!;'";
printf("原始字符串:%s\n", str);
printf("分割结果:\n");
// 首次调用:传入str和delim
char *token = strtok(str, delim);
int count = 0; // 统计子串个数
while (token != NULL) {
count++;
printf("子串%d:%s\n", count, token);
// 后续调用:传入NULL
token = strtok(NULL, delim);
}
printf("分割完成,共得到%d个子串\n", count);
printf("修改后的原字符串:%s\n", str); // 原字符串已被修改(第一个分隔符后为'\0')
return 0;
}运行结果:

示例 2:线程安全 —— 用 strtok_r () 替代 strtok ()
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
// 线程1:用strtok_r()分割"a,b,c,d"
void *thread1(void *arg) {
char str[] = "a,b,c,d";
const char *delim = ",";
char *save_ptr; // 用于保存分割状态(替代strtok()的静态变量)
printf("线程1开始分割:%s\n", str);
// 首次调用strtok_r:传入str、delim、&save_ptr
char *token = strtok_r(str, delim, &save_ptr);
while (token != NULL) {
printf("线程1:%s\n", token);
sleep(1); // 模拟耗时操作
// 后续调用:传入NULL、delim、&save_ptr
token = strtok_r(NULL, delim, &save_ptr);
}
printf("线程1分割结束\n");
return NULL;
}
// 线程2:用strtok_r()分割"x;y;z;w"
void *thread2(void *arg) {
char str[] = "x;y;z;w";
const char *delim = ";";
char *save_ptr;
printf("线程2开始分割:%s\n", str);
char *token = strtok_r(str, delim, &save_ptr);
while (token != NULL) {
printf("线程2:%s\n", token);
sleep(1);
token = strtok_r(NULL, delim, &save_ptr);
}
printf("线程2分割结束\n");
return NULL;
}
int main() {
pthread_t t1, t2;
// 创建线程
pthread_create(&t1, NULL, thread1, NULL);
pthread_create(&t2, NULL, thread2, NULL);
// 等待线程结束
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}运行结果(线程安全,无混乱):

示例 3:对比 strtok ()、strtok_r ()、strsep ()
#include <stdio.h>
#include <string.h>
// 对比三个分割函数:处理连续分隔符的差异
int main() {
// 测试字符串:含连续分隔符
char str1[] = "a,,b;c;;d";
char str2[] = "a,,b;c;;d";
char str3[] = "a,,b;c;;d";
const char *delim = ",;";
printf("=== 1. strtok() 分割(跳过连续分隔符) ===\n");
char *t1 = strtok(str1, delim);
while (t1 != NULL) {
printf("%s ", t1);
t1 = strtok(NULL, delim);
}
printf("\n");
printf("=== 2. strtok_r() 分割(跳过连续分隔符,线程安全) ===\n");
char *save_ptr;
char *t2 = strtok_r(str2, delim, &save_ptr);
while (t2 != NULL) {
printf("%s ", t2);
t2 = strtok_r(NULL, delim, &save_ptr);
}
printf("\n");
printf("=== 3. strsep() 分割(保留连续分隔符的空串) ===\n");
char *t3 = str3;
while ((t3 = strsep(&t3, delim)) != NULL) {
// 空串输出"<空>"
if (*t3 == '\0') {
printf("<空> ");
} else {
printf("%s ", t3);
}
}
printf("\n");
return 0;
}运行结果(关键差异:连续分隔符的处理):
=== 1. strtok() 分割(跳过连续分隔符) ===
a b c d
=== 2. strtok_r() 分割(跳过连续分隔符,线程安全) ===
a b c d
=== 3. strsep() 分割(保留连续分隔符的空串) ===
a <空> b c <空> d 注意:strsep()是 BSD 扩展函数(非 C 标准),在 GCC、Clang 中可用,但在 VC 等编译器中可能不支持,移植性较差。
实际开发中,除了strtok(),strtok_r()和strsep()也是常用的分割函数。下表从核心维度对比三者的差异,帮你快速选择合适的工具:
对比维度 | strtok() | strtok_r ()(C99 标准) | strsep ()(BSD 扩展) |
|---|---|---|---|
线程安全性 | 不安全(静态变量) | 安全(用户提供 save_ptr) | 安全(无静态变量) |
原字符串修改 | 是(分隔符替换为 '\0') | 是 | 是 |
连续分隔符处理 | 跳过(丢弃空串) | 跳过(丢弃空串) | 保留(返回空串) |
状态保存方式 | 静态变量(内部维护) | 用户传入的 save_ptr(外部维护) | 传入的字符串指针(外部维护) |
函数原型 | char* strtok(char*, const char*) | char* strtok_r(char*, const char*, char**) | char* strsep(char**, const char*) |
兼容性 | 所有 C 编译器支持(C89+) | 主流编译器支持(C99+) | BSD、Linux 支持,Windows 不支持 |
适用场景 | 单线程、简单分割、无需保留空串 | 多线程、简单分割、无需保留空串 | 单 / 多线程、需保留空串(如 CSV) |
核心差异总结:
strtok()作为 C 语言中的经典字符串分割函数,凭借简洁的接口和高效的实现,在单线程、简单分割场景中仍有广泛应用。但它的 “静态变量依赖”“原字符串修改”“跳过空串” 等特性,也要求开发者必须深入理解其原理,才能避免踩坑。
核心要点回顾:
掌握strtok()的同时,也建议了解strtok_r()、strsep()等替代方案,根据实际场景(线程、空串需求、兼容性)选择最合适的工具,才能写出更健壮、更易维护的代码。
经典面试题
问:strtok () 为什么是线程不安全的?如何解决这个问题?
答:
strtok()线程不安全的核心原因是它使用静态变量保存上次分割的位置(last_ptr)—— 静态变量属于进程全局资源,多线程同时调用strtok()时,会交叉修改last_ptr,导致分割状态混乱(如线程 A 的分割被线程 B 的调用打断,后续线程 A 会读取线程 B 的last_ptr)。
解决方法:
问:使用 strtok () 分割字符串时,原字符串会被修改吗?为什么?如果需要保留原字符串,该怎么做?
答:会修改原字符串。
原因:strtok()的分割逻辑是 “找到分隔符后,将其替换为'\0'(字符串结束符)”,以此标记当前子串的边界,同时通过静态变量记录下一个子串的起始位置 —— 这个替换操作直接修改了原字符串的内存内容,因此原字符串会被破坏。
保留原字符串的解决方案:
在调用strtok()前,先将原字符串复制到一个可修改的缓冲区(如用strcpy()、memcpy()),再对缓冲区调用strtok()进行分割。示例代码:
const char original[] = "hello,world"; // 原字符串(不可修改)
char buf[256];
strcpy(buf, original); // 复制到可修改的缓冲区
char *token = strtok(buf, ","); // 对缓冲区分割,不影响original问:对比 strtok () 和 strsep (),它们在处理连续分隔符时有什么不同?各自适合什么场景?
答:
两者在处理连续分隔符时的核心差异是是否保留空串:
适用场景:
注意:strsep()是 BSD 扩展函数,非 C 标准,移植性(如 Windows)不如strtok()的标准替代strtok_r()。
博主简介 byte轻骑兵,现就职于国内知名科技企业,专注于嵌入式系统研发,深耕 Android、Linux、RTOS、通信协议、AIoT、物联网及 C/C++ 等领域。乐于技术分享与交流,欢迎关注互动! 📌 主页与联系方式
⚠️ 版权声明 本文为原创内容,未经授权禁止转载。商业合作或内容授权请联系邮箱并备注来意。