1. 项目概述为什么我们要亲手“造轮子”在C语言的世界里字符串和内存操作函数比如strcpy、strcat、memcpy是我们每天都要打交道的“老朋友”。标准库string.h提供了它们我们只需一个#include就能轻松调用。但不知道你有没有过这样的疑问这些函数内部到底是怎么工作的为什么strcpy不检查目标缓冲区大小memmove和memcpy到底有什么区别面试官为什么总爱问这些函数的实现这就是我们这次项目的核心模拟实现部分C语言标准库中的字符串与内存操作函数。这绝不是一个简单的“重复造轮子”练习。通过亲手实现这些基础但至关重要的函数你将穿透API的黑箱直接触摸到C语言编程中最核心、也最容易出错的领域——指针运算与内存管理。你会深刻理解什么是“缓冲区溢出”为什么会有“段错误”以及如何写出更安全、更健壮的代码。无论你是正在学习指针感到困惑的新手还是希望夯实基础、应对技术面试的开发者这个项目都是一次极佳的“内功”修炼。接下来我将带你从零开始一步步拆解并实现几个关键函数分享我在实现过程中踩过的坑和总结的经验。2. 核心函数选型与设计思路拆解我们不可能一次性实现所有string.h中的函数那会过于庞杂。我的策略是精选最具代表性、最能揭示底层原理的几个函数进行模拟实现。选型基于两个原则一是高频使用二是原理典型。2.1 选定的核心函数列表与理由我选择了以下五个函数作为本次模拟实现的目标my_strlen(模拟strlen): 计算字符串长度。这是理解字符串以\0结尾这一根本特性的起点也是后续所有字符串操作的基础。实现它能让你彻底明白为什么遍历字符串要寻找\0。my_strcpy(模拟strcpy): 字符串拷贝。这是导致缓冲区溢出的“头号杀手”。实现它会让你对“不安全的函数”有切肤之痛并为后续实现安全版本埋下伏笔。my_strncpy(模拟strncpy): 带长度限制的字符串拷贝。它是strcpy的一个“安全”尝试但其行为非常特殊会补\0理解其实现能让你明白为什么它有时也不那么“安全”。my_strcat(模拟strcat): 字符串连接。它涉及两次“寻找结尾”的操作先找dest的\0再追加src是练习指针移动和边界检查的经典案例。my_memcpy与my_memmove(模拟memcpy和memmove): 内存拷贝。这是从“字符串”概念上升到“原始内存”操作的关键跨越。memmove需要处理内存重叠区域其实现巧妙地展示了如何通过判断内存地址关系来保证拷贝的正确性是算法思维在底层操作中的体现。2.2 函数原型与行为约定在开始编码前我们必须严格遵循标准库函数的原型这是模拟实现的基本要求。同时我们要在心里明确每个函数应该做什么、不应该做什么。// 字符串长度 size_t my_strlen(const char* str); // 字符串拷贝 char* my_strcpy(char* dest, const char* src); char* my_strncpy(char* dest, const char* src, size_t n); // 字符串连接 char* my_strcat(char* dest, const char* src); // 内存拷贝 void* my_memcpy(void* dest, const void* src, size_t n); void* my_memmove(void* dest, const void* src, size_t n);关键行为约定strlen: 遇到\0停止不包含\0的长度。strcpy: 将src包括结尾的\0拷贝到dest。它不检查dest的空间是否足够这是调用者的责任。strncpy: 拷贝最多n个字符。如果src的前n个字符里没有\0那么dest不会以\0结尾如果src长度小于n则会将dest剩余部分用\0填充直到n。这个行为很特别需要仔细实现。strcat: 在dest字符串的\0处开始追加src包括src的\0。同样不检查dest剩余空间。memcpy: 从src拷贝n个字节到dest。标准不要求处理内存重叠区域重叠时行为未定义。memmove: 功能同memcpy但必须正确处理内存重叠。这是它与memcpy唯一的、也是最重要的区别。注意我们的模拟函数命名加上了my_前缀这是为了避免与标准库函数名冲突。在实际练习时这是一个好习惯。3. 核心细节解析与关键实现技巧理解了要做什么我们深入到每个函数的核心实现细节。这里藏着许多新手容易忽略的“坑”。3.1my_strlen从“遍历”到“高效”最直观的实现是使用一个计数器count从头开始遍历直到遇到\0。size_t my_strlen(const char* str) { size_t count 0; while (*str ! \0) { count; str; } return count; }但有没有更“C语言”的写法有的。我们可以利用指针运算不引入额外的计数器。size_t my_strlen(const char* str) { const char* end str; // 用另一个指针记录起始位置 while (*end ! \0) { end; } return end - str; // 指针相减得到元素个数长度 }实操心得第二种写法更简洁也更能体现指针的本质。end - str的结果类型是ptrdiff_t但strlen返回size_t这里会发生隐式转换。在绝大多数情况下没问题但理解这个细节有助于你写出更严谨的代码。另外务必记得参数用const char*因为我们承诺不会修改传入的字符串。3.2my_strcpy理解“不安全性”的根源strcpy的危险性就藏在它极其简单的实现里。char* my_strcpy(char* dest, const char* src) { char* ret dest; // 保存目标起始地址用于返回 while ((*dest *src) ! \0) { ; // 空循环体所有工作都在条件判断里完成 } return ret; }这行while ((*dest *src) ! \0)是C语言的经典浓缩写法。它同时完成了取值、赋值、指针后移、判断是否拷贝到了\0。一旦src的长度超过了dest分配的空间循环不会停止它会一直向后写覆盖掉dest之后的内存数据这就是缓冲区溢出。病毒和许多安全漏洞常利用这一点。避坑技巧永远不要在生产代码中直接使用strcpy。如果非要使用必须在调用前确保dest的空间足够通常用strlen(src) 1来计算所需大小。更好的做法是使用我们接下来实现的strncpy或者更安全的snprintf、strlcpy非标准等。3.3my_strncpy一个“有点怪”的安全尝试strncpy的本意是提供一种带长度限制的拷贝但它特殊的行为模式常常让人误解。char* my_strncpy(char* dest, const char* src, size_t n) { char* ret dest; size_t i; for (i 0; i n src[i] ! \0; i) { dest[i] src[i]; } for ( ; i n; i) { dest[i] \0; // 关键行为如果 src 长度小于 n用 \0 填充剩余空间 } return ret; }核心细节解析第一个for循环负责拷贝字符条件有两个i n且src[i]不是\0。这意味着它最多拷贝n个字符或者遇到src的结尾就停止。第二个for循环是strncpy最特别的地方如果src在拷贝完n个字符前就结束了即src长度小于n它会用\0填满dest剩下的所有空间直到写满n个字符。最重要的陷阱如果src的长度大于或等于n那么strncpy会在拷贝完n个字符后停止并且不会在dest的末尾添加\0这意味着dest可能不是一个有效的C字符串没有以\0结尾。这是很多程序员误用strncpy导致bug的原因。注意正因为strncpy这个“可能不加\0”的特性很多人认为它并非真正的“安全”。一个常见的、更安全的做法是在调用strncpy后手动确保dest的最后一个字符是\0dest[n-1] \0;。但这需要你清楚n是dest缓冲区的大小。3.4my_strcat两次遍历的代价strcat的实现可以看作是strlen和strcpy的组合。char* my_strcat(char* dest, const char* src) { char* ret dest; // 第一步找到 dest 的结尾\0 的位置 while (*dest ! \0) { dest; } // 第二步从 dest 的结尾开始执行 strcpy 操作 while ((*dest *src) ! \0) { ; } return ret; }性能考量strcat需要先遍历dest找到结尾其时间复杂度是 O(目标字符串长度)。如果在循环中频繁连接多个字符串效率会很低。一个优化方法是自己记录当前dest的尾部位置避免重复遍历。这引出了另一个概念——“字符串构建器”模式。3.5my_memcpy与my_memmove内存重叠的艺术这是本次模拟实现中最精彩的部分它直接操作内存不关心内容是否是字符串。my_memcpy的基础实现 我们可以按字节拷贝。注意参数和返回类型都是void*这意味着我们需要在函数内部转换为char*进行字节操作。void* my_memcpy(void* dest, const void* src, size_t n) { char* d (char*)dest; const char* s (const char*)src; for (size_t i 0; i n; i) { d[i] s[i]; } return dest; }问题来了如果dest和src指向的内存区域有重叠Overlap比如dest在src后面一点用上面的逐字节正向拷贝会发生什么假设src地址是pdest地址是p2我们要拷贝5个字节。拷贝过程是(p2) (p)// 正确(p3) (p1)// 正确(p4) (p2)// 此时(p2)的位置已经被步骤1改写了所以这里拷贝的是错误的数据。... 后续拷贝都基于被污染的数据。这就是内存重叠导致的数据污染。memcpy标准不处理这种情况所以我们必须避免向重叠区域拷贝。但memmove被设计来处理它。my_memmove的智慧 处理重叠的核心思路是判断拷贝方向。如果dest的地址在src之前或者两者完全不重叠我们可以像memcpy一样从低地址向高地址正向拷贝。如果dest的地址在src之后并且有重叠即dest src dest src n我们就必须从高地址向低地址反向拷贝这样才能避免源数据在被使用前被覆盖。void* my_memmove(void* dest, const void* src, size_t n) { char* d (char*)dest; const char* s (const char*)src; if (d s) { // 情况1dest 在 src 前面或完全不重叠正向拷贝 for (size_t i 0; i n; i) { d[i] s[i]; } } else if (d s) { // 情况2dest 在 src 后面可能重叠反向拷贝 for (size_t i n; i 0; i--) { d[i-1] s[i-1]; } } // 情况3dest src什么都不用做 return dest; }为什么反向拷贝能解决问题继续上面的例子 (srcp, destp2, n5) 反向拷贝从最后一个字节开始(p6) (p4)// 拷贝最后一个字节(p5) (p3)(p4) (p2)(p3) (p1)(p2) (p)// 拷贝第一个字节 你会发现每次拷贝时源地址s[i-1]都还没有被目标地址d[i-1]覆盖过因为我们是倒着来的。这就完美解决了重叠拷贝的问题。4. 完整模拟实现与测试代码纸上得来终觉浅绝知此事要躬行。下面我将给出所有函数的完整实现并附上一个简单的测试程序你可以直接复制代码到你的IDE如VS Code中编译运行。4.1 头文件my_string.h首先我们创建一个头文件来声明我们的函数。#ifndef MY_STRING_H #define MY_STRING_H #include stddef.h // 为了使用 size_t // 模拟字符串函数 size_t my_strlen(const char* str); char* my_strcpy(char* dest, const char* src); char* my_strncpy(char* dest, const char* src, size_t n); char* my_strcat(char* dest, const char* src); // 模拟内存函数 void* my_memcpy(void* dest, const void* src, size_t n); void* my_memmove(void* dest, const void* src, size_t n); #endif // MY_STRING_H4.2 源文件my_string.c接着在源文件中实现所有函数。#include “my_string.h” // my_strlen - 计算字符串长度 size_t my_strlen(const char* str) { const char* end str; while (*end ! \0) { end; } return end - str; } // my_strcpy - 字符串拷贝 char* my_strcpy(char* dest, const char* src) { char* ret dest; while ((*dest *src) ! \0) { ; } return ret; } // my_strncpy - 带长度限制的字符串拷贝 char* my_strncpy(char* dest, const char* src, size_t n) { char* ret dest; size_t i; for (i 0; i n src[i] ! \0; i) { dest[i] src[i]; } for ( ; i n; i) { dest[i] \0; } return ret; } // my_strcat - 字符串连接 char* my_strcat(char* dest, const char* src) { char* ret dest; while (*dest ! \0) { dest; } while ((*dest *src) ! \0) { ; } return ret; } // my_memcpy - 内存拷贝不处理重叠 void* my_memcpy(void* dest, const void* src, size_t n) { char* d (char*)dest; const char* s (const char*)src; for (size_t i 0; i n; i) { d[i] s[i]; } return dest; } // my_memmove - 内存移动处理重叠 void* my_memmove(void* dest, const void* src, size_t n) { char* d (char*)dest; const char* s (const char*)src; if (d s) { // 正向拷贝 for (size_t i 0; i n; i) { d[i] s[i]; } } else if (d s) { // 反向拷贝 for (size_t i n; i 0; i--) { d[i-1] s[i-1]; } } // 如果 d s什么都不做 return dest; }4.3 测试文件test.c最后我们编写一个测试程序来验证函数的正确性。测试要覆盖正常情况、边界情况和异常情况。#include stdio.h #include string.h // 引入标准库用于对比结果 #include “my_string.h” void test_strlen() { printf(“ Testing my_strlen \n”); char* str1 “Hello”; char str2[] “World!”; char str3[] “”; // 空字符串 char str4[] “A\0BC”; // 中间包含\0 printf(“‘%s’ - my: %zu, std: %zu\n”, str1, my_strlen(str1), strlen(str1)); printf(“‘%s’ - my: %zu, std: %zu\n”, str2, my_strlen(str2), strlen(str2)); printf(“‘’ - my: %zu, std: %zu\n”, my_strlen(str3), strlen(str3)); printf(“‘A\\0BC’ - my: %zu (should be 1)\n”, my_strlen(str4)); printf(“\n”); } void test_strcpy() { printf(“ Testing my_strcpy \n”); char dest1[20]; char* src1 “Copy this”; my_strcpy(dest1, src1); printf(“src: ‘%s’, dest: ‘%s’\n”, src1, dest1); char dest2[10]; char src2[] {‘a’, ‘b’, ‘c’, ‘\0’}; my_strcpy(dest2, src2); printf(“src: ‘%s’, dest: ‘%s’\n”, src2, dest2); printf(“\n”); } void test_strncpy() { printf(“ Testing my_strncpy \n”); char dest1[10]; char* src1 “HelloWorld”; // 测试 n 小于 src 长度且 src 长度 n my_strncpy(dest1, src1, 5); dest1[5] ‘\0’; // 手动添加结束符因为 strncpy 可能没加 printf(“src: ‘%s’, n5 - dest: ‘%s’\n”, src1, dest1); // 测试 n 大于 src 长度 char dest2[10]; char* src2 “Hi”; my_strncpy(dest2, src2, 6); // src2 只有2个字符1个\0strncpy会填充\0 printf(“src: ‘%s’, n6 - dest: ‘%s’ (should be ‘Hi\\0\\0\\0\\0’)\n”, src2, dest2); // 打印前6个字符的ASCII值来验证 for (int i 0; i 6; i) { printf(“dest2[%d] %d\n”, i, dest2[i]); } printf(“\n”); } void test_strcat() { printf(“ Testing my_strcat \n”); char dest[20] “Hello, “; // 注意dest必须有足够空间 char* src “World!”; my_strcat(dest, src); printf(“After concat: ‘%s’\n”, dest); char dest2[10] “”; my_strcat(dest2, “Start”); my_strcat(dest2, “End”); printf(“Multiple concat: ‘%s’\n”, dest2); printf(“\n”); } void test_memcpy() { printf(“ Testing my_memcpy \n”); int src[] {1, 2, 3, 4, 5}; int dest[5]; my_memcpy(dest, src, sizeof(src)); printf(“Copied array: “); for (int i 0; i 5; i) { printf(“%d “, dest[i]); } printf(“\n”); // 测试重叠区域memcpy 不保证正确 char data[] “abcdefgh”; printf(“Before overlap copy: %s\n”, data); my_memcpy(data 2, data, 4); // 将前4个字符拷贝到从c开始的位置 printf(“After overlap copy with memcpy: %s (may be wrong)\n”, data); printf(“\n”); } void test_memmove() { printf(“ Testing my_memmove \n”); // 测试重叠区域dest src char data[] “abcdefgh”; printf(“Before overlap move: %s\n”, data); my_memmove(data 2, data, 4); // 正确地将”abcd”移动到从c开始的位置 printf(“After overlap move with memmove: %s (should be ‘ababcdgh’)\n”, data); // 测试另一方向的重叠dest src char data2[] “abcdefgh”; my_memmove(data2, data2 3, 4); // 将”defg”移动到开头 printf(“Move from later to earlier: %s (should be ‘defgefgh’)\n”, data2); printf(“\n”); } int main() { test_strlen(); test_strcpy(); test_strncpy(); test_strcat(); test_memcpy(); test_memmove(); printf(“All tests completed.\n”); return 0; }编译与运行 在终端或你的IDE中使用以下命令编译假设使用gccgcc -o test_program my_string.c test.c ./test_program观察输出将我们自定义函数的结果与预期行为或标准库函数结果进行对比确保逻辑正确。5. 常见问题与深度避坑指南在实际编写和调试这些函数的过程中我遇到了不少典型问题。这里把它们总结出来希望能帮你绕过这些坑。5.1 指针与数组的混淆这是新手最常犯的错误。char* str和char str[]在函数参数中经常可以互换但在实现时对它们进行操作的心态是不同的。char* str这是一个指针变量。str是改变这个指针本身的值让它指向下一个字符。char str[]在函数参数中它退化成指针。但在函数内部如果你用数组下标str[i]的方式访问str本身的值作为地址是不变的。在my_strlen的第二种实现中我创建了一个新的指针end来遍历而不是直接用参数str遍历就是为了保留起始地址str用于最后的减法运算。如果直接用str最后就找不到字符串开头了。5.2 关于const的正确使用const关键字不是摆设它是一份给编译器和读者的契约。const char* src承诺不通过指针src修改它所指向的内存内容。这保护了源数据也清晰地表达了函数的意图。在my_memcpy和my_memmove中src参数是const void*但在内部我们需要按字节访问所以转换为const char*。这个转换是安全的因为我们只是换了一种解读内存的方式并且依然保持const承诺。忘记const会导致什么首先编译器可能会警告。其次如果调用者传入一个字符串字面量如“hello”而你的函数试图修改它程序可能会在运行时崩溃字符串字面量通常存储在只读内存段。5.3strncpy的\0陷阱与安全使用模式前面已经详细说明了strncpy可能不在目标末尾添加\0的问题。这里给出一个安全使用strncpy的固定模式char dest[BUFFER_SIZE]; char src[] “This is a potentially long string”; // 安全模式总是确保目标缓冲区以 \0 结尾 my_strncpy(dest, src, BUFFER_SIZE - 1); // 预留一个位置给 \0 dest[BUFFER_SIZE - 1] ‘\0’; // 手动确保最后一个字符是 \0这个模式保证了dest无论如何都是一个有效的、以\0结尾的C字符串并且不会发生缓冲区溢出最多截断。5.4memmove重叠判断的逻辑细节在my_memmove的实现中重叠判断if (d s)和else if (d s)是核心。d s源和目标相同无需拷贝直接返回。d s目标在源的前面即使有重叠也是目标的尾部与源的头部重叠。正向拷贝时我们先拷贝源的低地址部分头部这部分数据在拷贝到目标的高地址部分尾部之前就已经被读走了所以是安全的。d s目标在源的后面此时重叠意味着目标的头部与源的尾部重叠。我们必须反向拷贝从最后一个字节开始先拷贝源的高地址部分尾部这样在覆盖目标低地址部分头部之前源数据已经被读取。一个常见的错误是只判断d s就进行反向拷贝而忽略了d s时也可能存在重叠但需要正向拷贝的情况。我们的判断逻辑覆盖了所有可能性。5.5 性能优化的思考我们实现的版本侧重于清晰易懂。但在高性能场景下还有优化空间字长拷贝现代的memcpy/memmove库实现通常不会逐字节拷贝。它们会判断内存对齐然后使用更宽的寄存器如32位、64位一次拷贝多个字节甚至使用SIMD指令进行向量化操作。这能极大提升大块内存拷贝的速度。strlen的优化也有利用CPU特性一次检查多个字节是否包含\0的算法如Glibc中的实现。strcat的效率如前所述频繁连接字符串时应避免反复调用strcat导致O(n²)复杂度。可以维护一个指向字符串当前末尾的指针。对于学习和理解原理我们的简单实现完全足够。但了解这些优化方向能让你明白标准库为何高效以及在需要时如何自己进行优化。亲手实现这些基础函数的过程就像一次对C语言内存模型的深度解剖。每一个指针的移动每一次边界的判断都让你对“程序如何在内存中运行”有了更具体的认知。这种认知是写出稳定、高效C代码的基石。下次当你再调用strcpy或memcpy时你脑海中浮现的将不再是一个黑盒函数名而是一段清晰的、由你自己构建的逻辑流程。这才是这个项目最大的价值所在。