1. 项目概述:为什么字符串和内存操作是C语言的基石
刚接触C语言的朋友,可能觉得指针和内存管理是最大的坎。但等你真正开始写项目,尤其是涉及到数据处理、协议解析或者系统底层交互时,你会发现,字符串和内存的操作才是真正考验功力的地方。C语言本身没有“字符串”这个内置类型,它用字符数组和一堆以'\0'结尾的约定来模拟。这就意味着,所有关于字符串的拼接、查找、比较,都得靠我们自己动手,或者调用标准库函数。而内存函数,则是直接与计算机最底层的存储打交道,数据的搬移、比较、设置,都离不开它们。
我见过很多初学者,能写出复杂的算法,却在strcpy和memcpy的边界问题上栽跟头,导致程序崩溃或者出现难以察觉的安全漏洞(比如缓冲区溢出)。所以,今天我们不只停留在“怎么用”的层面,而是要彻底搞懂这些核心库函数背后的原理。我们会逐一拆解常用的字符串函数(如strlen,strcpy,strcat,strcmp)和内存函数(如memcpy,memmove,memset,memcmp),并亲手模拟实现它们。这个过程,就像给汽车做一次深度拆解保养,你会对每一个零件的运作方式了如指掌,以后无论是开车(使用函数)还是修车(调试问题),都会得心应手。
这篇文章适合所有希望夯实C语言基础、理解底层原理的开发者。无论你是正在学习的学生,还是希望回顾基础的工程师,通过“模拟实现”这个硬核手段,你将对指针、内存和边界条件有前所未有的深刻理解。
2. 核心思路:从“使用者”到“创造者”的思维转变
学习库函数,如果只停留在调用层面,那永远是“黑盒”。一旦程序出现与这些函数相关的诡异bug,你就会束手无策。我们的核心思路,就是完成一次角色转换:从库函数的“使用者”,变为其“创造者”。
2.1 模拟实现的价值:知其然,更知其所以然
模拟实现标准库函数,绝不是重复造轮子,而是一种最高效的学习方法。它的价值至少体现在三个方面:
第一,强制你处理所有边界情况。当你自己写strcpy时,你会立刻面临几个灵魂拷问:如果目标空间不够大怎么办?如果源字符串是NULL怎么办?如果源字符串和目标内存区域有重叠怎么办?标准库的strcpy可不会帮你检查这些,它默认调用者是专业的。通过模拟,你会深刻理解为什么文档里会强调“目标缓冲区必须有足够空间”,以及不遵守规则的后果。
第二,深化对指针和内存操作的理解。字符串和内存函数本质都是指针游戏。模拟strlen让你熟悉指针遍历;模拟strcat让你学会在指针偏移后追加内容;模拟memmove则要求你考虑内存重叠时的拷贝策略。每一个函数的实现,都是一次指针算术和内存访问的实战演练。
第三,培养编写健壮代码的习惯。在模拟时,我们会加入参数检查(尽管标准库函数通常不做检查,但我们的实现可以更友好),会思考如何返回更有用的错误信息。这种对安全性和鲁棒性的关注,会潜移默化地影响你今后所有的编码工作。
2.2 我们的实现策略:平衡标准与教学
在模拟实现时,我们会遵循一个原则:接口与标准库保持一致,内部实现追求清晰和教学目的。这意味着我们的函数名、参数列表和返回值类型会尽量模仿标准库(例如,我们实现的函数会以my_前缀开头,如my_strcpy),但内部实现会更详细地展示每一步的逻辑,并添加必要的注释和断言(assert),以便于理解。
同时,我们会区分“字符串函数”和“内存函数”的本质:
- 字符串函数:操作对象是“以
'\0'结尾的字符序列”。它们遇到'\0'就停止。这是它们的边界,也是它们的限制。 - 内存函数:操作对象是“一片原始的内存字节”。它们不关心内容,只按指定的字节数操作。因此,它们可以处理任何数据,包括结构体、数组,当然也包括不含
'\0'的字符数据。
理解这个区别,是正确选用这两类函数的关键。
3. 字符函数深度解析:不只是判断字母数字
在深入字符串函数之前,有必要先了解一下C标准库<ctype.h>中的字符分类与转换函数。它们虽然是单个字符的操作,但却是构建字符串处理逻辑的基础砖块。
3.1 常用字符函数及其原理
这些函数接收一个int类型的参数(实际上是字符的ASCII码),返回一个int(非零表示真,零表示假)或转换后的字符。
isalpha(int c): 判断c是否为字母(A-Z或a-z)。其内部实现通常是查表法,即有一个预定义的字符属性表,根据字符的ASCII码值进行索引查找,效率极高。isdigit(int c): 判断c是否为数字字符(0-9)。这是数据校验和解析中最常用的函数之一。isalnum(int c): 判断c是否为字母或数字。常用于检查用户名、标识符的合法性。islower(int c)/isupper(int c): 判断大小写。tolower(int c)/toupper(int c): 转换大小写。注意,它们只对字母字符有效,对于数字或符号,则原样返回。
注意:这些函数的参数类型是
int而非char,主要是为了支持EOF(通常为-1)。同时,为了确保正确性,传入的字符值必须在unsigned char的范围内或等于EOF,否则会导致未定义行为。这是一个容易被忽略的细节。
3.2 模拟实现一个字符函数:以my_isupper为例
让我们动手实现一个,来理解其查表思想的精髓。我们不会真的做一张大表,而是用逻辑判断。
#include <stdio.h> int my_isupper(int c) { // 判断字符c的ASCII码是否在大写字母A-Z的范围内 return (c >= 'A' && c <= 'Z'); } int main() { char test_chars[] = {'A', 'z', '5', 'M', '#'}; for (int i = 0; i < sizeof(test_chars)/sizeof(test_chars[0]); ++i) { printf("'%c' is uppercase? %s\n", test_chars[i], my_isupper(test_chars[i]) ? "Yes" : "No"); } return 0; }实现要点:
- 参数使用
int类型,与标准保持一致。 - 核心逻辑就是一次简单的范围比较。标准库的实现可能更高效(比如位运算),但对于理解原理,这样足够清晰。
- 这个实现没有处理
EOF和超出unsigned char范围的情况,因为我们的重点是展示逻辑。在健壮的库实现中,通常会先进行范围检查。
通过这个简单的例子,我们可以看到,即便是最基本的函数,也蕴含着对数据范围(ASCII码)的精确把握。这是所有底层编程的共性。
4. 字符串函数模拟实现(一):求长度与拷贝
现在进入正餐,我们从最基础、最常用的字符串函数开始。
4.1my_strlen:遍历的艺术
标准原型:size_t strlen(const char *str);功能:计算字符串str的长度,不包括结尾的'\0'。
模拟实现与解析:
#include <assert.h> size_t my_strlen(const char *str) { assert(str != NULL); // 防御性编程,防止传入空指针 const char *p = str; // 用另一个指针遍历,不改变原指针 while (*p != '\0') { p++; } return p - str; // 指针相减,得到中间相隔的元素个数 }为什么这样实现?
- 使用
const char*:这表明函数不会修改源字符串,给调用者明确的承诺,也便于编译器优化。 - 使用辅助指针
p:直接对str进行++操作会丢失字符串的起始地址。用p来遍历是更清晰的做法。 - 指针相减:这是C语言的语法糖。两个指向同一数组的指针相减,结果就是它们之间元素的个数。这种方法比用计数器变量
i递增更简洁,也常被认为更“地道”。 - 添加
assert:标准库的strlen传入NULL会导致崩溃。我们加入断言,在调试阶段就能快速定位问题。当然,生产代码中可能会用更温和的错误处理。
常见误区:
- 在循环条件里写
while (*str++),这样循环结束后str已经指向'\0'之后了,无法计算长度。 - 忘记处理
str为NULL的情况,导致解引用空指针。
4.2my_strcpy:安全拷贝的基石
标准原型:char *strcpy(char *dest, const char *src);功能:将src指向的字符串(包括结尾的'\0')拷贝到dest指向的空间。
模拟实现与解析:
char* my_strcpy(char* dest, const char* src) { assert(dest != NULL && src != NULL); char* ret = dest; // 保存目标字符串的起始地址,用于返回 while ((*dest++ = *src++) != '\0') { // 循环体为空,所有操作都在条件判断中完成 } return ret; // 返回目标字符串的起始地址,以支持链式调用 }这段代码的精妙之处:
- 赋值表达式作为条件:
(*dest++ = *src++)这个表达式先执行赋值,然后将赋值的结果(即被赋值的字符)与'\0'比较。当拷贝到src的'\0'时,赋值完成后表达式的值就是'\0',循环终止。同时,'\0'也被成功拷贝到了dest。 - 后缀
++运算符:它在表达式求值后才增加指针的值,确保了本次循环中赋值的是当前指向的字符。 - 返回
ret:标准库设计返回dest的原始值,是为了方便链式调用,例如strlen(strcpy(dest, src))。
致命陷阱与注意事项:
警告:
strcpy是“缓冲区溢出”漏洞的罪魁祸首之一。它从不检查dest是否有足够空间。如果src比dest长,就会覆盖dest之后的内存,导致程序崩溃或被恶意利用。安全实践:在现代编程中,应绝对避免使用strcpy。必须使用更安全的版本,如strncpy(也有其陷阱),或非标准的strlcpy(更安全),或者干脆在拷贝前用strlen检查长度。我们的模拟实现只是为了理解原理,在实际项目中请使用安全函数。
5. 字符串函数模拟实现(二):连接、比较与查找
掌握了拷贝,我们来看连接和比较,它们是构建更复杂字符串逻辑的基础。
5.1my_strcat:字符串的拼接
标准原型:char *strcat(char *dest, const char *src);功能:将src字符串追加到dest字符串的末尾(覆盖dest原有的'\0',并在新字符串末尾添加'\0')。
模拟实现与解析:
char* my_strcat(char* dest, const char* src) { assert(dest != NULL && src != NULL); char* ret = dest; // 第一步:找到dest字符串的结尾(即'\0'的位置) while (*dest != '\0') { dest++; } // 第二步:从dest的末尾开始,执行strcpy操作 while ((*dest++ = *src++) != '\0') { ; } return ret; }实现拆解:
- 定位终点:第一个
while循环的作用是让dest指针走到原字符串的结束符'\0'处。这是连接操作的起点。 - 执行拷贝:第二个
while循环就是我们的my_strcpy逻辑,从dest的当前位置(原'\0'处)开始,将src的内容拷贝过来。 - 重叠问题:
strcat要求源字符串和目标字符串不能有重叠,否则行为未定义。我们的实现同样没有检查这一点。
一个关键点:strcat的第一个参数dest必须是一个已初始化的字符串(即以'\0'结尾),而不能是一个未初始化的字符数组。否则,第一个while循环可能永远找不到'\0',导致越界访问。
5.2my_strcmp:字符串的“字典序”比较
标准原型:int strcmp(const char *str1, const char *str2);功能:比较两个字符串。返回值小于0表示str1小于str2,等于0表示相等,大于0表示str1大于str2。比较规则是逐字符比较ASCII码,直到遇到不同的字符或'\0'。
模拟实现与解析:
int my_strcmp(const char* str1, const char* str2) { assert(str1 != NULL && str2 != NULL); // 逐字符比较,直到遇到不相同的字符或遇到'\0' while (*str1 == *str2) { if (*str1 == '\0') { // 如果相等且都为'\0',说明两字符串完全相同 return 0; } str1++; str2++; } // 返回两个不相等字符的ASCII码差值 return *(unsigned char*)str1 - *(unsigned char*)str2; }为什么返回值是差值?
- 标准规定返回值的正负性由第一对不相等的字符的差值决定。这给了调用者比单纯返回-1,0,1更多的信息。
- 关键技巧:
(unsigned char*)强制转换。这是为了正确处理char为有符号类型的系统。例如,字符'\xFF'(十进制-1)和'\x01'(十进制1)比较,如果直接用有符号char计算,-1 - 1 = -2,结果是负数,这符合直觉(0xFF > 0x01)。但如果char是无符号的,这个转换没有影响。加上它保证了代码的可移植性。
使用场景:strcmp是排序(如qsort)、查找、判断字符串是否相等的核心函数。需要注意的是,它区分大小写(‘A’和‘a’不同)。如果需要不区分大小写的比较,需要使用strcasecmp(POSIX标准)或_stricmp(Windows)。
6. 字符串函数模拟实现(三):受限长度操作与查找
由于strcpy和strcat的安全性问题,标准库提供了它们的安全版本——strncpy和strncat,通过指定最大拷贝字符数来防止溢出。
6.1my_strncpy:带长度限制的拷贝
标准原型:char *strncpy(char *dest, const char *src, size_t n);功能:拷贝src的前n个字符到dest。如果src的长度小于n,则用'\0'填充dest剩余部分;如果src的长度大于或等于n,则不会在dest末尾添加'\0'。
模拟实现与解析:
char* my_strncpy(char* dest, const char* src, size_t n) { assert(dest != NULL && src != NULL); char* ret = dest; size_t i = 0; // 拷贝最多n个字符,或者遇到src的结尾 for (i = 0; i < n && src[i] != '\0'; ++i) { dest[i] = src[i]; } // 如果i < n,说明src提前结束,需要用'\0'填充dest剩余位置 for (; i < n; ++i) { dest[i] = '\0'; } return ret; }strncpy的诡异特性与陷阱:
- 不保证结尾有
'\0':如果src的长度大于等于n,strncpy不会在dest的第n个位置写入'\0'。这意味着dest可能不是一个有效的C字符串!这是strncpy最危险的地方。很多程序员误以为它总是产生一个以'\0'结尾的字符串。 - 效率可能低下:如果
src很短而n很大,它需要写入大量的'\0'填充字符。 - 正确使用姿势:使用
strncpy后,必须手动确保字符串终止:dest[n-1] = '\0';。但更推荐使用snprintf(dest, n, "%s", src),它总能保证结果字符串以'\0'结尾(只要n>0)。
6.2my_strncat:带长度限制的连接
标准原型:char *strncat(char *dest, const char *src, size_t n);功能:将src的前n个字符追加到dest末尾,并在追加的字符后添加一个'\0'。
模拟实现与解析:
char* my_strncat(char* dest, const char* src, size_t n) { assert(dest != NULL && src != NULL); char* ret = dest; // 找到dest的末尾 while (*dest != '\0') { dest++; } // 从dest末尾开始,拷贝最多n个字符 size_t i = 0; for (i = 0; i < n && src[i] != '\0'; ++i) { dest[i] = src[i]; } // 在追加的字符后面添加终止符 dest[i] = '\0'; return ret; }与strncpy的关键区别:strncat总是在结果字符串的末尾添加一个'\0',无论是否拷贝满了n个字符。这使得它的行为比strncpy更符合直觉,也更安全。它保证结果是一个有效的C字符串(前提是dest初始时是有效的)。
6.3my_strstr:查找子串
标准原型:char *strstr(const char *haystack, const char *needle);功能:在haystack(干草堆)字符串中查找第一次出现needle(针)子串的位置,并返回该位置的指针。如果找不到,返回NULL。
模拟实现(暴力匹配法)与解析:
char* my_strstr(const char* haystack, const char* needle) { assert(haystack != NULL && needle != NULL); if (*needle == '\0') { // 空字符串是任何字符串的子串,返回haystack本身 return (char*)haystack; } const char* h; const char* n; const char* start = haystack; while (*start != '\0') { h = start; n = needle; // 从start位置开始,逐个字符比较 while (*h != '\0' && *n != '\0' && *h == *n) { h++; n++; } // 如果n走到了结尾,说明needle全部匹配成功 if (*n == '\0') { return (char*)start; } // 如果h走到了结尾,说明haystack剩余长度不够,匹配失败 if (*h == '\0') { return NULL; } // 本轮匹配失败,start向后移动一位,继续尝试 start++; } return NULL; // 遍历完haystack仍未找到 }算法分析:
- 这是最朴素的字符串匹配算法,时间复杂度在最坏情况下是O(m*n),其中m和n分别是
haystack和needle的长度。对于短字符串或简单场景足够用。 - 标准库的实现可能采用更高效的算法,如KMP(Knuth-Morris-Pratt)或Boyer-Moore,但在面试或学习场景,能写出正确的暴力解法已经达标。
- 边界条件处理:处理空字符串
needle的情况是必须的,这是一个常见的面试考点。
7. 内存函数模拟实现(一):内存的搬运工memcpy与memmove
内存函数操作的是无类型的字节流,因此它们用void*作为参数类型,并用size_t指定操作的字节数。这是它们与字符串函数最根本的区别。
7.1my_memcpy:简单直接的内存拷贝
标准原型:void *memcpy(void *dest, const void *src, size_t n);功能:从src指向的位置开始,拷贝n个字节到dest指向的位置。标准规定,源内存区和目标内存区不能重叠。如果重叠,行为是未定义的。
模拟实现与解析:
void* my_memcpy(void* dest, const void* src, size_t n) { assert(dest != NULL && src != NULL); char* d = (char*)dest; // 按字节操作,所以转为char* const char* s = (const char*)src; // 从前向后逐字节拷贝 for (size_t i = 0; i < n; ++i) { d[i] = s[i]; } return dest; }实现要点:
void*的转换:void*指针不能直接进行算术运算(如++)和解引用。需要先转换为具体类型的指针,这里我们转换为char*,因为char的大小是1字节,方便按字节操作。- 重叠问题:这个实现是“从前向后”拷贝。如果
dest在src之后,且两者有重叠区域,那么在拷贝过程中,src后半部分还没被拷贝的数据就会被dest向前移动的数据覆盖掉,导致拷贝结果错误。这就是为什么memcpy不能用于重叠内存的原因。 - 效率考虑:标准库的
memcpy实现通常会利用CPU的硬件特性(如SIMD指令)进行大块数据的快速拷贝,效率远高于逐字节循环。我们的实现仅作原理演示。
7.2my_memmove:能处理重叠的拷贝
标准原型:void *memmove(void *dest, const void *src, size_t n);功能:拷贝n个字节。与memcpy不同的是,memmove会处理源和目标内存区域重叠的情况。
模拟实现与解析:
void* my_memmove(void* dest, const void* src, size_t n) { assert(dest != NULL && src != NULL); char* d = (char*)dest; const char* s = (const char*)src; if (d < s) { // 情况1:目标地址在源地址之前,从前向后拷贝(和memcpy一样) for (size_t i = 0; i < n; ++i) { d[i] = s[i]; } } else if (d > s) { // 情况2:目标地址在源地址之后,从后向前拷贝 // 这是处理重叠的关键:先拷贝重叠区域后面的数据,避免被覆盖 for (size_t i = n; i > 0; --i) { d[i - 1] = s[i - 1]; } } // 情况3:地址相同,什么都不用做 return dest; }重叠处理的逻辑精髓:
dest在src之前(d < s):重叠区域在拷贝的后半部分。从前向后拷贝时,src重叠部分的数据在被覆盖之前就已经被拷贝走了,所以没问题。dest在src之后(d > s):重叠区域在拷贝的前半部分。如果还从前向后拷贝,src重叠部分的数据会被dest移动过来的数据先覆盖掉,导致错误。因此,必须从后向前拷贝,先处理尾部数据。- 地址相同:直接返回即可。
memcpyvsmemmove的选择:
- 当你确定两块内存绝不重叠时,优先使用
memcpy,因为编译器可能会对它做更激进的优化。 - 当你不确定,或者明确知道内存可能重叠时,必须使用
memmove。虽然它的名字里有“move”,但它执行的是拷贝操作,源内存的内容不会被改变。
8. 内存函数模拟实现(二):内存的初始化与比较
除了拷贝,内存的批量设置和比较也是底层编程中的高频操作。
8.1my_memset:内存的“粉刷匠”
标准原型:void *memset(void *str, int c, size_t n);功能:将str指向的内存区域的前n个字节,全部设置为值c(转换为unsigned char)。
模拟实现与解析:
void* my_memset(void* str, int c, size_t n) { assert(str != NULL); unsigned char* p = (unsigned char*)str; unsigned char uc = (unsigned char)c; // 只取c的低8位 for (size_t i = 0; i < n; ++i) { p[i] = uc; } return str; }关键细节:
- 参数
c的类型是int:这是历史原因。但memset是按字节设置的,所以实际使用的是c的低8位(即一个字节)。我们将它转换为unsigned char来确保只使用这8位。 - 常见用途:
- 初始化数组为零:
memset(arr, 0, sizeof(arr))。这是将一块内存清零最高效、最通用的方法。 - 初始化结构体:在定义结构体变量后,有时会用
memset(&obj, 0, sizeof(obj))来将其所有成员置零。 - 填充特定模式:例如创建一段特定字节序列。
- 初始化数组为零:
- 一个经典误区:不能用
memset来初始化非字符类型的数组为特定值(比如memset(int_arr, 1, sizeof(int_arr))并不能把每个int元素设置为1,而是把每个字节设置为1,导致每个int变成0x01010101)。
8.2my_memcmp:内存的逐字节比对
标准原型:int memcmp(const void *str1, const void *str2, size_t n);功能:比较str1和str2指向的内存区域的前n个字节。返回值规则与strcmp类似。
模拟实现与解析:
int my_memcmp(const void* str1, const void* str2, size_t n) { assert(str1 != NULL && str2 != NULL); const unsigned char* p1 = (const unsigned char*)str1; const unsigned char* p2 = (const unsigned char*)str2; for (size_t i = 0; i < n; ++i) { if (p1[i] != p2[i]) { // 返回第一个不相等字节的差值 return p1[i] - p2[i]; } } // 前n个字节全部相等 return 0; }与strcmp的核心区别:
- 比较范围:
memcmp严格比较n个字节,不管中间有没有'\0'。strcmp遇到'\0'就停止。 - 用途:
memcmp可以用来比较任何内存数据,比如两个结构体、两个数组等。而strcmp只能比较以'\0'结尾的字符串。 - 例子:比较两个
struct变量是否完全相等,可以用memcmp(&a, &b, sizeof(struct))。但要注意,如果结构体中含有指针成员,memcmp比较的是指针本身的值(地址),而不是指针指向的内容。
9. 模拟实现中的陷阱、技巧与经验实录
自己动手实现一遍,你会遇到很多在单纯调用时想不到的问题。这里记录一些关键的“坑”和技巧。
9.1 指针操作中的“未定义行为”雷区
- 对NULL指针解引用:这是最经典的崩溃原因。在所有模拟函数的开头,我们都使用了
assert进行保护。在生产环境中,可能需要更优雅的错误处理(如返回错误码)。 - 指针越界访问:在
while (*p != '\0')或for循环中,如果传入的字符串不是以'\0'结尾,循环将不会停止,直到访问到非法内存引发段错误。永远不要相信外部传入的数据是合法的。 - 指针类型转换的玄机:在
memcmp和strcmp中,我们将char*转换为unsigned char*再相减。这是因为C语言中char可能是有符号的。直接相减127 - (-1),如果按有符号算结果是128,但按无符号算127 - 255会下溢得到一个很大的正数,再转为int结果可能为负。使用unsigned char*能保证差值计算是基于0-255的值,结果符合字节比较的直观意义。
9.2 性能优化的简单思考
我们的模拟实现都是最朴素的O(n)循环。标准库的实现远非如此。
- 内存对齐访问:现代CPU对对齐的内存访问(如4字节、8字节对齐)速度更快。库函数可能会先处理开头不对齐的少数字节,然后以
int或long为单位进行大块拷贝/比较,最后处理尾部剩余的字节。 - 利用硬件指令:如x86架构的
rep movsb等字符串操作指令,可以由CPU直接完成大块内存的搬移,比软件循环快得多。 - 循环展开:减少循环条件判断的次数,例如一次循环处理4个字节。
虽然我们不必在模拟中实现这些,但要知道“为什么标准库函数这么快”,这是进阶的必经之路。
9.3 测试:验证你的模拟实现
编写全面的测试用例至关重要。以下是一个测试my_strcpy的简单框架思路:
void test_my_strcpy() { char dest[20]; const char* src1 = "Hello"; const char* src2 = ""; const char* src3 = "A very long string that might cause problems if not careful"; // 测试正常拷贝 my_strcpy(dest, src1); assert(strcmp(dest, src1) == 0); printf("Test 1 passed: Normal copy.\n"); // 测试空字符串拷贝 my_strcpy(dest, src2); assert(dest[0] == '\0'); printf("Test 2 passed: Empty string copy.\n"); // 测试源字符串长度等于目标缓冲区大小(危险边缘) char dest2[6] = "XXXXX"; // 没有空间给'\0'了! // 注意:这里my_strcpy会溢出,程序可能会崩溃。这正说明了strcpy的不安全性。 // 更安全的测试是使用strncpy或snprintf // my_strcpy(dest2, "Hello"); // 这行可能会崩溃或导致断言失败 printf("All basic tests for my_strcpy passed.\n"); }测试要覆盖:正常情况、边界情况(空字符串)、错误情况(NULL指针,需配合断言)。对于内存函数,还要测试重叠内存的情况。
9.4 一个综合案例:自己实现一个简单的字符串处理函数
假设我们需要一个函数,它接收一个字符串,将其中的小写字母全部转换为大写。我们可以综合利用所学:
void to_uppercase(char* str) { if (str == NULL) return; char* p = str; while (*p != '\0') { if (*p >= 'a' && *p <= 'z') { // 或者用 islower(*p) *p = *p - ('a' - 'A'); // 小写转大写的ASCII码差值 } p++; } } // 或者使用库函数 void to_uppercase_using_lib(char* str) { if (str == NULL) return; for (char* p = str; *p != '\0'; ++p) { *p = toupper(*p); // 使用ctype.h中的函数,更可移植 } }这个简单的例子展示了如何遍历字符串、判断字符属性、修改内容。它就像一块积木,可以用来构建更复杂的文本处理工具。