C 语言字符串 内存函数:用法 + 模拟实现全攻略
C 语言字符串 & 内存函数全攻略:从用法、坑点到手动模拟实现
最近系统学习了 C 语言标准库中最核心的字符操作函数、字符串处理函数、内存操作函数三大模块,这些函数是 C 语言开发的基石,日常编码、笔试面试几乎处处都会用到。光会调用函数远远不够,只有吃透底层逻辑、踩过坑、亲手模拟实现过,才能真正做到灵活运用、避开陷阱。
这篇博客我会结合课堂重点笔记,再加上我自己手写的模拟实现代码和测试用例,把这些函数的功能、用法、易错点、底层实现一次性讲透,既是自己的学习复盘,也希望能帮到同样在学习 C 语言的同学。
一、字符操作函数
字符操作函数是处理单个字符的工具,分为字符分类函数和字符转换函数两大类,所有函数都需要包含头文件<ctype.h>,使用起来非常便捷,比我们自己写条件判断要简洁、可读性强得多。
1.1 字符分类函数
这类函数的核心作用是:判断传入的字符是否符合指定类型,符合则返回非 0 整数,不符合则返回 0。
这里整理的常用字符分类函数如下表,覆盖了绝大多数开发场景:
| 函数名 | 符合条件返回真(非 0) |
|---|---|
| iscntrl | 任何控制字符 |
| isspace | 空白字符(空格、换行 \n、回车 \r、制表符 \t 等) |
| isdigit | 十进制数字字符 '0'~'9' |
| isxdigit | 十六进制数字字符(0-9、a-f、A-F) |
| islower | 小写字母 a~z |
| isupper | 大写字母 A~Z |
| isalpha | 字母(a~z、A~Z) |
| isalnum | 字母或数字 |
| ispunct | 标点符号(非字母、数字的可打印图形字符) |
| isgraph | 任何图形字符 |
| isprint | 任何可打印字符(含图形字符和空白字符) |
课上重点讲了islower的用法,其他函数的用法完全一致,举个最常见的例子:判断字符是否为小写字母,再配合转换函数实现大小写转换。
#include <stdio.h> #include <ctype.h> int main () { char str[] = "Test String.\n"; int i = 0; while (str[i]) { char c = str[i]; // 判断是否为小写字母 if (islower(c)) { printf("%c 是小写字母\n", c); } i++; } return 0; }1.2 字符转换函数
C 语言标准库提供了两个专门的字符转换函数,比我们手动用 ASCII 码加减 32 更规范、可读性更强:
int tolower ( int c );:将大写字母转换为小写字母,非字母字符不做修改int toupper ( int c );:将小写字母转换为大写字母,非字母字符不做修改
课堂练习的经典案例:将字符串中的小写字母全部转为大写,其他字符保持不变,用转换函数实现非常简洁:
#include <stdio.h> #include <ctype.h> int main () { char str[] = "Test String. 123!"; int i = 0; while (str[i]) { // 小写转大写 if (islower(str[i])) { str[i] = toupper(str[i]); } i++; } printf("转换结果:%s\n", str); return 0; }运行结果:转换结果:TEST STRING. 123!
二、基础字符串函数
这部分是字符串处理最核心的基础函数,包括求长度、拷贝、追加、比较四大基础操作,所有函数都需要包含头文件<string.h>。
2.1 strlen 字符串长度统计函数
2.1.1函数核心信息
- 函数原型:
size_t strlen ( const char * str ); - 核心功能:统计
str指向的字符串中,\0之前的字符个数(不包含\0本身) - 关键返回值:返回值类型是
size_t,也就是无符号整数,这是最容易踩的坑!
2.1.2必须注意的坑点
- 字符串必须以
\0结尾,否则 strlen 会继续向后遍历内存,直到找到\0,返回的是随机的错误值 - 返回值是无符号数,两个 strlen 的结果相减,永远不会得到负数。比如
strlen("bbb") - strlen("abcdef"),数学上是 3-6=-3,但因为是无符号数运算,结果会是一个极大的正数,导致判断逻辑出错。
错误示例:
#include <stdio.h> #include <string.h> int main() { const char* str1 = "abcdef"; const char* str2 = "bbb"; // 错误:strlen返回无符号数,相减结果永远>0 if(strlen(str2) - strlen(str1) > 0) { printf("str2更长\n"); // 会错误执行这一行 } else { printf("str1更长\n"); } return 0; }正确的写法是直接比较大小,而不是相减:if(strlen(str2) > strlen(str1))
2.1.3我的模拟实现
strlen 的三种经典模拟实现方式,之前写过其中两种,下面我将它们整合在一起,每一种都有不同的思路,能加深对 C 语言指针和循环的理解。
方式 1:计数器法(最直观)
#include <stdio.h> #include <assert.h> // 计数器方式实现strlen int my_strlen(const char * str) { // 断言:防止空指针 assert(str); int count = 0; // 遍历到\0为止 while(*str) { count++; str++; } return count; } int main() { char str[] = "hello world"; printf("长度:%d\n", my_strlen(str)); return 0; }方式 2:递归实现(不创建临时变量)
#include <stdio.h> #include <assert.h> // 递归方式实现strlen int my_strlen(const char * str) { assert(str); // 递归终止条件:遇到\0,返回0 if(*str == '\0') return 0; // 递归调用,每次指针后移一位,长度+1 else return 1 + my_strlen(str+1); } int main() { char str[] = "luminous"; printf("长度:%d\n", my_strlen(str)); return 0; }方式 3:指针 - 指针法(最简洁)
C 语言中,两个指向同一块内存的指针相减,得到的是两个指针之间的元素个数,刚好可以用来求字符串长度。
#include <stdio.h> #include <assert.h> // 指针-指针方式实现strlen int my_strlen(const char * str) { assert(str); const char *start = str; // 遍历到\0的位置 while(*str) { str++; } // 末尾指针 - 起始指针 = 元素个数 return str - start; } int main() { char str[] = "123456789"; printf("长度:%d\n", my_strlen(str)); return 0; }2.2 strcpy 字符串拷贝函数
2.2.1函数核心信息
- 函数原型:
char* strcpy(char * destination, const char * source ); - 核心功能:将
source源字符串中的内容(包括结尾的\0),完整拷贝到destination目标空间中 - 返回值:返回目标空间的起始地址,支持链式访问
2.2.2必须注意的坑点
- 源字符串必须以
\0结尾,否则会拷贝内存中的随机内容,造成越界 - 会自动拷贝源字符串的
\0到目标空间,这是拷贝结束的标志 - 目标空间必须足够大,能容纳下源字符串的全部内容,否则会造成缓冲区溢出
- 目标空间必须是可修改的,不能传入字符串常量(只读内存),否则会程序崩溃
2.2.3我的模拟实现
#include <stdio.h> #include <assert.h> // 模拟实现strcpy char* my_strcpy(char *dest, const char*src) { // 保存目标空间起始地址,用于返回 char *ret = dest; // 非空断言 assert(dest != NULL); assert(src != NULL); // 核心逻辑:*src赋值给*dest,然后两个指针都后移,直到赋值完\0,循环终止 while((*dest++ = *src++)) { ; // 空循环体,所有操作都在while条件里完成了 } return ret; } // 测试代码 int main() { char arr1[20] = {0}; char arr2[] = "hello, C语言"; my_strcpy(arr1, arr2); printf("拷贝结果:%s\n", arr1); return 0; }这个实现的核心是while((*dest++ = *src++)),一行代码就完成了赋值、指针后移、循环终止的全部逻辑,非常经典,也是面试里常考的写法。
2.3 strcat 字符串追加函数
2.3.1函数核心信息
- 函数原型:
char * strcat ( char * destination, const char * source ); - 核心功能:找到
destination目标字符串结尾的\0,从\0的位置开始,把source源字符串的内容追加进去,最终形成一个完整的新字符串 - 返回值:返回目标空间的起始地址,支持链式访问
2.3.2必须注意的坑点
- 源字符串和目标字符串都必须以
\0结尾:源字符串保证追加的内容完整,目标字符串保证能找到追加的起始位置 - 目标空间必须足够大,能容纳下原字符串 + 源字符串的总长度,避免溢出
- 目标空间必须可修改,不能传入字符串常量
- 标准库不支持自己给自己追加(比如
strcat(arr, arr)),会因为覆盖\0导致死循环,内存重叠场景要避免使用
2.3.3我的模拟实现
#include <stdio.h> #include <assert.h> // 模拟实现strcat char* my_strcat(char *dest, const char*src) { // 保存目标空间起始地址 char *ret = dest; // 非空断言 assert(dest != NULL); assert(src != NULL); // 第一步:找到目标字符串结尾的\0 while(*dest) { dest++; } // 第二步:从\0的位置开始,拷贝源字符串,和strcpy的逻辑一致 while((*dest++ = *src++)) { ; } return ret; } // 测试代码 int main() { char arr1[20] = "hello "; char arr2[] = "world!"; my_strcat(arr1, arr2); printf("追加结果:%s\n", arr1); return 0; }2.4 strcmp 字符串比较函数
2.4.1函数核心信息
- 函数原型:
int strcmp ( const char * str1, const char * str2 ); - 核心功能:逐字符对比两个字符串的 ASCII 码值,从第一个字符开始,相等就对比下一个,直到遇到不相等的字符,或者两个字符串都到结尾的
\0 - 标准规定的返回值:
- 第一个字符串大于第二个字符串,返回大于 0的整数
- 第一个字符串等于第二个字符串,返回0
- 第一个字符串小于第二个字符串,返回小于 0的整数
2.4.2必须注意的点
- 很多初学者会写
if(strcmp(str1, str2) == 1)来判断大于,这是错误的!标准只规定了返回大于 0、小于 0、等于 0,不同编译器的实现里,大于 0 不一定是 1,只要是正数就符合标准,正确的写法是if(strcmp(str1, str2) > 0) - 比较的是 ASCII 码值,比如
"abc"和"abq",第三个字符c的 ASCII 码是 99,q是 113,所以"abc"小于"abq"
我的模拟实现
#include <stdio.h> #include <assert.h> // 模拟实现strcmp int my_strcmp (const char * str1, const char * str2) { // 非空断言 assert(str1 != NULL); assert(str2 != NULL); // 字符相等就继续循环 while(*str1 == *str2) { // 两个都到\0了,说明完全相等,返回0 if(*str1 == '\0') return 0; // 指针后移 str1++; str2++; } // 遇到不相等的字符,返回ASCII码差值 return *str1 - *str2; } // 测试代码 int main() { char arr1[] = "abcdef"; char arr2[] = "abq"; int ret = my_strcmp(arr1, arr2); if(ret > 0) printf("arr1 > arr2\n"); else if(ret == 0) printf("arr1 == arr2\n"); else printf("arr1 < arr2\n"); return 0; }三、长度受限的安全字符串函数
上面的strcpy、strcat、strcmp都是不受长度限制的,很容易造成缓冲区溢出的安全问题,也是很多漏洞的来源。C 语言标准库提供了对应的长度受限版本:strncpy、strncat、strncmp,通过指定操作的字符个数,大大提升了代码的可控性和安全性,也是实际开发中更推荐使用的版本。
3.1 strncpy 长度受限的字符串拷贝
3.1.1库函数核心信息
- 函数原型:
char * strncpy ( char * destination, const char * source, size_t num ); - 核心功能:从
source源字符串拷贝最多num个字符到destination目标空间 - 必须牢记的规则(课堂重点):
- 若源字符串长度小于
num:拷贝完源字符串的\0后,会继续在目标空间补\0,直到凑够num个字节 - 若源字符串长度大于等于
num:只会拷贝num个字符,不会主动添加\0,这是最容易踩的坑,后续访问目标字符串可能会出现乱码 - 目标空间必须足够大,避免越界
- 若源字符串长度小于
3.1.2我的模拟实现代码
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<assert.h> // 模拟实现strncpy char* my_strncpy(char* destination, const char* source, size_t num) { // 断言:避免空指针访问 assert(destination && source); // 保存目标空间起始地址,用于最终返回 char* dest_start = destination; // 第一步:拷贝源字符串字符,直到num耗尽或源字符串结束 while (num > 0 && *source != '\0') { *destination = *source; destination++; source++; num--; } // 第二步:若num有剩余,补全'\0' while (num > 0) { *destination = '\0'; destination++; num--; } return dest_start; } // 测试代码 int main() { char arr[] = "Hello,luminous!"; char arr1[10] = { 0 }; size_t num; scanf("%zu",&num); // 安全处理:避免输入长度超出目标数组容量 if (num >= sizeof(arr1)) { num = sizeof(arr1) - 1; printf("输入的字符数超出范围,自动调整为:%zu\n", num); } char* p = my_strncpy(arr1, arr, num); printf("拷贝结果:%s",p); return 0; }3.2 strncat 长度受限的字符串追加
3.2.1库函数核心信息
- 函数原型:
char * strncat ( char * destination, const char * source, size_t num ); - 核心功能:从源字符串中最多追加
num个字符到目标字符串的末尾,追加完成后会自动在结尾添加\0 - 关键规则(课堂重点):
- 不管追加了多少个字符,最终一定会在结尾补
\0,保证目标空间里是一个合法的字符串 - 若源字符串长度小于
num,只会追加到\0为止,不会额外补\0 - 目标空间必须足够大,能容纳原字符串长度 + num + 1(结尾的 \0)
- 支持自己给自己追加,不会出现 strcat 的死循环问题
- 不管追加了多少个字符,最终一定会在结尾补
3.2.2用法示例
#include <stdio.h> #include <string.h> int main() { char arr1[20] = "hello "; char arr2[] = "world!"; // 最多追加3个字符 strncat(arr1, arr2, 3); printf("追加结果:%s\n", arr1); // 输出 hello wor return 0; }3.3 strncmp 长度受限的字符串比较
3.3.1库函数核心信息
- 函数原型:
int strncmp ( const char * str1, const char * str2, size_t num ); - 核心功能:比较两个字符串,最多比较
num个字符,按 ASCII 码值逐字符对比 - 返回值规则和 strcmp 完全一致:大于返回正数,等于返回 0,小于返回负数
- 适用场景:只需要对比字符串的前 N 个字符,比如对比 URL 的前缀、文件名的开头,不用遍历整个字符串,效率更高也更安全
3.3.2我的模拟实现代码
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<assert.h> // 模拟实现strncmp int my_strncmp(const char* str1, const char* str2, size_t num) { // 非空断言 assert(str1 && str2); while (num > 0) { // 遇到不相等的字符,直接返回差值 if (*str1 != *str2) { // 转unsigned char避免符号位带来的问题 return (unsigned char)*str1 - (unsigned char)*str2; } // 遇到'\0',说明两个字符串都提前结束,完全相等 if (*str1 == '\0') { return 0; } // 指针后移,继续对比下一个字符 str1++; str2++; num--; } // 循环结束,说明前num个字符完全相等 return 0; } // 测试代码 int main() { char arr[] = "Hello,luminous!"; char arr1[] = "Hi,Luminous!"; size_t num; scanf("%zu", &num); int ret = my_strncmp(arr, arr1, num); if (ret < 0) { printf("前%zu字符比较中,str1 < str2\n", num); } else if (ret > 0) { printf("前%zu字符比较中,str1 > str2\n", num); } else { printf("前%zu字符比较中,str1 = str2\n", num); } return 0; }四、字符串查找与分割函数
在实际开发中,查找子串、分割字符串是非常常见的需求,C 语言标准库提供了strstr和strtok两个函数专门解决这类问题。
4.1 strstr 字符串查找函数
4.1.1库函数核心信息
- 函数原型:
char * strstr ( const char * str1, const char * str2); - 核心功能:在
str1主串中,查找str2子串第一次出现的位置 - 返回值规则:
- 找到子串:返回子串在主串中首次出现的起始地址
- 找不到子串:返回
NULL - 特殊情况:若子串是空字符串,直接返回主串的起始地址
4.1.2我的模拟实现代码
这里用了最经典的暴力匹配算法,逻辑清晰易懂,完全符合标准库的功能要求,非常适合入门学习。课件里也提到,更高效的实现可以用 KMP 算法,后续可以进阶学习。
#include<stdio.h> #include<assert.h> // 模拟实现strstr(暴力匹配算法) char* my_strstr(const char* haystack,const char* needle) { // 非空断言 assert(haystack && needle); // cp指针记录当前主串的匹配起始位置 const char* cp = haystack; // 特殊情况:子串为空,直接返回主串起始地址 if (*needle == '\0') { return (char*)haystack; } // 遍历主串的每一个字符,作为匹配起点 while (*cp) { const char* s1 = cp; const char* s2 = needle; // 逐字符匹配,直到字符不相等或其中一个字符串结束 while (*s1 != 0 && *s2 != 0 && *s1 == *s2) { s1++; s2++; } // 子串遍历结束,说明完全匹配成功 if (*s2 == '\0') { return (char*)cp; } // 匹配失败,主串匹配起点后移一位 cp++; } // 主串遍历完都没匹配到,返回NULL return NULL; } // 测试代码 int main() { char arr1[] = "abbbacd"; char arr2[] = "bac"; char* p = my_strstr(arr1, arr2); if (p != NULL) { printf("找到子串,后续内容:%s\n", p); } else { printf("找不到子串\n"); } return 0; }4.2 strtok 字符串分割函数
这个函数是 C 语言里专门做字符串分割的工具,用法非常特殊,也是课堂上的重点,有很多必须记住的特性,稍不注意就会踩坑。
4.2.1库函数核心信息
- 函数原型:
char *strtok(char *str, const char *delim); - 核心功能:根据
delim指定的分隔符集合,把str字符串拆分成多个子串 - 必须牢记的关键特性(课堂重点):
- 破坏性修改:
strtok会直接修改原字符串,把分隔符替换成\0,如果需要保留原字符串,必须先拷贝一份,再对拷贝后的内容操作 - 特殊调用规则:首次调用传入待分割的字符串首地址,后续继续分割同一个字符串,第一个参数必须传
NULL,函数会内部记住上一次分割的位置 - 连续分隔符处理:多个连续的分隔符会被视为一个,不会返回空字符串
- 返回值:成功返回当前分割出的子串首地址,没有可分割的子串时返回
NULL
- 破坏性修改:
4.2.2我的测试代码
#define _CRT_SECURE_NO_WARNINGS #include<string.h> #include<stdio.h> int main() { // 待分割的邮箱地址 char arr[] = "wyh@luminous.com"; // 备份原字符串,避免原内容被修改 char arr2[20] = { 0 }; // 分隔符集合:@ 和 . 都是分隔符 const char* delim = "@."; strcpy(arr2, arr); char* res = NULL; // 标准用法:for循环完成全部分割 for (res = strtok(arr2, delim); res != NULL; res = strtok(NULL, delim)) { printf("分割出的子串:%s\n", res); } return 0; }这个例子里,会把邮箱地址拆分成wyh、luminous、com三个子串,非常适合处理格式化的字符串,比如日志、网址、IP 地址、配置项等。
五、错误信息处理函数
写 C 语言代码时,尤其是调用文件操作、内存分配这类标准库函数时,经常会遇到调用失败的情况。想要快速定位失败原因,就离不开strerror和perror这两个错误信息处理函数。
5.1 strerror 错误码转字符串函数
5.1.1库函数核心信息
- 函数原型:
char* strerror ( int errnum ); - 核心功能:传入错误码
errnum,返回对应的错误信息字符串的首地址 - 关键知识点:
- C 语言有一个全局变量
errno,定义在errno.h头文件中,程序启动时errno为 0(无错误);当标准库函数调用失败时,会自动把对应的错误码赋值给errno - 它能把我们看不懂的数字错误码,转换成人类可读的错误描述字符串
- 使用时需要包含
<string.h>和<errno.h>两个头文件
- C 语言有一个全局变量
5.1.2我的测试代码
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h> #include <errno.h> int main() { // 以只读方式打开文件,文件不存在时会打开失败 FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { // 打印错误信息和对应的错误码 printf("打开失败,失败原因是:%s(错误码:%d)",strerror(errno),errno); return 1; } else { printf("文件打开成功"); } return 0; }当文件不存在时,运行结果会输出:打开失败,失败原因是:No such file or directory(错误码:2),能非常直观地看到失败原因。
5.2 perror 直接打印错误信息函数
5.2.1库函数核心信息
- 函数原型:
void perror(const char *s); - 核心功能:可以理解为
strerror的 “懒人版”,直接完成错误信息的打印。它会先打印我们传入的自定义字符串s,然后自动打印冒号、空格,再打印当前errno对应的错误信息,最后自动换行 - 优势:一行代码就能完成错误打印,比
strerror更简洁,适合快速调试
5.2.2我的测试代码
#include <stdio.h> #include <string.h> #include <errno.h> int main() { FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { // 直接打印自定义前缀+错误信息 perror("打开文件失败,原因是"); } else { printf("打开成功"); } return 0; }当文件不存在时,运行结果会输出:打开文件失败,原因是: No such file or directory,完全不用我们手动处理errno和格式化打印,非常方便。
5.2.3两个函数的选择建议
- 如果你只需要把错误信息打印出来调试,直接用
perror,最省事 - 如果你需要把错误信息保存到日志文件、或者做二次处理,就用
strerror获取错误字符串,再做后续操作
六、内存操作函数
前面的字符串函数,只能处理以\0结尾的字符串,一旦遇到整型数组、结构体、浮点型数组这类数据,就无能为力了。而内存操作函数,直接对内存的字节进行操作,完全不关心内存里存的是什么数据类型,通用性极强,是 C 语言里的 “万能操作工具”,所有内存函数都需要包含头文件<string.h>。
6.1 memcpy 内存拷贝函数
6.1.1库函数核心信息
- 函数原型:
void * memcpy ( void * destination, const void * source, size_t num ); - 核心功能:从
source指向的内存位置,拷贝num个字节的数据到destination指向的内存位置 - 核心特点:
- 按字节拷贝,不关心数据类型,任何类型的数据都能处理
- 标准规定:
memcpy不处理重叠的内存,如果源和目标内存有重叠,复制结果是未定义的,重叠场景必须用memmove - 返回目标内存的起始地址
6.1.2我的模拟实现代码
#include<stdio.h> #include<assert.h> // 模拟实现memcpy void* my_memcpy(void* destination, const void* source,size_t num) { // 非空断言 assert(destination && source); // 保存目标内存起始地址,用于返回 char* dest = (char*)destination; const char* src = (const char*)source; // 按字节逐个拷贝 for (size_t i = 0; i < num; ++i) { dest[i] = src[i]; } return destination; } // 测试代码 int main() { // 拷贝整型数组 int arr1[] = { 1,2,3,4,5 }; int arr2[10] = { 0 }; // 拷贝3个int元素,每个int4字节,总共12字节 my_memcpy(arr2, arr1, 12); // 打印拷贝结果 printf("整型数组拷贝结果:"); for(int i=0; i<5; i++){ printf("%d ", arr2[i]); } // 也可以拷贝字符串 char str1[] = "Hello, World!"; char str2[20]; my_memcpy(str2, str1, sizeof(str1)); printf("\n字符串拷贝结果:%s\n", str2); return 0; }实现的核心是把void*强转成char*,因为char类型刚好占 1 个字节,完美适配按字节拷贝的需求,这也是 memcpy 能处理任意数据类型的关键。
6.2 memmove 内存移动函数
memmove是memcpy的 “增强版”,功能和memcpy几乎完全一致,核心的区别就是:memmove 可以安全处理内存重叠的场景,这也是课堂上的重点和难点。
6.2.1什么是内存重叠?
举个例子:有一个数组int arr[] = {1,2,3,4,5,6,7,8,9,10};,我们想把前 5 个元素,拷贝到arr+2的位置(也就是从数组下标 2 开始存放)。这时候源内存是arr[0]-arr[4],目标内存是arr[2]-arr[6],两块内存有重叠。
如果用memcpy从前向后拷贝,拷贝arr[0]到arr[2]、arr[1]到arr[3]之后,原本的arr[2]已经被覆盖成了 1,再拷贝的时候就会拿到错误的数据,最终结果不符合预期。而memmove就是专门解决这个问题的。
6.2.2库函数核心信息
- 函数原型:
void * memmove ( void * destination, const void * source, size_t num ); - 核心功能:拷贝
num个字节的数据,支持源与目标内存重叠的场景 - 实现核心:根据内存重叠的情况,选择不同的拷贝方向
- 当目标地址
dest小于源地址src:从前向后拷贝,和 memcpy 一致,不会覆盖 - 当目标地址
dest大于源地址src,且dest < src + num:内存重叠,必须从后向前拷贝,先拷贝最后一个字节,再往前拷贝,避免覆盖未读取的源数据
- 当目标地址
6.2.3我的模拟实现代码
#include<stdio.h> #include<assert.h> // 模拟实现memmove void* my_memmove(void* destination, const void* source, size_t num) { // 非空断言 assert(destination && source); char* dest = (char*)destination; const char* src = (const char*)source; // 判断是否需要从后向前拷贝 if (dest > src && dest < src + num) { // 内存重叠,从后向前拷贝 for (size_t i = num; i > 0; i--) { dest[i - 1] = src[i - 1]; } } else { // 无重叠,从前向后拷贝 for (size_t i = 0; i < num; i++) { dest[i] = src[i]; } } return destination; } // 测试代码:内存重叠场景 int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; // 重叠内存拷贝:把arr开头的20字节(5个int),拷贝到arr+2的位置 my_memmove(arr + 2, arr, 20); // 打印结果 printf("内存重叠拷贝结果:"); for (int i = 0; i < 10; i++) { printf("%d ", arr[i]); } return 0; }测试代码运行后,会输出1 2 1 2 3 4 5 8 9 10,完全符合预期,完美解决了内存重叠的拷贝问题。另外,Visual studio也把memcpy内存重叠的拷贝问题解决了,但是,并不是所有编译器都能把memcpy做到完美。所以日常开发中,如果你不确定内存是否会重叠,直接用memmove准没错,它的兼容性更强,风险更低。
6.3 memset 内存设置函数
6.3.1库函数核心信息
- 函数原型:
void * memset ( void * ptr, int value, size_t num ); - 核心功能:把
ptr指向的内存的前num个字节,统一设置为value值(函数内部会自动转换为unsigned char) - 必须注意的坑点:它是按字节设置的,这是最容易踩的坑!
- 比如给 int 数组设置初始值,不能写
memset(arr, 1, 20),这样每个字节都会被设为 1,一个 int 有 4 个字节,最终 int 元素的值会变成0x01010101(十进制 16843009),而不是我们想要的 1 - 最安全的用法:把内存块初始化为 0,
memset(arr, 0, sizeof(arr)),因为每个字节设为 0,不管什么类型,最终值都是 0,完全没问题 - 给字符数组批量设置字符,比如
memset(str, 'x', 6),也是完全正确的,因为 char 类型刚好占 1 个字节
- 比如给 int 数组设置初始值,不能写
6.3.2用法示例
#include <stdio.h> #include <string.h> int main () { char str[] = "hello world"; // 把前6个字节设置为'x' memset(str,'x',6); printf("设置结果:%s\n", str); // 输出 xxxxxxworld return 0; }6.4 memcmp 内存比较函数
6.4.1库函数核心信息
- 函数原型:
int memcmp ( const void * ptr1, const void * ptr2, size_t num ); - 核心功能:比较两块内存的前
num个字节,按字节逐位对比,对比规则和strcmp完全一致 - 返回值规则:
- 前
num个字节完全相等,返回0 - 遇到第一个不相等的字节,若
ptr1的字节值更大,返回大于 0的整数,反之返回小于 0的整数
- 前
- 核心优势:可以比较任意类型的数据,比如比较两个结构体、两个整型数组的前 N 个元素,比字符串比较函数的通用性强太多
6.4.2示例
#include <stdio.h> #include <string.h> int main() { char buffer1[] = "DWgaOtP12df0"; char buffer2[] = "DWGAOTP12DF0"; // 比较两个字符串的全部字节 int n = memcmp(buffer1, buffer2, sizeof(buffer1)); if (n > 0) printf("'%s' 大于 '%s'.\n", buffer1, buffer2); else if (n < 0) printf("'%s' 小于 '%s'.\n", buffer1, buffer2); else printf("'%s' 和 '%s' 一样.\n", buffer1, buffer2); return 0; }七、学习总结
这次系统学习下来,我最大的感受就是:C 语言的这些标准库函数,看似调用简单,实则里面藏了超多细节和坑点。光靠背函数用法、记参数,很容易在实际开发中踩坑,只有自己动手去模拟实现一遍,一步步调试,才能真正理解底层的设计逻辑,知道为什么要这么写,使用的时候才能下意识地避开风险。
比如strlen返回值的无符号数陷阱、strncpy不自动补\0的坑、strtok的破坏性修改、memset按字节设置的注意事项、memmove的内存重叠处理,这些知识点,自己写一遍代码、踩一遍坑,就彻底记牢了,比死记硬背高效太多。
这些函数是 C 语言开发的基本功,不管是日常写代码,还是找工作的笔试面试,都是必考的内容。后续我还会继续学习 C 语言的文件操作、结构体、指针进阶、动态内存分配等内容,也会持续把学习笔记、模拟实现代码整理成博客,既是复盘,也希望能和大家一起交流进步。
