C语言字符串操作实战:利用strstr与memmove高效删除子串

发布时间:2026/11/5 11:14:09

C语言字符串操作实战:利用strstr与memmove高效删除子串 1. 项目概述一个看似简单却暗藏玄机的字符串操作今天想聊一个在C语言学习和面试中高频出现但又常常被轻视的经典问题如何实现删除字符串中的指定子串。乍一看这问题简单得有点“小儿科”——不就是找到子串然后把后面的字符往前挪吗很多新手朋友可能随手就能写个循环嵌套觉得大功告成。但如果你真这么想那可能就错过了理解C语言字符串操作精髓以及提升代码健壮性和效率的绝佳机会。这个项目标题“用C语言实现删除字符串中的子串C 库函数strstr()、memmove()、strcpy()、memset()、strlen()”已经为我们指明了核心它不仅仅是一个功能实现更是一次对C标准库中几个关键字符串/内存操作函数的深度理解和综合运用实战。strstr负责查找memmove负责安全地“搬运”内存strcpy和memset看似简单却各有适用场景strlen则是我们操作的基础尺子。把这些函数像乐高积木一样正确、高效地组合起来解决一个实际问题远比单独背诵它们的函数原型有意义得多。无论是正在啃《C语言程序设计》的学生准备技术面试的求职者还是希望夯实基础的开发者通过亲手实现这个功能并深入理解背后的“为什么”你都能获得远超代码本身的收获对指针、内存布局、函数边界条件的深刻认知以及写出既正确又优雅的C代码的能力。我们这就开始一步步拆解这个“麻雀虽小五脏俱全”的经典案例。2. 核心思路拆解与方案选型在动手写代码之前我们必须把问题想清楚。删除字符串中子串本质上是一个查找并覆盖的过程。假设我们有一个源字符串src和一个待删除的子串sub我们需要在src中找出所有sub出现的位置然后将该位置之后的所有字符不包括被删除的子串向前移动覆盖掉这个子串。2.1 为什么不能简单地用循环和指针“硬挪”很多初学者的第一反应是写两层循环外层遍历字符串内层比较子串。一旦匹配成功就用另一个循环把后面的字符逐个向前赋值。这个方法在逻辑上可行但存在几个明显问题效率低下每次删除哪怕只删除一个字符都可能引发大量字符的逐个移动时间复杂度在最坏情况下会接近 O(n*m)。代码冗长易错需要手动处理字符串结束符\0指针的加减运算稍有不慎就会导致越界或错误。未充分利用标准库C标准库提供了高度优化、久经考验的函数重复造轮子不仅增加工作量还容易引入bug。2.2 基于标准库的高效方案设计标题给出的函数列表就是我们的“工具箱”。一个高效且健壮的方案流程如下查找定位使用strstr()函数在源字符串中查找子串首次出现的位置。这是整个算法的“眼睛”。计算长度使用strlen()获取源字符串和子串的长度为后续内存移动提供精确计量。安全移除找到子串后需要将子串之后的部分整体前移。这里的关键是不能使用strcpy()因为源和目的内存区域可能存在重叠我们正是要把后面的内容拷贝到前面来。此时必须使用memmove()它是专门为处理重叠内存区域拷贝而设计的。循环与结束完成一次删除后在新的字符串中继续查找子串因为删除后可能产生了新的匹配直到strstr()返回NULL表示已无更多子串。结果输出最终源字符串缓冲区中的内容就是删除所有子串后的结果。这个方案将时间复杂度优化到了接近 O(n)因为strstr和memmove的实现通常都经过高度优化并且我们避免了嵌套循环内的逐个字符操作。注意这里我们明确排除了使用strcpy()来执行删除操作。strcpy(dest, src)在dest和src内存区域重叠时行为是未定义的极有可能导致错误结果或程序崩溃。这是一个非常重要的知识点。3. 核心函数深度解析与避坑指南在实现之前我们必须吃透这几个核心函数了解它们的脾气秉性否则极易踩坑。3.1 strstr() – 字符串查找器char *strstr(const char *haystack, const char *needle);这个函数在haystack干草堆中查找needle针第一次出现的位置返回指向该位置的指针如果没找到返回NULL。关键点与避坑参数检查虽然标准规定传入NULL指针行为未定义但健壮的代码应该自己检查。如果needle是空字符串根据C99标准应返回haystack。返回值的使用返回的指针指向的是haystack中匹配开始的位置。我们需要保存这个指针因为它决定了从哪里开始删除。查找的连续性在一次删除操作后下一次查找的起点应该是当前删除操作完成后的新字符串的当前位置而不是简单地从之前找到的位置后继续。因为字符串内容已经改变了。3.2 memmove() vs memcpy() strcpy() – 内存搬运大师void *memmove(void *dest, const void *src, size_t n);这是本项目的灵魂函数。它的作用是从src指向的内存地址拷贝n个字节到dest指向的内存地址。它与memcpy()的关键区别在于memmove()会先将源数据拷贝到一个临时缓冲区然后再拷贝到目标地址从而安全地处理源和目标内存区域重叠的情况。而memcpy()则假定它们不重叠重叠时行为未定义。为什么这里必须用memmove假设字符串是“hello world”要删除“lo”。找到“lo”在位置p。我们需要把p2即” world”的起始位置开始的内容移动到p的位置。源p2和目标p是重叠的memcpy(p, p2, len)会导致数据损坏。而memmove能正确处理这种情况。参数计算n应该拷贝多少字节应该是子串之后剩余字符串的长度 1包括结尾的\0。我们可以用strlen(found_position sub_len)来获取剩余字符串长度然后1来包含\0。3.3 strlen() – 长度测量员size_t strlen(const char *str);返回字符串长度不包括结尾的\0。这个函数看似简单但要注意它是通过遍历字符串直到遇到\0来计数的时间复杂度是 O(n)。在循环中频繁调用strlen可能成为性能瓶颈。在本项目中我们可以在循环开始前获取一次子串长度在循环内根据需要计算剩余部分长度。3.4 strcpy() 和 memset() – 本项目的“配角”在本项目的核心删除逻辑中strcpy()并不适用原因已述。memset()通常用于将一段内存设置为特定的值。在本项目中一个可能的用法是在删除子串后将字符串末尾空出来的部分如果有的话用\0填充以确保字符串正确终止并且避免残留的旧字符造成混淆。但这并非必须因为memmove已经将末尾的\0一并移动了。4. 分步实现与代码精讲理论清晰后我们开始动手实现。我将提供一个完整、健壮且带有详细注释的版本。4.1 函数接口设计首先明确我们的函数原型。由于C语言中字符串通常存储在字符数组中我们的操作会直接修改传入的缓冲区。一个经典的接口设计是void delete_substr(char *str, const char *sub);str指向待处理的字符串缓冲区的指针。函数将直接修改这块内存。sub指向待删除子串的指针。这个设计简单直接但调用者必须确保str指向的缓冲区足够大且可修改即不能是字符串常量。4.2 完整实现代码#include stdio.h #include string.h #include assert.h /** * brief 从字符串 str 中删除所有子串 sub 的出现。 * param str 待处理的字符串会被修改。 * param sub 待删除的子串。 * note 该函数直接修改传入的 str 缓冲区。 */ void delete_all_substr(char *str, const char *sub) { // 防御性编程检查输入指针是否有效 if (str NULL || sub NULL) { return; // 或进行错误处理如 fprintf(stderr, Invalid input\n); } size_t sub_len strlen(sub); // 如果子串为空则无需任何操作根据strstr语义空串被视为匹配开头 if (sub_len 0) { return; } // 主循环持续查找并删除直到字符串中不再包含子串 char *pos NULL; while ((pos strstr(str, sub)) ! NULL) { // 计算需要移动的尾部字符串的起始位置和长度 char *tail_start pos sub_len; // 子串之后的部分 size_t tail_len strlen(tail_start) 1; // 1 为了包含结尾的 \0 // 使用 memmove 安全地重叠内存拷贝 memmove(pos, tail_start, tail_len); // 注意删除后str 的内容已经改变循环条件中的 strstr 会基于新字符串继续查找 } } /** * brief 从字符串 str 中删除子串 sub 的第一次出现。 * param str 待处理的字符串会被修改。 * param sub 待删除的子串。 * return 指向删除操作后字符串的指针通常就是 str。如果未找到子串返回原 str。 */ char *delete_first_substr(char *str, const char *sub) { if (str NULL || sub NULL) { return str; } char *pos strstr(str, sub); if (pos ! NULL) { size_t sub_len strlen(sub); size_t tail_len strlen(pos sub_len) 1; memmove(pos, pos sub_len, tail_len); } return str; }4.3 代码逐行精讲与避坑点输入验证if (str NULL || sub NULL) return;这是健壮代码的第一步。直接操作空指针会导致程序崩溃。处理空子串if (sub_len 0) return;这是一个边界条件。strstr(str, )通常会返回str这会导致无限循环或错误操作。我们提前处理这种情况。核心循环while ((pos strstr(str, sub)) ! NULL)这是一个非常简洁高效的写法。每次循环都从当前字符串str的开头查找子串。pos记录了本次找到的位置。计算移动参数char *tail_start pos sub_len;这指向了被删除子串之后的第一个字符。size_t tail_len strlen(tail_start) 1;这里1是整个实现中最容易忽略的关键点。strlen(tail_start)只计算了\0之前的字符数。我们必须把\0也一起移动过来才能保证移动后的字符串正确终止。忘记1会导致新字符串没有结束符后续操作如printf、strlen会读取到后面的垃圾数据直至遇到一个偶然的\0引发不可预知的错误。执行移动memmove(pos, tail_start, tail_len);这就是执行“删除”的魔法语句。它将尾部字符串含\0安全地搬运到子串开始的位置实现了覆盖删除。循环继续移动完成后while循环的条件会再次执行strstr(str, sub)。由于str指向的缓冲区内容已经更新所以它会从新的字符串中继续查找是否还有匹配的子串。例如删除“aa”中的“a”第一次删除后字符串变为“a”第二次strstr(“a”, “a”)依然能找到直到字符串为空。关于delete_first_substr函数它只删除第一次出现的子串。有时我们只需要这个功能。它的实现是delete_all_substr的简化版只执行一次查找和移动。返回值的设计模仿了标准库风格方便链式调用。5. 测试用例与边界条件处理写好了函数必须进行全面的测试。以下是一些必须考虑的测试场景int main() { char str1[100] hello world, hello universe!; printf(原始: %s\n, str1); delete_all_substr(str1, hello ); printf(删除hello 后: %s\n, str1); // 预期: world, universe! // 测试子串在末尾 char str2[100] This is a test string test; delete_all_substr(str2, test); printf(删除test后: %s\n, str2); // 预期: This is a string // 测试子串重叠出现 char str3[100] aaaa; delete_all_substr(str3, aa); printf(删除aa后: %s\n, str3); // 这是一个有趣案例预期结果是什么 // 第一次删除前两个a字符串变为aa继续查找删除最终变为空串。 // 测试空子串 char str4[100] normal string; delete_all_substr(str4, ); printf(删除空串后: %s\n, str4); // 预期: normal string (不变) // 测试子串不存在 char str5[100] original; delete_all_substr(str5, xyz); printf(删除不存在的xyz后: %s\n, str5); // 预期: original (不变) // 测试字符串为空 char str6[100] ; delete_all_substr(str6, something); printf(空字符串删除后: %s\n, str6); // 预期: (不变) // 测试只删除第一次出现 char str7[100] repeat repeat repeat; delete_first_substr(str7, repeat ); printf(仅删除第一次repeat 后: %s\n, str7); // 预期: repeat repeat return 0; }运行这些测试观察输出是否符合预期是验证函数正确性的最好方法。特别是重叠删除“aaaa”中删除“aa”和空串处理是常见的思维盲区。6. 常见问题、性能分析与优化在实际使用和面试中围绕这个实现会衍生出许多深入的问题。6.1 为什么循环中不更新str指针细心的读者会发现在delete_all_substr的循环里我们始终用strstr(str, sub)这个str是最初传入的指针一直没变。而字符串内容在memmove后头部可能已经改变了如果从开头删除但str指向的地址没变这有问题吗没有问题。str是一个指向字符数组首地址的指针。memmove移动的是该指针所指向缓冲区里的内容。无论内容怎么变str这个“门牌号”始终指向这块内存的开始。strstr每次都是从这块内存的起始地址开始搜索新的内容逻辑是正确的。如果我们在循环里让str pos;反而会出错因为跳过了已经被检查和处理过的前半部分。6.2 内存重叠的陷阱再探讨我们强调必须用memmove。为了加深理解可以思考如果非要用strcpy来实现这个移动怎么写才是安全的 答案是无法安全实现。因为strcpy在拷贝时不知道长度它遇到\0才停止。在重叠的情况下源字符串的\0可能在拷贝过程中被覆盖导致strcpy停不下来直到发生缓冲区溢出或访问到非法内存。这是一个根本性的设计限制。6.3 性能分析与潜在优化我们的算法时间复杂度可以近似认为是 O(n*m)其中 n 是原串长度m 是子串长度这主要是strstr的最坏情况复杂度。memmove的复杂度是 O(k)k 是每次需要移动的尾部长度。优化点1避免重复计算子串长度。我们在循环外只计算一次sub_len这是好的。优化点2减少strlen调用。在循环内我们使用了strlen(tail_start)来计算尾部长度。如果待删除的子串很长或者删除操作很多这里可以优化。我们可以通过指针运算直接得到尾部长度size_t tail_len (str strlen(str)) - tail_start 1;。但前提是我们要在循环内维护当前字符串的长度这增加了状态管理的复杂度。对于大多数情况当前的写法清晰度优先性能开销可接受。优化点3使用更高效的查找算法。标准库的strstr实现通常已经足够高效可能使用了 KMP、Boyer-Moore 等算法。但在极端性能要求的场景下可以自己实现特定的查找逻辑。6.4 一个进阶挑战不分配额外空间原地删除所有指定字符这是一个相关的经典面试题。例如删除字符串中的所有空格。思路更简单使用两个指针或索引一个快指针p_fast用于遍历原字符串一个慢指针p_slow用于指向下一个有效字符的存放位置。当p_fast指向的字符不是要删除的字符时就将其拷贝到p_slow的位置然后两个指针都前进否则只有p_fast前进。最后在p_slow的位置写入\0。这个方法只需要一次遍历时间复杂度 O(n)且是严格的原地操作。void remove_char(char *str, char c) { if (str NULL) return; char *dst str; // 慢指针 char *src str; // 快指针 while (*src) { if (*src ! c) { *dst *src; } src; } *dst \0; }理解了这个“双指针”技巧再回头看我们的删除子串函数会发现其核心思想有相通之处只不过“判断是否要删除”的逻辑从“单个字符是否相等”变成了“是否匹配一个子串”。7. 项目总结与扩展思考通过这个“删除字符串中子串”的项目我们完成了一次对C语言字符串和内存操作的深度遍历。它远不止于几行代码而是串联起了指针、数组、内存管理、标准库函数应用和边界条件处理等多个核心知识点。我个人在实现和教学这个案例时最深的体会是C语言的简洁和强大背后是对程序员责任的严格要求。每一个函数的选择memmovevsmemcpy、每一个边界条件的判断空串、NULL指针、每一次长度的计算是否包含\0都直接关系到程序的正确性与安全性。忽略其中任何一点都可能埋下难以察觉的bug。这个项目可以很自然地扩展到其他相关功能替换子串找到子串后不是删除而是用另一个字符串替换它。这需要处理新子串长度不同带来的内存伸缩问题可能涉及realloc如果字符串在堆上或确保缓冲区足够大。分割字符串基于一个分隔符子串将字符串分割成多个部分。这通常需要动态数组或链表来保存结果。实现一个简单的字符串处理库将删除、替换、查找、分割等功能封装成一组函数并统一处理内存分配错误。最后建议你不仅仅停留在阅读和理解上。打开你的编辑器无论是VSCode、CLion还是简单的文本编辑器配合GCC把代码敲一遍运行不同的测试用例甚至故意写一些有bug的版本比如把memmove换成memcpy观察会发生什么。这种从“知道”到“做到”再到“理解为什么必须这样做”的过程才是提升C语言功力的不二法门。
延伸阅读

