第17章:字符串中替换指定字符/子串:手写 replace 函数
字符串替换,这活儿看着简单,对吧?
但说实话,我见过太多人在这个坑里摔得鼻青脸肿。标准库的 str_replace 确实好用,可一旦你需要在嵌入式环境、或者自己造轮子的时候,手写一个 replace 函数就成了基本功。
今天我就带你把这个函数从头到尾捋一遍。嗯,咱们不搞花架子,直接上干货。
17.1 替换单个字符:最简单的版本
先来个热身。替换单个字符,比如把字符串里所有的 'a' 换成 'b'。
这个逻辑很直白:遍历字符串,找到目标字符就改写。
#include <stdio.h>
#include <string.h>
void replace_char(char *str, char old_ch, char new_ch) {
if (str == NULL) return; // 我习惯先做空指针检查
while (*str) {
if (*str == old_ch) {
*str = new_ch;
}
str++;
}
}
int main() {
char text[] = "hello world, hello c";
replace_char(text, 'l', 'x');
printf("%s\n", text); // 输出:hexxo worxd, hexxo c
return 0;
}
这里有个细节:我用了 char text[] 而不是 char *text。为什么?因为字符串字面量是只读的,你直接改它会崩溃。这个坑我踩过,真的。
17.2 替换子串:一次替换一个
替换字符简单,替换子串就有点意思了。
假设我们要把 "abc" 替换成 "xyz"。核心思路是:
- 找到子串出现的位置(用
strstr或者手写匹配) - 把后面的内容挪动,腾出空间
- 写入新子串
我直接给你看代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 替换第一个匹配的子串
char* replace_substr_first(const char *src, const char *old_sub, const char *new_sub) {
if (!src || !old_sub || !new_sub) return NULL;
const char *pos = strstr(src, old_sub);
if (!pos) {
// 没找到,直接返回原串的拷贝
return strdup(src);
}
size_t src_len = strlen(src);
size_t old_len = strlen(old_sub);
size_t new_len = strlen(new_sub);
// 计算新串长度
size_t result_len = src_len - old_len + new_len;
char *result = (char*)malloc(result_len + 1);
if (!result) return NULL;
// 拷贝匹配位置之前的部分
size_t prefix_len = pos - src;
memcpy(result, src, prefix_len);
// 拷贝新子串
memcpy(result + prefix_len, new_sub, new_len);
// 拷贝匹配位置之后的部分
memcpy(result + prefix_len + new_len, pos + old_len, src_len - prefix_len - old_len + 1);
return result;
}
int main() {
const char *s = "I love C language, C is powerful.";
char *res = replace_substr_first(s, "C", "C++");
if (res) {
printf("%s\n", res); // I love C++ language, C is powerful.
free(res);
}
return 0;
}
你看,这里我用了 memcpy 而不是 strcpy。为什么?因为 memcpy 可以指定长度,更精确,不会因为遇到 '\0' 就停。我个人习惯在字符串处理中,只要涉及长度已知的拷贝,一律用 memcpy。
17.3 替换所有匹配的子串
一次替换一个不够过瘾?那就来替换全部。
思路其实一样,只是需要循环查找。每次替换完,从新串的末尾继续找。
char* replace_substr_all(const char *src, const char *old_sub, const char *new_sub) {
if (!src || !old_sub || !new_sub) return NULL;
size_t old_len = strlen(old_sub);
size_t new_len = strlen(new_sub);
// 先计算替换次数,方便分配内存
int count = 0;
const char *p = src;
while ((p = strstr(p, old_sub)) != NULL) {
count++;
p += old_len;
}
if (count == 0) return strdup(src);
// 分配足够大的缓冲区
size_t result_len = strlen(src) + count * (new_len - old_len);
char *result = (char*)malloc(result_len + 1);
if (!result) return NULL;
char *write_ptr = result;
const char *read_ptr = src;
const char *found;
while ((found = strstr(read_ptr, old_sub)) != NULL) {
// 拷贝匹配前的内容
size_t chunk_len = found - read_ptr;
memcpy(write_ptr, read_ptr, chunk_len);
write_ptr += chunk_len;
// 拷贝新子串
memcpy(write_ptr, new_sub, new_len);
write_ptr += new_len;
// 跳过旧子串
read_ptr = found + old_len;
}
// 拷贝剩余部分
strcpy(write_ptr, read_ptr);
return result;
}
realloc。我在项目中处理大文本时,这个优化能省下不少时间。
17.4 原地替换 vs 新串返回
你可能会问:能不能直接在原字符串上改?
答案是:分情况。
- 新子串和旧子串长度相等:可以原地替换,直接覆盖就行。
- 新子串比旧子串短:也可以原地替换,但后面要处理多余的空位。
- 新子串比旧子串长:原地替换会覆盖后面的内容,必须用新缓冲区。
我一般建议:统一用新缓冲区返回。这样接口清晰,调用方自己管理内存,不容易出 bug。
但如果你在嵌入式环境,内存紧张,那就得用原地替换。我给你一个原地替换的版本(仅限新子串 <= 旧子串长度):
void replace_substr_inplace(char *str, const char *old_sub, const char *new_sub) {
if (!str || !old_sub || !new_sub) return;
size_t old_len = strlen(old_sub);
size_t new_len = strlen(new_sub);
if (new_len > old_len) {
fprintf(stderr, "Error: new substring longer than old, cannot replace in place.\n");
return;
}
char *write_ptr = str;
char *read_ptr = str;
char *found;
while ((found = strstr(read_ptr, old_sub)) != NULL) {
// 拷贝匹配前的内容
size_t chunk_len = found - read_ptr;
memmove(write_ptr, read_ptr, chunk_len);
write_ptr += chunk_len;
// 拷贝新子串
memcpy(write_ptr, new_sub, new_len);
write_ptr += new_len;
// 跳过旧子串
read_ptr = found + old_len;
}
// 拷贝剩余部分
strcpy(write_ptr, read_ptr);
}
注意这里我用了 memmove 而不是 memcpy。因为源和目标可能有重叠,memmove 能正确处理重叠区域。这个细节,我曾经在项目里吃过亏——用 memcpy 导致数据错乱,查了半天才发现。
17.5 知识结构图
下面这张图帮你理清本章的核心逻辑:
17.6 避坑指南
我这些年写字符串替换,踩过的坑不少。给你列几个最典型的:
调用方可能传 NULL 进来,你的函数必须能优雅地处理,而不是直接崩溃。
用
malloc 分配了新串,调用方用完必须 free。我建议在注释里明确写明「返回值需要调用方释放」。
原地替换时,源和目标区域可能重叠。记住:
memmove 比 memcpy 更安全。
原地替换后,一定要在正确位置写
'\0',否则打印时会带出乱码。
17.7 性能优化小贴士
如果你要处理超长字符串(比如几 MB 的日志),有几个优化点:
- 预分配缓冲区:先统计替换次数,一次性分配足够内存,避免多次
realloc。 - 使用
memchr定位字符:如果只替换单个字符,memchr比逐字节遍历更快。 - 避免重复计算长度:把
strlen的结果缓存起来,不要在循环里反复调用。
我记得有一次,一个日志处理模块要替换几万行文本中的时间戳格式。最初版本用 strstr 逐行处理,跑了 8 秒。后来改成预分配 + memcpy 批量拷贝,直接降到 0.3 秒。你看,优化前后差了 20 多倍。
17.8 总结
手写 replace 函数,说白了就是三件事:查找、拷贝、挪动。
单个字符替换最简单,子串替换要小心长度变化,原地替换要留意内存重叠。把这些细节都照顾到了,你的 replace 函数就能在各种场景下稳定工作。
嗯,今天就聊到这儿。代码你拿回去跑一跑,改一改,试试替换不同长度的子串,看看边界情况怎么处理。动手才是最好的学习方式。