C语言中的字符串处理函数:strstr与strtok
目录
一、strstr:字符串查找的 "定位器"
1. 函数原型与参数详解
2. 完整实战示例(含边界场景)
输出结果
3. 原理深度剖析
4. 关键注意事项(避坑指南)
二、strtok:字符串分割的 "手术刀"
1. 函数原型与参数详解
2. 完整实战示例(含核心场景)
输出结果
3. 原理深度剖析
核心缺陷:线程不安全
4. 关键注意事项(避坑指南)
三、strstr 与 strtok 实战组合示例
输出结果
四、总结:核心要点与选型建议
1. strstr 核心要点
2. strtok 核心要点
3. 实战选型建议
在 C 语言开发中,字符串处理是高频场景 —— 从日志解析、数据清洗到网络协议解析,都离不开「查找子字符串」和「分割字符串」的需求。strstr(字符串查找)和strtok(字符串分割)是<string.h>库中最核心的两个工具函数,本文将从用法、原理、实战、避坑四个层面,彻底讲透这两个函数的使用技巧。
一、strstr:字符串查找的 "定位器"
strstr的核心作用是在主字符串中查找子字符串的首次出现位置,是实现 “关键词检索”“内容匹配” 的基础工具。
1. 函数原型与参数详解
#include <string.h> // 从haystack中查找needle的首次出现 char *strstr(const char *haystack, const char *needle);表格
| 参数 | 含义 |
|---|---|
haystack | 被查找的主字符串(const 修饰,函数不会修改该字符串) |
needle | 要查找的子字符串(const 修饰) |
| 返回值 | 找到则返回指向首次匹配位置的指针;未找到返回NULL;needle 为空返回 haystack |
2. 完整实战示例(含边界场景)
#include <stdio.h> #include <string.h> int main() { // 场景1:正常查找(存在子字符串) const char *haystack1 = "Hello, world! This is a test string."; const char *needle1 = "world"; char *result1 = strstr(haystack1, needle1); if (result1) { printf("场景1:找到子字符串,位置(偏移量):%ld\n", result1 - haystack1); printf(" 匹配后的字符串:%s\n", result1); // 输出从匹配位置到结尾的内容 } else { printf("场景1:未找到子字符串\n"); } // 场景2:子字符串不存在 const char *needle2 = "java"; char *result2 = strstr(haystack1, needle2); if (result2) { printf("场景2:找到子字符串\n"); } else { printf("场景2:未找到子字符串\n"); } // 场景3:子字符串为空(特殊边界) const char *needle3 = ""; char *result3 = strstr(haystack1, needle3); if (result3) { printf("场景3:空字符串匹配结果:%s\n", result3); // 返回haystack本身 } // 场景4:子字符串是主字符串的后缀 const char *haystack4 = "test123"; const char *needle4 = "123"; char *result4 = strstr(haystack4, needle4); if (result4) { printf("场景4:后缀匹配位置:%ld\n", result4 - haystack4); } return 0; }输出结果
场景1:找到子字符串,位置(偏移量):7 匹配后的字符串:world! This is a test string. 场景2:未找到子字符串 场景3:空字符串匹配结果:Hello, world! This is a test string. 场景4:后缀匹配位置:43. 原理深度剖析
strstr的底层实现通常基于朴素字符串匹配算法(暴力匹配),核心逻辑如下:
graph TD A[初始化:i=0(haystack指针),j=0(needle指针)] --> B{haystack[i] == needle[j]?} B -- 是 --> C[i++, j++] C --> D{j == needle长度?} D -- 是 --> E[返回haystack+i-j(匹配起始位置)] D -- 否 --> B B -- 否 --> F[i = i-j+1,j=0] F --> G{i < haystack长度?} G -- 是 --> B G -- 否 --> H[返回NULL]- 时间复杂度:最坏情况O(m×n)(m 为主串长度,n 为子串长度),例如主串
"aaaaaab"、子串"aaab"; - 优化说明:部分编译器(如 GCC)会对
strstr做优化 —— 短子串用暴力匹配,长子串自动切换为 KMP 算法(时间复杂度O(m+n)),兼顾简单性和效率。
4. 关键注意事项(避坑指南)
- 大小写敏感:
strstr("Hello", "hello")会返回NULL,如需忽略大小写,需手动实现:// 简易版不区分大小写的strstr char *strstr_nocase(const char *haystack, const char *needle) { if (*needle == '\0') return (char*)haystack; for (; *haystack; haystack++) { if (tolower(*haystack) == tolower(*needle)) { const char *h = haystack; const char *n = needle; while (*h && *n && tolower(*h) == tolower(*n)) { h++; n++; } if (*n == '\0') return (char*)haystack; } } return NULL; } - 空指针风险:传入
NULL会导致程序崩溃,建议封装时增加校验:char *safe_strstr(const char *haystack, const char *needle) { if (haystack == NULL || needle == NULL) return NULL; return strstr(haystack, needle); } - 匹配空字符串:C 标准规定
needle为空时返回haystack,需注意此边界逻辑。
二、strtok:字符串分割的 "手术刀"
strtok的核心作用是按指定分隔符拆分字符串,是处理 CSV、日志、命令行参数等场景的必备工具。
1. 函数原型与参数详解
#include <string.h> // 分割字符串:str为待分割字符串,delim为分隔符集合 char *strtok(char *str, const char *delim);| 参数 | 含义 |
|---|---|
str | 首次调用传入待分割字符串;后续调用传入NULL(表示继续分割上次的字符串) |
delim | 分隔符集合(如",;:"表示逗号、分号、冒号都是分隔符) |
| 返回值 | 指向当前分割出的子字符串的指针;无更多子串返回NULL |
2. 完整实战示例(含核心场景)
#include <stdio.h> #include <string.h> #include <stdlib.h> // 用于malloc/free int main() { // 场景1:基础分割(多分隔符) char str1[] = "apple,banana;cherry:date"; const char *delim1 = ",;:"; printf("场景1:基础分割\n"); char *token1 = strtok(str1, delim1); while (token1 != NULL) { printf(" Token: %s\n", token1); token1 = strtok(NULL, delim1); } // 场景2:保留原字符串(strtok会修改原字符串,需先复制) char *original = "北京|上海|广州|深圳"; char *str2 = (char*)malloc(strlen(original) + 1); // 分配内存 strcpy(str2, original); // 复制原字符串 const char *delim2 = "|"; printf("\n场景2:保留原字符串\n"); printf(" 原字符串:%s\n", original); char *token2 = strtok(str2, delim2); while (token2 != NULL) { printf(" Token: %s\n", token2); token2 = strtok(NULL, delim2); } free(str2); // 释放内存 // 场景3:连续分隔符(自动合并) char str3[] = "a,,b;;c::d"; const char *delim3 = ",;:"; printf("\n场景3:连续分隔符\n"); char *token3 = strtok(str3, delim3); while (token3 != NULL) { printf(" Token: %s\n", token3); token3 = strtok(NULL, delim3); } return 0; }输出结果
场景1:基础分割 Token: apple Token: banana Token: cherry Token: date 场景2:保留原字符串 原字符串:北京|上海|广州|深圳 Token: 北京 Token: 上海 Token: 广州 Token: 深圳 场景3:连续分隔符 Token: a Token: b Token: c Token: d3. 原理深度剖析
strtok的核心实现依赖静态变量和字符串修改,步骤如下:
- 首次调用(str≠NULL):
- 跳过字符串开头的分隔符(如
",,a,b"会跳过前两个逗号); - 记录当前分割起始位置,继续向后查找分隔符;
- 找到分隔符后,将其替换为
'\0'(截断字符串); - 返回分割起始位置的指针,并将静态变量更新为分隔符的下一个位置。
- 跳过字符串开头的分隔符(如
- 后续调用(str=NULL):
- 从静态变量记录的位置开始,重复上述步骤;
- 直到遍历到字符串末尾,返回
NULL。
核心缺陷:线程不安全
由于静态变量被所有线程共享,多线程同时调用strtok会导致分割位置错乱。解决方案:
- POSIX 系统使用
strtok_r(可重入版,手动传入上下文指针); - Windows 系统使用
strtok_s(安全版)。
示例(strtok_r用法):
#include <stdio.h> #include <string.h> int main() { char str[] = "a:b:c:d"; const char *delim = ":"; char *saveptr; // 上下文指针,替代静态变量 char *token = strtok_r(str, delim, &saveptr); while (token != NULL) { printf("Token: %s\n", token); token = strtok_r(NULL, delim, &saveptr); } return 0; }4. 关键注意事项(避坑指南)
- 修改原字符串:
strtok会将分隔符替换为'\0',若需保留原字符串,必须先复制(如场景 2); - 连续分隔符处理:
strtok会自动跳过连续的分隔符(如",,a"分割后仅得到"a"),无需额外处理; - 只读字符串不可分割:直接传入字符串常量(如
strtok("a,b,c", ","))会触发崩溃(常量不可修改),需先复制到可写数组; - 多线程禁用:优先使用
strtok_r/strtok_s替代,避免线程安全问题; - 分隔符是 "集合":
delim是分隔符的集合,而非固定字符串(如delim="ab"表示'a'和'b'都是分隔符)。
三、strstr 与 strtok 实战组合示例
实际开发中,常需先查找关键词,再分割内容,例如解析日志:
#include <stdio.h> #include <string.h> int main() { // 日志格式:[2026-03-16] user=zhangsan;age=25;addr=Beijing char log[] = "[2026-03-16] user=zhangsan;age=25;addr=Beijing"; const char *keyword = "user="; // 步骤1:用strstr找到user=的位置 char *user_pos = strstr(log, keyword); if (user_pos == NULL) { printf("未找到用户信息\n"); return 1; } user_pos += strlen(keyword); // 跳过"user=",指向实际值 // 步骤2:用strtok分割后续内容(按;分割) char *user = strtok(user_pos, ";"); char *age = strtok(NULL, ";"); char *addr = strtok(NULL, ";"); printf("解析结果:\n"); printf(" 用户:%s\n", user); printf(" 年龄:%s\n", age); printf(" 地址:%s\n", addr); return 0; }输出结果
解析结果: 用户:zhangsan 年龄:age=25 地址:addr=Beijing四、总结:核心要点与选型建议
1. strstr 核心要点
- 功能:查找子字符串首次出现位置,返回指针(可计算偏移量);
- 特性:大小写敏感、空字符串匹配返回主串、底层多为暴力匹配(长子串优化为 KMP);
- 避坑:校验空指针、处理大小写需求、注意边界匹配。
2. strtok 核心要点
- 功能:按分隔符集合分割字符串,返回子串指针;
- 特性:修改原字符串、跳过连续分隔符、依赖静态变量(线程不安全);
- 避坑:复制原字符串再分割、多线程用
strtok_r/strtok_s、不可分割只读字符串。
3. 实战选型建议
- 字符串查找:简单场景用
strstr,忽略大小写需自定义strstr_nocase; - 字符串分割:单线程用
strtok,多线程 / 高并发用strtok_r/strtok_s; - 复杂场景:组合使用
strstr(定位)+strtok(分割),提升处理效率。
掌握strstr和strtok的核心逻辑与避坑技巧,能大幅提升 C 语言字符串处理的效率和代码健壮性。在实际项目中,需结合场景选择合适的函数,并做好边界校验和线程安全处理,才能编写出工业级的字符串处理代码。
