第18章:字符串中插入子串——手写 insert 函数

字符串插入,说白了就是在某个位置“塞”一段新内容进去。

比如你有一个字符串 "HelloWorld",想在索引 5 的位置插入一个空格,变成 "Hello World"。这个操作看起来简单,但自己动手写的时候,坑还真不少。

我记得刚入行那会儿,有个项目需要动态拼接 SQL 语句。用户输入条件后,要在 WHERE 子句的特定位置插入字段名。我图省事直接用了 sprintf 拼接,结果缓冲区溢出,线上挂了两次。从那以后,我所有字符串操作都自己写,至少心里有底。

18.1 插入操作的核心逻辑

插入子串,本质上分三步:

  1. 把原字符串从插入点开始的部分往后“挪”出足够空间
  2. 把要插入的子串复制到空出来的位置
  3. 把原字符串后半段接回去

你想想看,这不就是数组元素的插入吗?只不过操作对象是字符数组。

核心公式:

新字符串长度 = 原字符串长度 + 插入子串长度

移动字节数 = 原字符串从插入点开始的剩余长度(包括 '\0')

18.2 手写 insert 函数——基础版

先来个最直接的实现。假设目标缓冲区足够大,我们不检查溢出。

#include <stdio.h>
#include <string.h>

// 在 str 的 pos 位置插入子串 substr
// 假设 buf 空间足够大
void str_insert(char *str, int pos, const char *substr) {
    int len_str = strlen(str);
    int len_sub = strlen(substr);
    int i;

    // 1. 从后往前移动字符,腾出位置
    //    注意要包括 '\0'
    for (i = len_str; i >= pos; i--) {
        str[i + len_sub] = str[i];
    }

    // 2. 插入子串
    for (i = 0; i < len_sub; i++) {
        str[pos + i] = substr[i];
    }
}

这段代码能工作吗?能。但问题很多。

第一个问题:pos 合法性没检查。如果 pos 大于原字符串长度,或者小于 0,程序直接崩。

第二个问题:缓冲区溢出。调用者必须保证 str 指向的空间足够容纳新字符串。这在很多场景下是致命伤。

第三个问题:如果 substrstr 指向同一块内存(自插入),结果会怎样?

嗯,这里要注意。自插入时,substr 指向的内容在移动过程中会被覆盖,导致插入内容错乱。我踩过这个坑,调试了半小时才发现。

18.3 安全版 insert 函数

咱们来写一个更健壮的版本。加入边界检查,支持自插入,并且返回错误码。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// 返回值:0 成功,-1 参数错误,-2 缓冲区不足
int str_insert_safe(char *str, size_t buf_size, int pos, const char *substr) {
    size_t len_str = strlen(str);
    size_t len_sub = strlen(substr);
    size_t new_len = len_str + len_sub;

    // 参数检查
    if (str == NULL || substr == NULL) return -1;
    if (pos < 0 || (size_t)pos > len_str) return -1;
    if (new_len >= buf_size) return -2;  // 留一个字节给 '\0'

    // 处理自插入:先拷贝子串到临时缓冲区
    char *temp = NULL;
    if (substr >= str && substr < str + buf_size) {
        temp = (char *)malloc(len_sub + 1);
        if (temp == NULL) return -1;
        strcpy(temp, substr);
        substr = temp;
    }

    // 从后往前移动
    size_t i;
    for (i = len_str; i >= (size_t)pos; i--) {
        str[i + len_sub] = str[i];
    }

    // 插入子串
    for (i = 0; i < len_sub; i++) {
        str[pos + i] = substr[i];
    }

    if (temp) free(temp);
    return 0;
}

这个版本做了几件事:

  • 检查 pos 是否在合法范围(0 到 len_str)
  • 检查缓冲区是否够用
  • 检测自插入,提前拷贝子串到临时内存
  • 返回错误码,让调用者决定怎么处理

我个人习惯在工程代码里用这种带返回值的函数。出了问题能快速定位,而不是在某个角落悄悄崩溃。

18.4 插入操作的性能考量

字符串插入的时间复杂度是 O(n),因为要移动后半段字符。如果频繁在字符串头部插入,性能会很差。

举个例子:

char buf[100] = "World";
str_insert_safe(buf, sizeof(buf), 0, "Hello ");  // 每次都要移动所有字符

如果循环 1000 次在头部插入,时间复杂度就是 O(n²)。

我在做日志系统时遇到过这个问题。日志前缀要不断往消息头部加时间戳,每次插入都移动大量数据。后来我改成从尾部构建字符串,最后再整体反转,性能提升了一个数量级。

优化建议:

  • 频繁头部插入 → 考虑用链表或从尾部构建
  • 频繁中间插入 → 考虑用间隙缓冲区(gap buffer)
  • 大量小插入 → 先收集再一次性插入

18.5 插入操作的边界情况

写 insert 函数时,边界情况是 bug 的重灾区。我列几个常见的:

场景 预期行为 常见错误
pos = 0 在头部插入 忘记移动 '\0'
pos = len_str 在尾部追加 越界访问
插入空字符串 不做任何操作 移动 0 字节导致死循环
缓冲区刚好够 成功插入,最后一位是 '\0' 忘记留 '\0' 位置
自插入(重叠内存) 正确插入 源数据被覆盖

我曾经在嵌入式项目里写过一个 insert 函数,没处理 pos = len_str 的情况。结果在尾部插入时,把 '\0' 覆盖了,后续所有字符串操作都异常。查了整整一个下午。

18.6 知识体系图

下面这张图总结了字符串插入的核心要点:

字符串插入核心知识体系 str_insert 实现步骤 ① 从后往前移动字符 ② 复制子串到空位 ③ 保证 '\0' 正确结尾 安全要点 ✓ 边界检查 (pos范围) ✓ 缓冲区溢出检查 ✓ 自插入处理 性能优化 头部插入 → 从尾部构建 中间插入 → 间隙缓冲区 批量插入 → 合并操作 核心:移动 → 复制 → 收尾,安全第一

18.7 避坑指南

最后,我把自己这些年踩过的坑总结一下:

常见陷阱:

  • 忘记移动 '\0':移动字符时要从后往前,包括结尾的 '\0'。否则字符串会变成“无底洞”。
  • pos 类型用 intstrlen 返回 size_t,用 int 比较会有符号扩展问题。我建议统一用 size_t
  • 自插入不处理:如果 substr 指向 str 内部的某个位置,移动时会覆盖源数据。必须提前拷贝。
  • 缓冲区大小计算错误:新长度 = len_str + len_sub,但还要加 1 给 '\0'。很多人漏掉这个 1。
  • 插入点等于字符串长度时:此时相当于追加,移动循环条件要小心,别越界。

嗯,差不多就这些。字符串插入看起来简单,但写一个健壮的版本,需要考虑的细节真不少。我建议你在自己的代码库里保留一个经过充分测试的 insert 函数,别每次都临时写。

毕竟,生产环境不相信眼泪,只相信经过考验的代码。


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