第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"。核心思路是:

  1. 找到子串出现的位置(用 strstr 或者手写匹配)
  2. 把后面的内容挪动,腾出空间
  3. 写入新子串

我直接给你看代码:

#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 知识结构图

下面这张图帮你理清本章的核心逻辑:

手写 replace 函数核心逻辑 字符串替换 替换单个字符 替换子串 原地 vs 新串 遍历 + 直接赋值 注意字符串常量 strstr 查找 + memcpy 拷贝 先统计次数再分配 新子串长 → 新缓冲区 新子串短 → 可原地替换

17.6 避坑指南

我这些年写字符串替换,踩过的坑不少。给你列几个最典型的:

坑1:忘记处理空指针
调用方可能传 NULL 进来,你的函数必须能优雅地处理,而不是直接崩溃。
坑2:内存泄漏
malloc 分配了新串,调用方用完必须 free。我建议在注释里明确写明「返回值需要调用方释放」。
坑3:重叠内存
原地替换时,源和目标区域可能重叠。记住:memmovememcpy 更安全。
坑4:替换后字符串变短,末尾残留旧数据
原地替换后,一定要在正确位置写 '\0',否则打印时会带出乱码。

17.7 性能优化小贴士

如果你要处理超长字符串(比如几 MB 的日志),有几个优化点:

  • 预分配缓冲区:先统计替换次数,一次性分配足够内存,避免多次 realloc
  • 使用 memchr 定位字符:如果只替换单个字符,memchr 比逐字节遍历更快。
  • 避免重复计算长度:把 strlen 的结果缓存起来,不要在循环里反复调用。

我记得有一次,一个日志处理模块要替换几万行文本中的时间戳格式。最初版本用 strstr 逐行处理,跑了 8 秒。后来改成预分配 + memcpy 批量拷贝,直接降到 0.3 秒。你看,优化前后差了 20 多倍。

17.8 总结

手写 replace 函数,说白了就是三件事:查找、拷贝、挪动

单个字符替换最简单,子串替换要小心长度变化,原地替换要留意内存重叠。把这些细节都照顾到了,你的 replace 函数就能在各种场景下稳定工作。

嗯,今天就聊到这儿。代码你拿回去跑一跑,改一改,试试替换不同长度的子串,看看边界情况怎么处理。动手才是最好的学习方式。


公众号:蓝海资料掘金营,微信deep3321