更多相关文章

2026/11/5 11:14:08

独立开发者如何借助Taotoken模型广场为不同任务选择性价比最优模型

独立开发者如何借助Taotoken模型广场为不同任务选择性价比最优模型 对于独立开发者或小微工作室而言,在有限的预算内进行AI应用开发,意味着需要在模型效果与调用成本之间找到最佳平衡点。不同的开发任务——例如智能对话、代码生成、文案润色——往往对…

2026/11/2 19:52:03

C++高性能日志系统:spdlog与fmt集成方案与工程实践

1. 项目概述:为什么需要fmt与spdlog的强强联合?在C项目里,日志系统就像项目的“黑匣子”和“健康监测仪”。它不仅要能稳定、无遗漏地记录下程序运行时的每一个关键状态和错误,还得足够高效,不能因为打日志这件事本身拖…

2026/11/5 9:46:01

Unity跨平台插件开发实战:FLUX.1-dev框架与多平台SDK集成指南

1. 项目概述:为什么我们需要一个跨平台的Unity插件? 如果你是一个Unity开发者,尤其是在移动端或者多平台项目上工作过,你肯定遇到过这样的场景:项目需要接入一个第三方SDK,比如一个广告平台、一个数据分析工…

2026/11/5 11:14:04

Java机器学习图书推荐系统源码:ItemCF与TF-IDF工程实践

