1. 从“会用”到“懂它”:为什么我们需要模拟实现字符串函数
在C语言的世界里,字符串处理是绕不开的基本功。strlen、strcpy、strcat、strcmp、strstr这些函数,就像工具箱里的螺丝刀和扳手,每个C程序员都再熟悉不过。我们调用它们,依赖它们,但很多时候,我们只是停留在“会用”的层面。你有没有想过,strcpy在复制时遇到\0是怎么停下来的?strcmp比较两个字符串时,内部到底在比较什么?当面试官让你手写一个strstr时,你是否会感到一丝心虚?
模拟实现这些字符串函数,恰恰是打通“知其然”到“知其所以然”的关键一步。这绝不仅仅是为了应付面试题。通过亲手实现,你会深刻理解指针在字符串操作中的精妙舞步,你会对内存布局和边界条件有肌肉记忆般的警觉,你会明白那些标准库函数设计背后的权衡与智慧。更重要的是,当你在嵌入式开发、底层系统编程或者性能优化场景中,遇到标准库不可用或需要定制化行为时,这份“徒手造轮子”的能力将成为你的底气。今天,我们就抛开<string.h>,从零开始,重新打造这些字符串处理的基石,看看光鲜的API背后,藏着怎样的逻辑与细节。
2. 基石:模拟实现my_strlen—— 测量字符串的“脉搏”
strlen的功能极其单纯:计算一个以\0结尾的字符串的长度,不包括终止符本身。虽然简单,但它的实现方式却有好几种,每种都体现了不同的编程思想。
2.1 计数器法:最直观的实现
最符合直觉的做法就是使用一个计数器,从头开始遍历字符串,直到遇到\0。
size_t my_strlen_counter(const char* str) { size_t count = 0; // 计数器初始化为0 if (str == NULL) { // 良好的健壮性检查 return 0; // 或者根据需求返回特定错误值 } while (*str != '\0') { // 解引用指针,判断当前字符是否为结束符 count++; // 计数器加1 str++; // 指针移动到下一个字符 } return count; }实现要点与避坑:
- 参数类型:使用
const char*,表明函数不会修改传入的字符串,这是对调用者的承诺,也是良好的接口设计。 - 返回值类型:
size_t是无符号整数类型,专门用于表示对象大小或数组索引。它保证了返回值非负,并且与标准库strlen的声明保持一致。 - 空指针检查:这是一个非常重要的防御性编程习惯。虽然C标准规定向
strlen传递NULL指针是未定义行为,但在我们自己实现的函数中进行检查,可以避免程序因非法内存访问而崩溃,提高代码的健壮性。在实际项目中,根据上下文决定是返回0、-1还是直接断言失败。 - 循环条件:
while (*str)是while (*str != '\0')的简洁写法,因为\0的ASCII码值为0,在条件判断中为假。很多资深程序员喜欢这种写法,因为它更简洁。
2.2 指针相减法:无需额外变量的“炫技”
C语言允许对指向同一数组(或字符串)的指针进行相减,结果是它们之间相差的元素个数。利用这个特性,我们可以实现一个不需要计数器的版本。
size_t my_strlen_pointer(const char* str) { const char* start = str; // 记录起始位置 if (str == NULL) { return 0; } while (*str) { // 遍历到字符串末尾的'\0' str++; } return (size_t)(str - start); // 尾指针减去头指针,即为长度 }为什么指针相减是可行的?在C语言中,指针的算术运算是以指向类型的大小为单位的。char类型大小为1字节,所以str++每次移动1字节。当str移动到\0之后的位置时,str - start计算的就是两个char*指针之间相差的char元素个数,正好是字符串的长度。这种方法通常被视作更“高效”或更“优雅”,因为它避免了一个局部变量的累加操作。但在现代编译器的优化下,两种方法的性能差异微乎其微。
注意:指针相减的结果类型是
ptrdiff_t,这是一个有符号整数类型。而strlen返回size_t(无符号)。在返回时进行强制类型转换(size_t)是必要的,但前提是你能确保结果非负(对于strlen,这总是成立的)。这是一个微妙的类型细节。
2.3 递归法:理解函数栈的绝佳案例
虽然完全不推荐在生产代码中使用,但用递归实现strlen是一个帮助理解函数调用栈和递归思想的绝佳练习。
size_t my_strlen_recursive(const char* str) { if (str == NULL || *str == '\0') { // 递归基:空指针或空字符串 return 0; } return 1 + my_strlen_recursive(str + 1); // 递归步:当前字符(1) + 剩余字符串的长度 }它的工作原理:函数不断检查当前字符,如果不是\0,就返回1加上“从下一个字符开始的子串的长度”。这个调用会一层层深入,直到遇到\0,然后逐层返回并累加。
致命缺陷:对于长字符串(比如几K甚至更长),递归深度会非常大,极易导致栈溢出(Stack Overflow),使程序崩溃。此外,函数调用开销也远大于简单的循环。因此,这种方法仅存在于教科书和面试讨论中,用以考察对递归的理解,切记不可用于实际项目。
3. 复制与拼接:模拟实现my_strcpy与my_strcat
strcpy和strcat是导致缓冲区溢出(Buffer Overflow)安全漏洞的“重灾区”。模拟实现它们,能让你对“源”和“目标”内存区域的关系有刻骨铭心的认识。
3.1my_strcpy:内存的“搬运工”
strcpy(dest, src)的功能是将src指向的字符串(包括结尾的\0)复制到dest指向的内存空间。
char* my_strcpy(char* dest, const char* src) { if (dest == NULL || src == NULL) { // 双指针检查 // 处理错误,例如返回NULL或使用断言。这里简单返回dest。 return dest; } char* ret = dest; // 保存目标字符串的起始地址,用于返回 while ((*dest++ = *src++) != '\0') { // 经典“双飞燕”写法 ; // 空循环体 } return ret; // 返回目标字符串的起始地址,以支持链式调用 }核心逻辑拆解:
- 参数顺序:
dest在前,src在后,这是标准库的约定,遵循“目标 <- 源”的数据流向直觉。 - 返回值:返回
dest的原始值。这允许链式调用,例如strcpy(a, strcpy(b, c)),虽然这种写法可读性不高,但体现了API设计的灵活性。 - 那个神奇的while循环:
(*dest++ = *src++) != '\0'这行代码是C指针操作的浓缩精华。*src++:先取src指向的字符,然后将src指针自增。*dest++ = ...:将取出的字符赋值给dest指向的位置,然后将dest指针自增。(...) != '\0':判断刚才赋值的字符是不是\0。如果不是,循环继续;如果是,循环结束,并且\0已经被复制过去了。- 这个写法极其紧凑高效,是C语言中常见的惯用法。
最重要的安全警示:strcpy本身不检查dest指向的内存空间是否足以容纳src字符串。如果dest空间不足,就会发生缓冲区溢出,覆盖相邻内存,导致数据损坏、程序崩溃,甚至被利用执行恶意代码。这是它最危险的地方。
char buf[5]; my_strcpy(buf, "Hello, World!"); // 灾难!buf只有5字节,却要复制14字节(含\0)。因此,在实际项目中,绝对禁止使用裸的strcpy。必须使用更安全的替代品:
strncpy(dest, src, n):最多复制n个字符。但它的行为很怪异:如果src长度小于n,它会用\0填充dest剩余部分;如果src长度大于等于n,它不会在结尾添加\0!这常常导致没有终止符的字符串,引发后续错误。snprintf(dest, size, "%s", src):这是最推荐的方式之一,size指定了dest缓冲区的大小,会确保不会溢出,并且总是以\0结尾。- C11标准引入了
strcpy_s,但可移植性不佳。
模拟实现strcpy的价值,就在于让你亲身体验到这种“信任调用者”的原始接口所带来的风险,从而在以后编码时,对内存边界保持敬畏。
3.2my_strcat:字符串的“连接者”
strcat(dest, src)的功能是将src字符串追加到dest字符串的末尾(覆盖dest原有的\0),并在新字符串的末尾添加\0。
char* my_strcat(char* dest, const char* src) { if (dest == NULL || src == NULL) { return dest; } char* ret = dest; // 第一步:找到dest字符串的结尾(即'\0'的位置) while (*dest != '\0') { dest++; } // 第二步:从dest的末尾开始,执行strcpy操作 while ((*dest++ = *src++) != '\0') { ; } return ret; }实现逻辑解析:my_strcat可以看作是my_strlen和my_strcpy的组合。第一步的循环,其作用等同于在dest上执行strlen,目的是将dest指针移动到其原有字符串的终止符\0处。第二步的循环,就是标准的strcpy逻辑,从dest的当前末尾位置开始,将src复制过来。
安全陷阱加倍:strcat的安全问题比strcpy更隐蔽。它需要满足:strlen(dest) + strlen(src) + 1 <= dest缓冲区总大小如果dest的剩余空间不足以容纳src,同样会发生缓冲区溢出。由于它涉及两个字符串长度的计算,在动态拼接字符串时更容易出错。
一个常见的错误用法:
char path[100] = "/home/user/"; my_strcat(path, "docs/"); my_strcat(path, "file.txt"); // 看起来没问题 my_strcat(path, very_long_filename); // 如果文件名很长,这里就会溢出!安全建议:使用snprintf进行拼接是更安全的选择,它能直观地控制总长度。
snprintf(final_path, sizeof(final_path), "%s%s%s", base_path, sub_dir, filename);4. 比较与查找:模拟实现my_strcmp与my_strstr
如果说strcpy和strcat关乎内存安全,那么strcmp和strstr则更侧重于逻辑与算法。
4.1my_strcmp:字符串的“裁判”
strcmp(str1, str2)用于比较两个字符串的大小(按字典序,即ASCII码顺序)。
- 返回值 < 0:表示
str1小于str2。 - 返回值 = 0:表示
str1等于str2。 - 返回值 > 0:表示
str1大于str2。
int my_strcmp(const char* str1, const char* str2) { if (str1 == NULL || str2 == NULL) { // 处理空指针,可以约定NULL小于任何字符串,或直接断言失败。 // 这里简单返回一个特定值(非标准),实际中应明确处理逻辑。 if (str1 == str2) return 0; // 两个都是NULL,视为相等 return (str1 == NULL) ? -1 : 1; // 一个为NULL,规定NULL较小 } // 核心比较逻辑 while (*str1 && (*str1 == *str2)) { str1++; str2++; } // 循环结束有三种情况: // 1. *str1 == '\0' && *str2 == '\0' -> 两字符串完全相等,返回0 // 2. *str1 != *str2 -> 在某个位置字符不同,返回它们的差值 // 3. *str1 == '\0' && *str2 != '\0' -> str1是str2的前缀,str1小,返回负值(*str1 - *str2) // 4. *str1 != '\0' && *str2 == '\0' -> str2是str1的前缀,str1大,返回正值(*str1 - *str2) // 后三种情况都可以用 *(unsigned char*)str1 - *(unsigned char*)str2 统一处理 return *(const unsigned char*)str1 - *(const unsigned char*)str2; }为什么使用unsigned char*强制转换?这是一个至关重要的细节。char类型在C标准中可能是有符号的(范围-128~127),也可能是无符号的。如果使用有符号的char进行比较,当字符串中包含ASCII码大于127的字符(即扩展ASCII字符或UTF-8多字节字符的一部分)时,这些字符会被当作负数处理。例如,字符0xA1(有符号时为-95)和0x41(‘A’, 65)比较,-95 < 65,这不符合我们按字节值无符号比较的预期。强制转换为unsigned char后,所有字节值都在0~255范围内,比较结果就符合字典序的直观预期。标准库的strcmp正是这样实现的。
strcmp的用途:它不仅是判断字符串是否相等(if(strcmp(a,b)==0)),更是排序(qsort的比较函数)、查找(bsearch)等算法的基础。理解它返回正负值的意义,对于编写正确的比较回调函数至关重要。
4.2my_strstr:字符串中的“侦探”
strstr(haystack, needle)是经典的子串查找函数,在“干草堆”(haystack)中寻找“针”(needle)。它返回在haystack中第一次出现needle子串的位置指针,如果没找到则返回NULL。
实现strstr有多种算法,从朴素的暴力匹配到高效的KMP算法。这里我们先实现最直观的暴力匹配法,再探讨其优化思路。
4.2.1 暴力匹配法(Brute-Force)
char* my_strstr_brute(const char* haystack, const char* needle) { if (haystack == NULL || needle == NULL) { return NULL; } if (*needle == '\0') { // 空字符串是任何字符串的子串 return (char*)haystack; } const char* h_ptr; const char* n_ptr; for (const char* start = haystack; *start != '\0'; ++start) { h_ptr = start; n_ptr = needle; // 从start位置开始,逐个字符比较 while (*n_ptr != '\0' && *h_ptr != '\0' && *h_ptr == *n_ptr) { ++h_ptr; ++n_ptr; } // 如果needle的所有字符都匹配完了(*n_ptr == '\0'),说明找到了 if (*n_ptr == '\0') { return (char*)start; } // 如果haystack先到头了(*h_ptr == '\0'),说明剩余长度不够,可以提前结束外层循环 // 这里为了逻辑清晰,让循环自然继续,但可以优化。 } return NULL; // 遍历完所有可能的起始位置都没找到 }算法逻辑:从haystack的每一个字符位置开始,尝试与needle进行匹配。如果匹配失败,就将haystack的匹配起始位置向后移动一位,重新开始匹配。其时间复杂度在最坏情况下是O(m*n),其中m和n分别是haystack和needle的长度。当两者都很长时,效率较低。
4.2.2 效率优化与KMP算法思想
暴力法的低效在于,当某次匹配失败时,它只是简单地将起始位置向后挪一位,忽略了本次匹配过程中已经获得的部分匹配信息。例如,在haystack = "ABABABC"中找needle = "ABABC",当匹配到第5个字符失败时(‘A’ != ‘C’),暴力法会从haystack的第2个字符‘B’重新开始。但实际上,由于needle本身的前缀“ABAB”有公共前后缀“AB”,我们可以利用这个信息,将needle直接向右滑动多位,而不是只移动一位。
这就是KMP算法的核心思想。KMP算法通过预处理needle,得到一个next数组(或称部分匹配表),该数组记录了needle每个位置匹配失败时,needle指针应该回退到的位置。这样,haystack的指针i就永不回退,从而将时间复杂度降低到O(m+n)。
由于KMP算法的实现涉及next数组的构建,代码相对复杂,这里给出其核心框架:
// 构建next数组 (预处理needle) void get_next(const char* needle, int next[]) { int len = strlen(needle); next[0] = -1; int i = 0, j = -1; while (i < len) { if (j == -1 || needle[i] == needle[j]) { ++i; ++j; next[i] = j; } else { j = next[j]; } } } // KMP查找 char* my_strstr_kmp(const char* haystack, const char* needle) { if (!haystack || !needle) return NULL; if (*needle == '\0') return (char*)haystack; int n_len = strlen(needle); int h_len = strlen(haystack); // 注意:这里用了strlen,实际可以边匹配边判断结尾 int* next = (int*)malloc(sizeof(int) * (n_len + 1)); get_next(needle, next); int i = 0, j = 0; // i遍历haystack, j遍历needle while (i < h_len && j < n_len) { if (j == -1 || haystack[i] == needle[j]) { ++i; ++j; } else { j = next[j]; // needle指针根据next数组回退 } } free(next); if (j == n_len) { return (char*)(haystack + i - j); // 找到,返回起始位置 } return NULL; }实操心得:
- 何时选择哪种算法?对于日常编程和面试,掌握暴力匹配法并理解其缺陷就足够了。KMP算法通常在算法竞赛、特定性能要求的文本搜索库(如grep的早期版本)或面试中要求手写时才会用到。
- 标准库的实现:现代C标准库(如Glibc)中的
strstr实现远比我们想象的复杂。它可能针对短模式串、长模式串、以及当前CPU架构(支持SIMD指令集如SSE4.2)进行了高度优化,甚至可能混合了多种算法(如Two-Way算法),以达到在绝大多数实际场景下的最优性能。自己实现的版本在性能上通常无法与高度优化的库函数相比。 - 模拟实现的价值:我们模拟
strstr,目的不是为了取代标准库,而是为了深入理解字符串匹配这个计算机科学中的经典问题,学习如何从朴素解法出发,分析其瓶颈,并引入更高效的算法思想(如KMP的next数组)。这个过程锻炼的是算法设计和分析能力。
5. 进阶思考与边界陷阱
在亲手实现了这些基础函数后,我们有必要站在更高的视角审视它们,并探讨一些常见的“坑”。
5.1 内存重叠问题:strcpy与memmove的启示
考虑以下代码:
char str[20] = "hello, world"; my_strcpy(str + 7, str); // 试图从str的起始位置复制到str+7的位置我们的my_strcpy会得到正确结果吗?很可能不会。因为当源(src)和目标(dest)内存区域重叠时,且dest > src,在复制过程中,src指向的内容可能在我们读取它之前就被覆盖了。例如,在复制第一个字符h到str[7]后,str[0]可能已经被后续操作修改了(取决于具体实现和编译器)。
标准库的strcpy规定,当源和目标内存重叠时,其行为是未定义的。这意味着什么结果都可能发生。为了解决重叠内存的复制问题,C标准库提供了memmove函数。memmove会先判断内存重叠的方向,如果dest在src之后,它会选择从后向前复制,以避免数据被破坏。
模拟实现一个简单的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]; } } // 如果地址相等,什么都不用做 return dest; }这个例子告诉我们,在处理内存操作时,必须时刻警惕源和目标的关系。strcpy和strcat之所以危险,不仅因为可能溢出,也因为在某些重叠场景下行为不可预测。
5.2 “长度受限”版本:strncpy,strncat,strncmp
由于strcpy、strcat的不安全性,标准库提供了它们的“长度受限”版本,旨在让程序员指定一个最大操作长度n,以防止溢出。
char *strncpy(char *dest, const char *src, size_t n);- 行为:最多从
src复制n个字符到dest。 - 陷阱1:如果
src的前n个字符里没有\0,那么dest将不会以\0结尾!这会产生一个非终止的字符串,后续用strlen或printf等函数操作dest会导致越界访问。 - 陷阱2:如果
src的长度小于n,strncpy会用\0填充dest剩余的空间。这听起来不错,但如果你用它来复制一个很短的字符串到一个很大的缓冲区,它会写入大量不必要的\0,性能低下。 - 结论:
strncpy最初设计用于处理固定长度的字段(如UNIX文件系统中的目录项),它并非一个安全的strcpy替代品。如果你想要一个安全的字符串复制,应该使用snprintf(dest, n, "%s", src)。
- 行为:最多从
char *strncat(char *dest, const char *src, size_t n);- 行为:最多从
src追加n个字符到dest末尾,并总是在新字符串的末尾添加一个\0。 - 注意:参数
n是限制从src中取用的字符数,而不是dest剩余缓冲区的大小。你需要确保dest有足够的空间容纳strlen(dest) + n + 1个字符。它比strncpy行为更友好,但依然需要程序员自己计算缓冲区大小。
- 行为:最多从
int strncmp(const char *str1, const char *str2, size_t n);- 行为:最多比较前
n个字符。如果在前n个字符内就分出大小或遇到\0,则提前返回结果。 - 这是相对安全的,因为它只读取限定长度的内存,常用于比较字符串前缀。
- 行为:最多比较前
模拟实现这些函数是很好的练习,但更重要的是理解它们设计上的差异和潜在陷阱。在现代C编程中,对于缓冲区操作,使用snprintf和明确的大小管理(如sizeof(dest))是更受推崇的安全实践。
5.3 性能与可读性的权衡
我们实现的函数都是最基础的版本。在性能关键的场景下,库函数通常会使用更高效的方法:
- 字长优化:现代CPU处理一个
int或long(比如4或8字节)和处理一个char(1字节)速度差不多。因此,像strlen和strcpy的优化版本,会先检查指针是否对齐到字边界,然后一次读取和比较一个机器字(word),而不是一个字节。这可以大幅提升长字符串的处理速度。 - 编译器内建函数:像GCC这样的编译器,会将
strlen、memcpy等常用函数识别为内建函数(Built-in),并可能生成极其优化的内联汇编代码,甚至利用SIMD指令进行并行处理。
对于我们自己的实现,在大多数情况下,清晰、正确比极致的性能更重要。除非你正在编写底层库或对性能有极端要求,否则应优先选择可读性高、易于维护的代码。模拟实现的意义在于理解原理,而非替代优化后的标准库。
6. 综合实战:构建一个自定义的字符串工具模块
理解了单个函数的实现后,我们可以尝试将它们组织起来,形成一个简单但功能完整的字符串工具模块。这涉及到头文件设计、函数声明、以及一些增强功能的实现。
6.1 模块头文件设计 (mystring.h)
一个好的模块应该提供清晰、安全的接口。
#ifndef MY_STRING_H #define MY_STRING_H #include <stddef.h> // 为了使用 size_t // 1. 基础函数模拟 size_t my_strlen(const char* str); char* my_strcpy(char* dest, const char* src); char* my_strcat(char* dest, const char* src); int my_strcmp(const char* str1, const char* str2); char* my_strstr(const char* haystack, const char* needle); // 2. “安全”版本(推荐使用) // 安全拷贝,保证dest总是以\0结尾,返回实际写入的字符数(不含\0) size_t my_strcpy_safe(char* dest, size_t dest_size, const char* src); // 安全拼接,保证dest总是以\0结尾,返回实际写入的字符数(不含\0) size_t my_strcat_safe(char* dest, size_t dest_size, const char* src); // 3. 实用扩展函数 // 字符串反转(原地) char* my_strrev(char* str); // 查找字符首次出现位置 char* my_strchr(const char* str, int ch); // 查找字符最后一次出现位置 char* my_strrchr(const char* str, int ch); // 计算字符串中特定字符出现的次数 int my_strcount(const char* str, char ch); #endif // MY_STRING_H6.2 实现安全版本函数
让我们实现头文件中声明的两个安全函数,它们要求调用者传入目标缓冲区的大小。
// my_strcpy_safe 实现 size_t my_strcpy_safe(char* dest, size_t dest_size, const char* src) { if (dest == NULL || src == NULL || dest_size == 0) { if (dest != NULL && dest_size > 0) { dest[0] = '\0'; // 确保目标缓冲区是空字符串 } return 0; } size_t i; for (i = 0; i < dest_size - 1 && src[i] != '\0'; ++i) { dest[i] = src[i]; } // 循环结束后,i要么等于dest_size-1,要么src[i]是\0 dest[i] = '\0'; // 无论如何,确保字符串以\0结尾 // 如果因为缓冲区满而停止,可以返回一个特殊值或截断后的长度。 // 这里返回已复制的字符数(不含\0) return i; } // my_strcat_safe 实现 size_t my_strcat_safe(char* dest, size_t dest_size, const char* src) { if (dest == NULL || src == NULL || dest_size == 0) { return 0; } // 先找到dest当前的结尾 size_t dest_len = 0; while (dest_len < dest_size && dest[dest_len] != '\0') { dest_len++; } if (dest_len >= dest_size) { // dest已经不是一个有效的以\0结尾的字符串,或者缓冲区已满 // 为了安全,我们在末尾添加一个\0(如果空间允许) if (dest_size > 0) { dest[dest_size - 1] = '\0'; } return dest_len; // 返回当前长度(可能是错误的) } // dest_len 现在是dest字符串的长度,dest[dest_len]是\0 // 剩余空间 = dest_size - dest_len - 1 (留给新的\0) size_t available = dest_size - dest_len - 1; size_t j; for (j = 0; j < available && src[j] != '\0'; ++j) { dest[dest_len + j] = src[j]; } dest[dest_len + j] = '\0'; return dest_len + j; // 返回新字符串的总长度 }安全版本的核心思想:
- 接受缓冲区大小参数:这是最关键的一步,让函数知道操作的安全边界。
- 始终保证终止符:无论什么情况,都要确保在操作后,目标缓冲区内的字符串以
\0结尾,防止产生非终止字符串。 - 处理边界条件:仔细处理空指针、零大小缓冲区、已满缓冲区等边缘情况。
- 提供有意义的返回值:返回实际写入的字符数或新的字符串长度,方便调用者判断是否发生了截断。
6.3 实现一个扩展函数:my_strrev
字符串反转是一个常见的面试题和练习,它很好地结合了指针操作和数组下标操作。
char* my_strrev(char* str) { if (str == NULL || *str == '\0') { return str; } char* start = str; char* end = str; // 让end指针指向字符串的最后一个字符(不是\0) while (*(end + 1) != '\0') { end++; } // 双指针法,从两端向中间交换字符 while (start < end) { char temp = *start; *start = *end; *end = temp; start++; end--; } return str; // 支持链式调用 }另一种使用下标的方法:
char* my_strrev_index(char* str) { if (!str) return NULL; int len = my_strlen(str); for (int i = 0; i < len / 2; i++) { char temp = str[i]; str[i] = str[len - 1 - i]; str[len - 1 - i] = temp; } return str; }注意事项:这个函数是原地修改,会改变原始字符串。如果希望保留原字符串,需要先分配新内存进行复制再反转。同时,它对于包含多字节字符(如UTF-8编码的中文)的字符串是错误的,因为多字节字符的字节顺序不能随意打乱。它只适用于单字节字符集(如ASCII)。
通过构建这样一个模块,你将不仅仅是在实现孤立的函数,而是在学习如何设计API、如何考虑安全性、如何组织代码。这才是从“模拟实现”走向“工程实践”的重要一步。