10、字符串处理:strlen/strcpy/strcat、strcmp/strncmp、strtok分割、sprintf安全替代
字符串处理,说白了就是C语言里最基础也最容易踩坑的地方。我刚开始写嵌入式代码那会儿,觉得字符串嘛,不就是一堆字符排排坐?结果一个缓冲区溢出,直接把整个系统干崩了。从那以后,我对每个字符串函数都多留了个心眼。
今天咱们就把这些函数掰开揉碎了讲。你想想看,一个命令行工具,从解析参数到输出结果,哪一步离得开字符串?
10.1 长度与拷贝:strlen 和 strcpy/strncpy
strlen 这个函数,看起来简单吧?它返回字符串的长度,不包括结尾的 '\0'。但有个坑——如果传入的指针指向的不是一个以 '\0' 结尾的合法字符串,它会一直数下去,直到撞上内存里的某个 '\0',或者直接崩溃。
我个人习惯是:如果字符串长度不变,提前算好存起来。别在 for 循环的条件里写 i < strlen(s),那等于每次循环都重新数一遍。
再说 strcpy。这个函数会把源字符串(包括 '\0')拷贝到目标缓冲区。问题是——它不做边界检查。目标缓冲区多大?源字符串多长?它不管。我曾经在项目里见过一个 bug,strcpy 把日志信息写到了栈上的局部变量里,结果把返回地址给覆盖了。嗯,调试了整整一个下午。
char buf[16];
char *msg = "This is a very long message that will overflow!";
strcpy(buf, msg); // 灾难现场
安全做法是用 strncpy,它多了一个参数:最大拷贝长度。
char buf[16];
strncpy(buf, msg, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0'; // 手动保证结尾
10.2 拼接与比较:strcat 和 strcmp/strncmp
strcat 用于拼接字符串。它会把源字符串追加到目标字符串的末尾。同样的问题——不做边界检查。目标缓冲区剩余空间够不够?它不知道。
我建议你永远别用 strcat,改用 strncat。后者会检查剩余空间,不会越界。
char path[64] = "/home/user/";
char *filename = "very_long_filename_that_might_exceed_buffer.txt";
strncat(path, filename, sizeof(path) - strlen(path) - 1);
比较字符串用 strcmp。它按字典序比较两个字符串,返回 0 表示相等,负数表示第一个小于第二个,正数反之。
但 strcmp 也有隐患——如果两个字符串都不是 '\0' 结尾的,它会一直比下去。所以更安全的做法是用 strncmp,指定最多比较前 n 个字符。
if (strncmp(input, "quit", 4) == 0) {
// 用户输入了 quit
}
10.3 分割字符串:strtok 的正确用法
strtok 是个很有意思的函数。它能把字符串按分隔符拆成一个个 token。比如解析 CSV 文件、命令行参数,都离不开它。
但 strtok 有个大坑:它会修改原字符串,把分隔符替换成 '\0'。而且它是用静态内部缓冲区保存状态的,所以不是线程安全的。如果你在多线程环境里同时调用 strtok,结果会乱套。
char str[] = "apple,banana,orange";
char *token = strtok(str, ",");
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, ",");
}
第一次调用传原字符串,后面传 NULL。这是 strtok 的约定。我个人习惯是:如果只是单线程解析简单字符串,用 strtok 没问题。但如果涉及多线程,或者需要保留原字符串,我会用 strtok_r(可重入版本)。
char str[] = "apple,banana,orange";
char *saveptr;
char *token = strtok_r(str, ",", &saveptr);
while (token != NULL) {
printf("%s\n", token);
token = strtok_r(NULL, ",", &saveptr);
}
10.4 sprintf 的安全替代方案
sprintf 是把格式化的数据写入字符串。但它同样不做边界检查。如果格式化后的字符串太长,缓冲区就炸了。
我在项目中遇到过这样的情况:一个 sprintf 把日志信息拼接到一个固定大小的缓冲区里,结果某天日志内容特别长,直接把堆栈给踩了。从那以后,我再也不敢用 sprintf 了。
安全替代方案有两个:
- snprintf:指定最大写入长度,保证不会越界。
- asprintf:自动分配足够大的缓冲区(GNU 扩展,不是标准 C)。
char buf[64];
snprintf(buf, sizeof(buf), "Value: %d, Name: %s", 42, "hello");
// 如果内容太长,会被截断,但不会溢出
snprintf 的返回值很有意思——它返回的是如果缓冲区足够大,应该写入的字符数(不包括 '\0')。如果返回值 >= 缓冲区大小,说明内容被截断了。你可以利用这个特性来判断是否需要重新分配更大的缓冲区。
10.5 知识体系总览
下面这张图把本章的核心函数和它们的关系梳理了一下。你可以把它当作一个快速参考。
10.6 避坑指南与最佳实践
说了这么多,我总结几条实战经验:
- 永远假设缓冲区不够大。 用 snprintf 代替 sprintf,用 strncpy 代替 strcpy。
- strtok 会改原字符串。 如果你需要保留原数据,先 strdup 一份。
- strlen 不是免费的。 在循环里别反复调用,提前算好存起来。
- 字符串比较用 strncmp。 哪怕你知道两个字符串长度差不多,多一个长度参数就是多一层保护。
- snprintf 的返回值要检查。 如果返回值 >= 缓冲区大小,说明内容被截断了,你可能需要重新处理。
好了,这一章的内容就到这里。字符串处理是 C 语言的基本功,也是命令行工具开发里最常用的操作。把这些函数用熟了,你写出来的代码会稳很多。