简介:这份源码包围绕基于机器学习的图书推荐系统展开,面向Java Web课程设计、毕业设计以及希望实践协同过滤推荐算法的开发者,重点解决图书个性化推荐与后台管理问题。项目包含图书管理、读书会管理及科技、人文、历史、中小学等图书分类推荐…

2026/11/5 11:14:04

课堂行为ST自动分析软件:毕业设计可运行资源与调优指南

简介:这份资源是面向教育技术方向学生与毕业设计学习者的课堂行为智能分析软件项目,通过AI与计算机视觉技术对课堂视频或智能设备采集的数据进行学生行为检测与参与度量化,为教学效果评估提供参考。压缩包共760个文件,以631个Pyth…

2026/11/5 11:14:04

readelf解析.so:从ELF头到重定位表的崩溃偏移定位清单

简介:一份以 Android 平台 so 文件解析为主题的示例工程,面向具备 Java 基础、希望进一步了解 JNI 与原生动态库的开发者,可用于梳理动态链接库的代码段、数据段、符号表和重定位信息等基本结构,并建立 Java 层与原生层衔接的整体…

2026/11/5 11:14:04

基于Django的运维平台实战:资产管理、任务执行与发布流程

简介:这份资源是一套基于Django框架开发的轻量级运维平台源码,面向具备Python基础、希望了解Web运维系统实现思路的开发者与运维人员,可用于学习主机管理、服务监控、日志查看、任务调度、权限控制、资源统计、脚本执行与故障告警等常见运维模…

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 0:02:07

多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中…

2026/11/5 0:02:07

大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#…

2026/11/5 0:02:07

鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的…

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