ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

从黑盒到白盒:亲手模拟strcpy,深入理解C语言指针与内存操作

从黑盒到白盒:亲手模拟strcpy,深入理解C语言指针与内存操作 1. 从“黑盒”到“白盒”为什么要亲手模拟一个strcpy如果你写过C语言strcpy这个函数对你来说肯定不陌生。它太基础了基础到我们常常把它当作一个理所当然的“黑盒”来用传进去一个源字符串和一个目标地址它就把内容复制过去。在初学阶段老师可能还会强调一下它的危险性——目标缓冲区必须足够大否则就是经典的缓冲区溢出漏洞。但除此之外呢我们很少去深究这个看似简单的函数内部到底是怎么运转的。这就是“动手模拟”的意义所在。它不是一个为了造轮子而造轮子的练习而是一次将“黑盒”变为“白盒”的深度探索。通过亲手实现一个my_strcpy你会被迫去思考那些被标准库封装起来的底层细节指针是如何一步步移动的字符串的终止标志\0是如何被处理的在复制过程中内存的字节是如何被搬运的这个过程远比调用十次、百次strcpy更能让你理解C语言中指针和内存操作的精髓。更进一步说strcpy是C语言字符串操作家族的基石。理解了它的实现strcat字符串连接、strcmp字符串比较等函数的实现思路也就触类旁通了。它们核心的逻辑都是对指针的遍历和操作。当你自己实现一遍后再去看man手册里关于这些函数的描述会有一种“原来如此”的透彻感。这不仅仅是掌握了一个函数而是打通了字符串处理这一类问题的任督二脉。从工程实践的角度看理解strcpy的底层行为是写出健壮、安全代码的前提。你知道为什么strcpy(dest, src)可能会覆盖掉dest之后的内存吗因为你清楚地模拟过指针越界的过程。你知道为什么有些“安全”版本的strncpy函数行为诡异比如不保证目标字符串以\0结尾吗因为你在实现自己的版本时已经考虑过各种边界条件。这种从内部机理出发的理解是防范未来代码中潜在漏洞的最有力武器。所以让我们暂时忘掉#include string.h从零开始用最原始的指针操作来揭开strcpy的神秘面纱。这个过程会比你想象的有趣也更有收获。2. 庖丁解牛标准strcpy的函数原型与行为约定在动手造轮子之前我们必须先彻底搞清楚原版轮子的规格说明书。strcpy的函数原型就是这份最核心的规格书。char *strcpy(char *dest, const char *src);这个声明看似简单却蕴含了C语言设计哲学的多个关键点。我们来逐一拆解2.1 参数分析dest与src的“角色”与“约束”第一个参数char *dest这是目标destination字符串的指针。它指向一块我们已经分配好的、连续的内存空间strcpy的任务就是把数据复制到这里。注意这里传入的是指针函数内部对dest的修改移动指针会影响外部的那个指针副本吗不会因为C语言是值传递但通过这个指针我们可以修改它所指向的内存内容。这是理解后续所有操作的基础。第二个参数const char *src这是源source字符串的指针。const关键字在这里至关重要它是一个对函数实现者和调用者的双重承诺。对调用者而言它承诺“我传给你的字符串你不会去修改它”增强了代码的可读性和安全性。对函数实现者也就是我们而言它是一个强制约束在函数体内任何试图通过src指针修改其指向内存内容的操作都会导致编译器报错。这保证了源字符串在复制过程中的“只读”属性。2.2 返回值解析为什么返回char*函数的返回值是char *并且通常返回的就是传入的dest指针。这个设计初看有些多余但它实现了“链式调用”function chaining的可能性。例如你可以这样写printf(%s\n, strcpy(buffer, Hello));这里strcpy执行后返回了buffer这个返回值直接作为printf的参数。虽然这种写法在实际项目中需谨慎使用可能影响可读性但它体现了C语言简洁、高效的设计理念。在我们的模拟实现中也必须遵守这个约定在函数末尾返回目标指针。2.3 核心行为约定复制规则与终止条件这是strcpy的灵魂所在我们的模拟实现必须100%复现这些行为复制内容将src指向的字符串包括终止的空字符\0复制到dest指向的位置。复制顺序从内存的低地址向高地址逐个字节对于char类型就是一个字节进行复制。终止条件复制过程持续进行直到遇到src中的空字符\0并且将这个\0也复制到dest中。复制完\0后整个复制过程立即停止。内存重叠标准并未定义当src和dest所指向的内存区域发生重叠overlap时的行为。这意味着如果dest的地址在src和srcstrlen(src)之间结果是未定义的Undefined Behavior, UB。一个健壮的库实现可能会处理这种情况如使用memmove但标准的strcpy不保证。我们的基础模拟版本也可以先不考虑这种情况。缓冲区大小调用者必须保证dest指向的空间足以容纳src字符串包括\0。这是调用者的责任函数内部不做任何检查。这正是strcpy被认为“不安全”的根源。理解这些约定后我们就可以用最直观的代码来描述其行为了一个从src到dest的、以\0为终点的字节搬运循环。接下来我们就将这个循环用代码实现出来。3. 第一版实现最直观的“指针移动法”让我们从最符合直觉的方式开始实现。既然是要逐个字符复制直到遇到\0那么一个while循环是最直接的选择。这个版本我将它称为“指针移动法”因为它直接操作传入的指针参数。3.1 代码实现与逐行解读char *my_strcpy_v1(char *dest, const char *src) { // 步骤1保存目标指针的起始位置用于最终返回 char *origin_dest dest; // 步骤2核心复制循环 while (*src ! \0) { // 只要源字符不是字符串结束符 *dest *src; // 将源字符赋值给目标位置 dest; // 目标指针向后移动一个字符位置 src; // 源指针向后移动一个字符位置 } // 步骤3复制字符串终止符 \0 *dest \0; // 步骤4返回目标字符串的起始地址 return origin_dest; }现在我们来拆解这个循环的每一个细节这比代码本身更重要char *origin_dest dest;这是实现返回值约定的关键技巧。dest是一个指针变量传入函数的是它的值一个内存地址。在循环中我们需要移动这个指针dest来遍历目标缓冲区。如果我们移动了dest函数结束时它指向的就是字符串的末尾\0的位置而不是起始位置。为了能返回起始地址我们在一开始就用另一个变量origin_dest“锚定”这个起始位置。整个函数中origin_dest保持不变而dest作为“工作指针”不断移动。while (*src ! \0)这是循环的条件。*src是“解引用”操作它获取src指针当前所指向的内存中的字符值。只要这个值不是空字符\0循环就继续。这里有一个非常重要的点这个条件判断发生在每次复制之前。这意味着如果src一开始就指向\0即空字符串循环体一次都不会执行直接跳到后面复制\0的步骤。*dest *src;这是复制的核心语句。把src指向的字符值赋值给dest指向的内存位置。注意这里操作的是字符值不是指针。dest;和src;这是指针算术。dest表示将dest指针向前移动指向下一个char类型元素的内存地址。因为char占1字节所以这里dest的值内存地址实际增加了1。src同理。这两行代码驱动着指针遍历整个字符串。循环结束后*dest \0;当while循环因为*src \0而退出时我们已经复制了\0之前的所有字符。但请注意此时src指向的是\0这个字符本身而我们还没有执行*dest *src;来复制它。所以我们需要在循环体外手动将\0复制到dest的当前位置。此时dest指针正好指向目标缓冲区中该放置结束符的位置。return origin_dest;返回我们一开始保存的起始地址满足函数原型约定。3.2 逻辑推演与边界测试让我们在脑子里或者画在纸上模拟一下这个过程假设src指向字符串Hi(内存布局:H, i, \0)dest指向一块足够大的空缓冲区。初始origin_dest dest*src是H。第一轮循环H ! \0成立执行*dest H然后dest,src。现在dest指向目标第二个位置src指向源字符串的i。第二轮循环i ! \0成立执行*dest i然后dest,src。现在dest指向目标第三个位置src指向源字符串的\0。第三轮循环判断*src现在是\0条件*src ! \0为假循环结束。循环外执行*dest \0将结束符复制到目标缓冲区的第三个位置。返回origin_dest。最终目标缓冲区包含了H, i, \0复制完成。对于空字符串src一开始就指向\0循环直接跳过循环外执行*dest \0结果也是正确的。注意一个常见的思维误区有人可能会想为什么不在循环条件里写成while ((*dest *src) ! \0)这种更紧凑的形式这确实是更经典的写法我们会在下一个版本看到。但在第一版中我们刻意拆解开是为了让“判断”、“赋值”、“指针后移”这三个步骤清晰可见这对于理解底层机制至关重要。在初学阶段清晰性比简洁性更重要。4. 第二版实现追求极致的“表达式浓缩法”如果你看过一些经典的C语言库源码或者资深工程师的代码你会发现他们很少写像我们第一版那样“啰嗦”的strcpy。他们追求的是在保证正确性的前提下极致的简洁和效率。这就是我们的第二版实现我称之为“表达式浓缩法”。它不仅是代码风格的变化更体现了对C语言表达式和运算符优先级的深刻理解。4.1 经典的单行实现char *my_strcpy_v2(char *dest, const char *src) { char *origin_dest dest; while ((*dest *src) ! \0) ; // 空循环体 return origin_dest; }甚至连保存起始地址的变量都可以省去直接操作参数副本但为了清晰我们保留origin_dest。这段代码极其紧凑信息密度很高。我们来彻底解析这行核心的while条件(*dest *src) ! \0这行代码同时完成了赋值、指针后移和条件判断三件事。它的执行顺序由C语言的运算符优先级和结合性决定子表达式*dest和*src后缀的优先级高于解引用*。所以dest先被求值但它的值是dest递增之前的值即当前地址。然后这个值旧地址被用来进行解引用操作*。最后dest指针本身的值被加1指向下一个位置。src同理。简单记*p的意思是“取得p当前指向的值然后让p指向下一个位置”。赋值操作将*src源字符值赋给*dest目标内存位置。赋值表达式本身也有一个值它的值就是被赋值后的左操作数的值也就是赋值后*dest旧地址处的值的值。不等于比较! \0将上一步赋值表达式的值即刚刚复制到dest的那个字符与\0进行比较。如果这个字符不是\0则循环条件为真继续下一轮如果这个字符就是\0则条件为假循环终止。4.2 执行过程推演还是以src Hi为例第一轮*src得到Hsrc后移*dest H将H赋值dest后移赋值表达式的值为H。H ! \0为真循环继续。第二轮*src得到isrc后移*dest i将i赋值dest后移赋值表达式的值为i。i ! \0为真循环继续。第三轮*src得到\0src后移*dest \0将\0赋值dest后移赋值表达式的值为\0。\0 ! \0为假循环终止。妙处在于当复制到\0时赋值操作已经完成\0已经被复制并且循环条件判断为假使得循环自然结束不需要在循环体外再单独复制\0。代码逻辑和第一版完全等价但更加精炼。4.3 空循环体与代码风格注意while后面直接跟了一个分号;这是一个“空语句”null statement作为循环体。它表示“循环条件中已经完成了所有工作循环体不需要做任何额外操作”。这是一种常见的C语言 idiom惯用法。有些编码规范建议将空语句单独放在一行或者用{ }括起来以避免歧义但这种写法在系统编程中非常普遍。经验之谈简洁与清晰的权衡。第二版代码是C语言技巧性的体现深受有经验的开发者喜爱。但对于团队协作或代码可维护性要求高的项目第一版那样逻辑清晰的写法可能更受欢迎尤其是在给初学者阅读或调试时。知道如何写第二版证明你理解了语言特性懂得在何时使用第一版则体现了你的工程素养。5. 第三版实现贴近硬件的“地址偏移法”前两个版本我们都聚焦在指针本身的操作上。还有一种理解角度是把指针看作一个内存地址通过数组下标的方式来进行访问。这就是我们的第三版实现“地址偏移法”。它虽然不常见于strcpy的标准实现但对于理解指针和数组的等价性以及从另一个视角看待内存访问非常有帮助。5.1 使用下标遍历char *my_strcpy_v3(char *dest, const char *src) { int i 0; // 循环复制直到遇到src[i]为结束符 do { dest[i] src[i]; } while (src[i] ! \0); // 注意判断的是刚刚复制过去的那个字符 return dest; // 因为dest指针本身没有移动所以可以直接返回 }这个版本看起来和指针版本很不一样它使用了一个整数索引i。dest[i]在编译器看来完全等价于*(dest i)。也就是说它先计算dest向后偏移i个char单位后的地址然后解引用。src[i]同理。5.2 do...while循环的玄机这里我特意使用了do...while循环而不是while。为什么如果使用while(src[i] ! \0)那么循环开始前会先判断src[0]。如果src是空字符串src[0]就是\0循环体一次都不会执行dest[0]就不会被赋值为\0导致错误。使用do...while循环则先执行一次循环体再判断条件。这样即使src是空字符串也会先执行dest[0] src[0]即dest[0] \0然后判断src[0] ! \0为假循环结束。这完美地处理了空字符串的情况并且保证\0被复制。这个版本的逻辑是复制一个字符然后判断这个刚被复制的字符是不是\0如果不是就继续。这和前两版“判断下一个字符是不是\0不是就复制”的逻辑在结果上是等价的但执行顺序略有不同。5.3 版本对比与思维转换特性指针移动法 (V1)表达式浓缩法 (V2)地址偏移法 (V3)核心思想显式移动指针逻辑步骤分离利用表达式副作用高度浓缩将指针视为基地址用索引偏移循环类型while(前测试)while(前测试)do...while(后测试)\0处理循环外显式复制在循环条件中复制并判断在循环体中复制后判断指针状态dest/src最终指向\0后dest/src最终指向\0后dest/src始终指向起始位置代码风格清晰易懂适合教学简洁高效常见于源码另类视角强调指针/数组等价性空字符串正确处理正确处理正确处理 (因do-while)通过实现这个版本你会更深刻地认识到在C语言中array[index]和*(array index)本质上是同一回事。指针的加法运算就是计算偏移后的地址。这种理解对于后续学习更复杂的数据结构如动态数组、字符串处理函数strchr,strstr等至关重要。踩坑点数组越界的视角。用下标法时我们更容易直观地想到“i不能超过缓冲区大小”。这提醒我们无论哪种实现缓冲区溢出的危险始终存在。dest[i]的访问越界和*(dest i)的越界是同一个问题。6. 深入测试验证我们的模拟实现代码写完了但绝不能相信它一次就能完美工作。我们需要设计一套全面的测试用例像“铁面判官”一样审视我们的my_strcpy。测试不仅要覆盖正常情况更要主动去触碰那些边界和极端情况。6.1 基础功能测试首先我们搭建一个简单的测试框架#include stdio.h #include string.h // 用于和标准库函数对比 // 这里插入我们实现的 my_strcpy 函数 (以V2版为例) char *my_strcpy_v2(char *dest, const char *src) { char *origin_dest dest; while ((*dest *src) ! \0) ; return origin_dest; } void test_case(const char *test_name, const char *src) { char std_dest[100] {0}; // 标准库用的缓冲区 char my_dest[100] {0}; // 我们函数用的缓冲区 // 使用标准库函数 strcpy(std_dest, src); // 使用我们的函数 my_strcpy_v2(my_dest, src); // 比较结果 if (strcmp(std_dest, my_dest) 0) { printf([PASS] %s: \%s\\n, test_name, src); } else { printf([FAIL] %s\n, test_name); printf( Expected: \%s\\n, std_dest); printf( Got: \%s\\n, my_dest); } } int main() { printf( 开始测试 my_strcpy \n); // 测试1: 普通字符串 test_case(普通ASCII字符串, Hello, World!); // 测试2: 空字符串 test_case(空字符串, ); // 测试3: 单个字符 test_case(单个字符, A); // 测试4: 长字符串 test_case(长字符串, This is a relatively longer string to test the function.); // 测试5: 包含空格、标点 test_case(带空格标点, Hello, Dr. Smith! How are you?); printf( 测试结束 \n); return 0; }这些测试能确保我们的函数在常规输入下行为和标准库一致。6.2 边界与陷阱测试基础测试通过只是第一步下面这些测试才是真正考验代码健壮性的地方。我们需要单独设计测试函数因为它们可能引发程序崩溃不适合放在自动对比框架里。void test_boundary() { printf(\n 边界与陷阱测试 \n); // 测试6: 目标缓冲区恰好够大危险边缘 char exact_buffer[6]; // 刚好能容纳 Hello \0 const char *src_exact Hello; my_strcpy_v2(exact_buffer, src_exact); printf(测试‘恰好够大’缓冲区: dest\%s\ (应等于\Hello\)\n, exact_buffer); // 手动检查是否复制正确且没有越界写入这需要内存检查工具如Valgrind // 测试7: 源字符串是字面量目标缓冲区足够大 char large_dest[100]; my_strcpy_v2(large_dest, String Literal); printf(测试字面量源字符串: dest\%s\\n, large_dest); // 测试8: 重叠内存测试 (未定义行为观察现象) char overlap[50] abcdefghijk; printf(重叠内存测试前: %s\n, overlap); // 尝试将 overlap3 复制到 overlap。这是未定义行为 // my_strcpy_v2(overlap, overlap 3); // 注释掉因为结果不可预测可能死循环或错误 // printf(重叠内存测试后: %s\n, overlap); printf(注意标准strcpy不支持内存重叠此测试已跳过。\n); }6.3 内存安全与未定义行为UB警示上面的测试中我们刻意避开了“目标缓冲区太小”这个最经典的陷阱。为什么因为一旦我们传入一个小于源字符串长度的目标缓冲区我们的my_strcpy和标准的strcpy一样会毫不犹豫地继续写入覆盖缓冲区之后的内存。这可能导致覆盖其他变量数据引发程序逻辑错误。破坏函数调用栈如返回地址导致程序崩溃或被利用进行攻击缓冲区溢出攻击。属于C语言标准中的“未定义行为”Undefined Behavior程序可以做任何事情包括看似正常地工作但埋下深坑。这就是strcpy被诟病“不安全”的根本原因。我们的模拟实现完美地复现了这个“特性”——或者说这个“缺陷”。它忠实地反映了标准库函数的行为信任调用者不做任何边界检查。重要心得测试的维度。测试一个函数尤其是底层函数不能只测“它应该做什么”更要测“当输入不符合约定时它会做什么”。我们的测试表明my_strcpy在合法输入下行为正确在非法输入缓冲区不足下它和原版一样危险。这提醒我们在使用任何不检查边界的字符串函数时缓冲区大小的计算是调用者的首要责任。在实际项目中我们应优先使用strncpy、snprintf或更安全的字符串库。7. 从模拟到思考strcpy的缺陷与安全替代方案通过亲手实现我们不仅知道了strcpy怎么工作更真切地感受到了它的“锋利”与“危险”。它是一把没有护手的刀用好了效率极高用错了伤人伤己。在现代软件开发中尤其是对安全性要求高的领域直接使用strcpy往往被视为不良实践。那么我们该如何应对7.1 strcpy的核心缺陷绝对的信任strcpy的设计哲学是“信任调用者”。它假设dest指向的空间一定足够大。这个假设在简单的、受控的上下文中可能成立但在复杂的、处理外部输入如网络数据、用户输入、文件内容的系统中几乎无法保证。一旦假设被违反缓冲区溢出就发生了这是无数安全漏洞的根源。7.2 经典“安全”替代strncpy及其陷阱最常被提及的替代者是strncpy。它的原型是char *strncpy(char *dest, const char *src, size_t n);它多了一个参数n用于指定最多复制多少个字符到dest。看起来安全了对吗大错特错strncpy的行为非常反直觉是著名的“坑王”如果src的长度小于n它会将src的所有字符包括\0复制过去然后用\0填充dest中剩余的空间直到写满n个字符。这常常导致性能浪费。如果src的长度大于或等于n它会精确地复制n个字符到dest并且不会在末尾添加\0这意味着dest可能不是一个有效的C字符串没有终止符。这是最危险的一点很多程序员因为忘记手动添加\0而引入bug。strncpy最初的设计目的是用于固定长度的字段如Unix文件系统早期的目录项而不是用来制作安全的字符串副本。用它来替代strcpy你必须非常小心char buf[10]; strncpy(buf, source, sizeof(buf) - 1); // 最多复制9个字符 buf[sizeof(buf) - 1] \0; // 手动确保终止符你必须手动添加\0并且n通常是缓冲区大小-1。7.3 更现代的方案snprintf对于简单的字符串复制一个更安全、更直观的选择是使用snprintfchar buf[10]; snprintf(buf, sizeof(buf), %s, source);snprintf的第二个参数是目标缓冲区的大小函数会保证写入的字符数不超过这个大小包括结尾的\0。如果源字符串太长它会被截断但目标字符串始终是以\0结尾的有效字符串。这几乎是我们想要的安全复制行为。虽然它比strcpy慢因为要解析格式字符串但在大多数场景下安全性的提升远胜于微小的性能损失。7.4 平台/编译器提供的安全版本许多编译器和运行库提供了更安全的函数如Windows的strcpy_s或GCC/Clang通过_FORTIFY_SOURCE宏对strcpy等函数进行的加强编译检查。这些函数通常要求明确传入目标缓冲区大小并在运行时检查。例如// 示例并非所有环境都有 errno_t strcpy_s(char *dest, rsize_t dest_size, const char *src);使用这些函数可以增强安全性但会牺牲一定的可移植性。7.5 终极建议建立自己的安全字符串处理习惯明确缓冲区大小任何时候分配字符数组都要清楚它的大小。使用sizeof(buffer)而不是硬编码数字。优先使用带长度限制的函数如snprintf,strlcpy某些系统,strncpy但务必记得手动加\0。计算可用空间复制前动态计算目标缓冲区的剩余空间。彻底弃用strcpy/strcat在新项目中可以考虑通过代码审查规则或静态分析工具禁止直接使用这些危险函数。我们模拟strcpy是为了理解底层机制从而更深刻地认识到为什么这些安全准则如此重要。知其然亦知其所以然方能写出既高效又安全的代码。8. 举一反三基于strcpy实现其他字符串函数掌握了strcpy的核心——指针遍历和复制我们就可以轻松地实现C标准库中的其他字符串函数。这就像学会了加减法乘除也就有了基础。这里我们尝试实现两个最相关的函数strcat字符串连接和strcmp字符串比较。8.1 实现strcat先找到尾巴再接上新头strcat(dest, src)的功能是将src字符串追加到dest字符串的末尾。它的内部可以看作是两个步骤找到dest字符串的结束符\0的位置。从这个位置开始执行一次strcpy将src复制过来。char *my_strcat(char *dest, const char *src) { // 步骤1找到dest字符串的末尾 char *ptr dest; while (*ptr ! \0) { // 遍历dest直到找到\0 ptr; } // 此时ptr指向dest末尾的\0 // 步骤2从ptr开始复制src字符串包括\0 while ((*ptr *src) ! \0) ; return dest; }看我们自己的my_strcpy的逻辑被完美复用了。第一个while循环是字符串遍历第二个while循环就是strcpy的核心。这里同样没有检查目标缓冲区是否足够大它继承了strcpy的“信任”哲学因此也有同样的安全隐患。安全的版本需要调用者确保dest有足够空间容纳dest和src连接后的结果。8.2 实现strcmp逐字符比武定胜负strcmp(s1, s2)比较两个字符串。它从第一个字符开始逐个比较对应位置的字符的ASCII值如果所有字符都相同直到遇到\0则返回0相等。如果发现不同的字符则返回*s1 - *s2第一个不匹配字符的差值。如果其中一个字符串先遇到\0而另一个还没结束则较短的字符串被认为“更小”。int my_strcmp(const char *s1, const char *s2) { // 循环比较直到字符不同或遇到\0 while (*s1 ! \0 *s1 *s2) { s1; s2; } // 循环结束后有两种可能 // 1. *s1 ! *s2 (包括*s2是\0的情况) // 2. *s1 *s2 \0 (两字符串完全相等) // 无论是哪种返回差值都符合标准 return (*(unsigned char *)s1 - *(unsigned char *)s2); }注意一个关键细节类型转换(unsigned char *)。为什么需要它因为strcmp比较的是字符的无符号值。在C语言中char类型可能是有符号的取值范围-128到127也可能是无符号的这取决于编译器和平台。如果char是有符号的一个大于127的字符如某些扩展ASCII或UTF-8多字节序列的一部分会被当作负数。直接相减*s1 - *s2如果s1是负数而s2是正数结果可能不符合“字典序”比较的预期。转换为unsigned char后所有字符值都在0-255之间比较就与平台无关了。这是标准库实现中的一个重要技巧我们的模拟也必须复现。8.3 触类旁通通过这三个函数strcpy,strcat,strcmp的实现你会发现C语言字符串处理的模式非常统一基于指针的循环遍历在\0处停止在循环体内进行核心操作复制、比较等。基于这个模式你可以尝试实现strlen遍历字符串直到\0计数。strchr遍历字符串寻找第一个匹配给定字符的位置。strstr在字符串中寻找子串稍复杂涉及嵌套循环。亲手实现这些函数是理解指针和内存操作最有效的练习。每一次实现都是对“指针即地址字符串即字符数组”这一核心概念的巩固。当你不再惧怕手动操作指针去处理字符串时你对C语言的理解就上了一个坚实的台阶。
返回列表