第18章:字符串中插入子串——手写 insert 函数
字符串插入,说白了就是在某个位置“塞”一段新内容进去。
比如你有一个字符串 "HelloWorld",想在索引 5 的位置插入一个空格,变成 "Hello World"。这个操作看起来简单,但自己动手写的时候,坑还真不少。
我记得刚入行那会儿,有个项目需要动态拼接 SQL 语句。用户输入条件后,要在 WHERE 子句的特定位置插入字段名。我图省事直接用了 sprintf 拼接,结果缓冲区溢出,线上挂了两次。从那以后,我所有字符串操作都自己写,至少心里有底。
18.1 插入操作的核心逻辑
插入子串,本质上分三步:
- 把原字符串从插入点开始的部分往后“挪”出足够空间
- 把要插入的子串复制到空出来的位置
- 把原字符串后半段接回去
你想想看,这不就是数组元素的插入吗?只不过操作对象是字符数组。
核心公式:
新字符串长度 = 原字符串长度 + 插入子串长度
移动字节数 = 原字符串从插入点开始的剩余长度(包括 '\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 指向的空间足够容纳新字符串。这在很多场景下是致命伤。
第三个问题:如果 substr 和 str 指向同一块内存(自插入),结果会怎样?
嗯,这里要注意。自插入时,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 知识体系图
下面这张图总结了字符串插入的核心要点:
18.7 避坑指南
最后,我把自己这些年踩过的坑总结一下:
常见陷阱:
- 忘记移动 '\0':移动字符时要从后往前,包括结尾的 '\0'。否则字符串会变成“无底洞”。
- pos 类型用 int:
strlen返回size_t,用 int 比较会有符号扩展问题。我建议统一用size_t。 - 自插入不处理:如果
substr指向str内部的某个位置,移动时会覆盖源数据。必须提前拷贝。 - 缓冲区大小计算错误:新长度 = len_str + len_sub,但还要加 1 给 '\0'。很多人漏掉这个 1。
- 插入点等于字符串长度时:此时相当于追加,移动循环条件要小心,别越界。
嗯,差不多就这些。字符串插入看起来简单,但写一个健壮的版本,需要考虑的细节真不少。我建议你在自己的代码库里保留一个经过充分测试的 insert 函数,别每次都临时写。
毕竟,生产环境不相信眼泪,只相信经过考验的代码。
公众号:蓝海资料掘金营,微信 deep3321