【C语言】《C 语言库函数源码级别的理解:模拟实现字符串与内存操作全系列(含最常用12个模拟实现)》
字符函数和字符串函数
引言
C语言开发过程中,最频繁的操作之一就是处理字符串。不管是用户输入、文件读取、还是网络数据解析,都绕不开各种字符串操作。C语言中对字符和字符串的处理非常频繁,但C语言本身没有字符串类型,字符串通常放在字符数组中,或者放在常量字符串中。常量字符串是只读的,适用于那些不需要修改字符串内容的函数。使用字符串函数时要注意几点:目标空间必须足够大、还得确保字符串有
'\0'结尾等。
一.字符操作
1.字符分类函数
C语⾔中有⼀系列的函数是专⻔做字符分类的,也就是⼀个字符是属于什么类型的字符的。
这些函数的使⽤都需要包含⼀个头⽂件是 ctype.h
这些函数的使⽤⽅法⾮常类似,我们就讲解⼀个函数的事情,其他的⾮常类似:
int islower ( int c );islower 是能够判断参数部分的 c 是否是⼩写字⺟的。通过返回值来说明是否是⼩写字⺟,如果是⼩写字⺟就返回⾮0的整数,如果不是⼩写字⺟,则返回0。
练习:
写⼀个代码,将字符串中的⼩写字⺟转⼤写,其他字符不变。
#include <stdio.h> #include <ctype.h> int main() { int i = 0; char str[] = "Test String.\n"; char c; while (str[i]) { c = str[i]; if (islower(c)) c -= 32; putchar(c); // 输出当前字符 i++; // 移到下一个字符 } return 0; }运行如下
isupper是 C 语言标准库<ctype.h>里的函数,用来判断一个字符是否是大写字母('A'到'Z')。如果是大写字母,返回非零值(真);如果不是,返回 0(假)。
2. 字符转换函数
C语⾔提供了2个字符转换函数:
int tolower ( int c ); //将参数传进去的⼤写字⺟转⼩写 int toupper ( int c ); //将参数传进去的⼩写字⺟转⼤写上⾯的代码,我们将⼩写转⼤写,是-32完成的效果,有了转换函数,就可以直接使⽤ tolower 函
数。
#include <stdio.h> #include <ctype.h> int main () { int i = 0; char str[] = "Test String.\n"; char c; while (str[i]) { c = str[i]; if (islower(c)) c = toupper(c); putchar(c); i++; } return 0; }运行结果:
3. strlen 的使⽤和模拟实现
strlen - C++ Reference
- 字符串以 '\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前⾯出现的字符个数(不包含 '\0' )。
- 参数指向的字符串必须要以 '\0' 结束。
- 注意函数的返回值为 size_t,是⽆符号的( 易错 )
- strlen的使⽤需要包含头⽂件
代码如下:
#include <stdio.h> #include <string.h> int main() { const char* str1 = "abcdef"; const char* str2 = "bbb"; if (strlen(str2) - strlen(str1) > 0) { printf("str2>str1\n"); } else { printf("srt1>str2\n"); } return 0; }这段代码想比较两个字符串的长度,但写的时候有个问题。
strlen返回的是size_t,也就是无符号整数,两个无符号数相减的结果还是无符号的,永远不可能是负数。所以strlen(str2) - strlen(str1) > 0这个条件实际上一直为真,因为 3 减 6 会变成一个大正数,不是 -3。比较长度直接用>比较就行了。
三种模拟实现方法:
//方式1:计数器方式 int my_strlen(const char* str) { int count = 0; assert(str); while (*str) { count++; str++; } return count; } //方式2:递归方式(不创建临时变量) int my_strlen(const char* str) { assert(str); if (*str == '\0') return 0; else return 1 + my_strlen(str + 1); } //方式3:指针-指针方式 int my_strlen(const char* str) { assert(str); const char* p = str; while (*p != '\0') p++; return (int)(p - str); } int main() { char arr[] = "hello zifuhanshu"; size_t count = my_strlen(arr); printf("%zu\n", count); return 0; }方式1:计数器
就是从头到尾数一遍,遇到'\0'停下,数了几个就是几个。方式2:递归
判断当前字符是不是'\0',是就返回0,不是就返回1 + 自己调用自己(往后移一位)。代码看起来挺简洁的,但实际用的话字符串太长容易把栈搞爆,平时写代码不推荐。方式3:指针-指针
把头指针记下来,再弄一个指针p从头走到尾,走到'\0'停下,然后p - 头指针就是中间的字符个数。
二.长度不受限制的字符串函数
1.strcpy 的使⽤和模拟实现
strcpy - C++ Reference
char* strcpy(char * destination, const char * source );把源字符串(包含结尾的
'\0')整个复制到目标空间里。
- 源字符串必须以 '\0' 结束。
- 会将源字符串中的 '\0' 拷⻉到⽬标空间。
- ⽬标空间必须⾜够⼤,以确保能存放源字符串。
- ⽬标空间必须可修改。即目标空间 dest 不可以被 const 声明。
strcpy的模拟实现:
#include <stdio.h> #include <assert.h> // 提供 assert 断言 // 自己实现字符串拷贝函数 char* my_strcpy(char* dest, const char* src) { assert(dest && src); // 检查指针是否为空,防止传入空指针导致崩溃 char* ret = dest; // 保存目标地址,后面 dest 会移动,用于最后返回 while (*dest++ = *src++) { // 先执行 *dest = *src(赋值) ; // 然后 dest++、src++ 同时往后移一位 } // 判断赋值后的字符是不是 '\0' // 是 '\0' 就停止,不是就继续 return ret; // 返回目标字符串的起始地址 } int main() { char arr1[] = "hello world"; // 源字符串 char arr2[20] = "xxxxxxxxxxxxxx"; // 目标空间,初始全为 x my_strcpy(arr2, arr1); // 调用拷贝函数 printf("%s\n", arr2); // 打印拷贝后的结果 return 0; }运行结果:
解析:
while (*dest++ = *src++) { ; }先赋值
*dest = *src,然后dest++、src++同时后移,判断赋值后的字符是不是'\0',是就停。'\0'也会被拷贝过去。等价:
while (*src != '\0') { *dest = *src; dest++; src++; } *dest = '\0';
2.strcat 的使⽤和模拟实现
strcat - C++ Reference
char* strcat(char* destination, const char* source);
strcat是字符串拼接函数,把源字符串追加到目标字符串的末尾,返回目标空间的起始地址。源字符串必须以 '\0' 结束。
- ⽬标字符串中也得有 \0 ,否则没办法知道追加从哪⾥开始。
- ⽬标空间必须有⾜够的⼤,能容纳下源字符串的内容。
- ⽬标空间必须可修改。即目标空间 dest 不可以被 const 声明。
模拟实现strcat函数:
代码如下:
#include <stdio.h> #include <assert.h> #inlcude<stdio.h> char* my_strcat(char* dest, const char* src) { char* ret = dest; assert(dest != NULL); assert(src != NULL); while (*dest) { dest++; } while ((*dest++ = *src++)) { ; } return ret; } int main() { char arr1[20] = "hello "; char arr2[] = "world"; my_strcat(arr1, arr2); printf("%s\n", arr1); return 0; }运行结果:
过程如下:
先把
dest的起始地址保存到ret,因为后面dest会移动,最后要返回起始地址;
assert断言检查dest和src是不是空指针,防止程序崩溃;我用第一个
while循环用来找目标字符串的'\0',找到之后dest就指向'\0'的位置;我用第二个
while循环从'\0'位置开始把源字符串拷过去,'\0'也一起拷,遇到'\0'就停;最后返回
ret,就是目标字符串的起始地址,方便链式调用。
自己给自己追加,会发生什么?
结果:不要这样干,结果是未定义的。
strcat的工作流程是:
strcat的工作流程是先找到目标字符串的'\0',然后从那个位置开始把源字符串拷贝过去。如果dest和src指向同一个字符串,找'\0'的时候dest会一直走到末尾,然后开始拷贝。问题是拷贝过程中源数据被自己覆盖掉了,原来的'\0'没了,循环停不下来,一直越界访问直到程序崩溃。不同的编译器表现可能不一样,但不管怎样都不建议这么写。
运行结果:
3. strcmp 的使⽤和模拟实现
strcmp - C++ Reference
strcmp是 C 语言里用来比较两个字符串的函数strcmp(const char* str1, const char* str2)它的作用就是逐个字符比较两个字符串,从第一个字符开始比,相等就往后继续,直到比出大小或者遇到
'\0'。标准规定:
- 第⼀个字符串⼤于第⼆个字符串,则返回⼤于0的数字
- 第⼀个字符串等于第⼆个字符串,则返回0
- 第⼀个字符串⼩于第⼆个字符串,则返回⼩于0的数字
strcmp函数的模拟实现:
#include <stdio.h> #include <assert.h> int my_strcmp(const char* str1, const char* str2) { assert(str1 && str2); // 检查空指针 while (*str1 == *str2) // 字符相等就继续往后比 { if (*str1 == '\0') // 两个都走到末尾了,说明完全相等 return 0; str1++; str2++; } return *str1 - *str2; // 不相等就返回差值(>0说明str1大,<0说明str2大) } int main() { char arr1[] = "abq"; char arr2[] = "abcdef"; int ret = my_strcmp(arr1, arr2); printf("%d\n", ret); return 0; }运行结果:
那么如何判断两个字符串?
答案是:⽐较两个字符串中对应位置上字符ASCII码值的⼤⼩。
char* p1 = "abcdef"; char* p2 = "aqwer"; int ret = strcmp(p1, p2); printf("%d\n", ret);
strcmp比较两个字符串的时候,从第一个字符开始逐个往后比。第一对'a'和'a'相等,继续比第二对;第二对'b'和'q'不相等,'b'的 ASCII 是98,'q'是113,'b' - 'q'是 -15,所以返回负数,表示p1 < p2。返回值不一定是 -1 或 1,只要正负号对就行,具体数值不同编译器可能不一样。另外
strcmp是区分大小写的,'A'和'a'不一样
补充:
不同编译器的返回值差异
strcmp的返回值在不同编译器下表现不一样:
VS2019:大于返回 1,等于返回 0,小于返回 -1
Linux-gcc:大于返回正数(比如 14),等于返回 0,小于返回负数(比如 -14)
C 标准只规定了正数、0、负数三种情况,具体数值不固定,所以不要依赖具体的返回值,只判断正负和是否为零就行。
三.长度受限制的字符串函数
1.strncpy 函数的使⽤
char * strncpy ( char * destination, const char * source, size_t num );strncpy - C++ Reference
strncpy的使用和注意事项
strncpy和strcpy的区别就是多了一个 num数,可以控制拷贝多少个字符。
拷贝 num 个字符:从源字符串里拷 num 个到目标空间。
源字符串长度小于 num:拷完源字符串后,目标后面会补
'\0',一直补到count个。源字符串长度大于等于 num:只拷前 num个,不会自动补
'\0',用完后记得自己补。目标空间必须足够大且可修改,源和目标不能重叠
strncyp函数的模拟实现:
#include <stdio.h> #include <assert.h> char* my_strncpy(char* dest, const char* src, size_t num) { assert(dest && src); char* ret = dest; size_t i = 0; // 1. 拷贝 count 个字符,遇到 src 的 '\0' 就提前停 while (i < num && src[i] != '\0') { dest[i] = src[i]; i++; } // 2. 如果 src 长度小于 count,后面补 '\0' while (i < num) { dest[i] = '\0'; i++; } return ret; } int main() { char arr1[] = "abcdef"; char arr2[20] = "xxxxxxxxxx"; my_strncpy(arr2, arr1, 3); printf("%s\n", arr2); return 0; }运行结果:
过程如下:
my_strncpy先拷数据,再补'\0'。先用while (i < num && src[i] != '\0')把源字符串的前 num 个字符拷过去,但如果中途遇到'\0'就直接停,不继续拷了;第二步如果拷完还没到 num个,说明源字符串比 num 短,剩下的位置全部补'\0',直到补满num 个。
2.strncat 函数的使⽤
char * strncat ( char * destination, const char * source, size_t num );strncat是长度受限的字符串拼接函数,把源字符串的前num个字符追加到目标字符串的末尾,然后自动补一个'\0',返回目标空间的起始地址。
strncat - C++ Reference
- 如果源字符串的长度小于 count,则只复制 '\0' 之前的内容。
strncat的模拟实现:
#include <stdio.h> #include <assert.h> char* my_strncat(char* dest, const char* src, size_t num) { assert(dest && src); char* ret = dest; // 1. 找到目标字符串的末尾('\0'的位置) while (*dest != '\0') { dest++; } // 2. 拷贝 num 个字符,遇到 src 的 '\0' 就提前停 while (num-- && (*dest++ = *src++)) { ; } // 3. 手动补上 '\0' *dest = '\0'; return ret; } int main() { char arr1[] = "abcdef"; char arr2[20] = "xx\0xxxxxxxxxx"; int len = strlen(arr1); my_strncat(arr2, arr1, len); printf("%s\n", arr2); return 0; }运行结果:
过程如下:
arr2初始是xx\0xxxxxxxxxx,strlen碰到第一个'\0'就停了,所以len = 6,但arr2的有效长度只到第2个字符。my_strncat先找到arr2的'\0'位置(也就是第2个字符后面),然后从那里开始把arr1的6个字符"abcdef"拷贝过去,最后补上'\0'。所以结果是xxabcdef,后面的"xxxxxxxxxx"就被覆盖掉了。
3. strncmp函数的使⽤
int strncmp ( const char * str1, const char * str2, size_t num );是 C 语言里用来比较两个字符串前num个字符的函数
strncmp - C++ Reference
⽐较str1和str2的前num个字符,如果相等就继续往后⽐较,最多⽐较num个字⺟,如果提前发现不⼀样,就提前结束,⼤的字符所在的字符串⼤于另外⼀个。如果num个字符都相等,就是相等返回0.
strncmp的模拟实现:
#include <stdio.h> #include <assert.h> int my_strncmp(const char* str1, const char* str2, size_t num) { assert(str1 && str2); // 如果 num 为 0,直接返回 0 if (num == 0) return 0; // 逐个比较前 num 个字符 while (num-- && *str1 && *str2 && *str1 == *str2) { str1++; str2++; } // 如果 num 减到了 0,说明前 num 个字符都相等,返回 0 if (num == 0) return 0; // 否则返回 ASCII 差值 return *str1 - *str2; } int main() { char arr1[] = "abcdef"; char arr2[] = "abqdefghi"; int ret = my_strncmp(arr1, arr2, 2); printf("%d\n", ret); return 0; }运行结果:
过程如下:
strncmp比较的是字符的 ASCII 码值。当比较到第三对字符时,arr1是'c',arr2是'q',两个字符不相等,函数返回*str1 - *str2,也就是'c' - 'q'。查一下 ASCII 表,'c'是 99,'q'是 113,99 减 113 等于-14。负数说明str1小于str2。
四.字符串查找
1.strstr 的使⽤和模拟实现
char * strstr ( const char * str1, const char * str2);在长字符串str1里找子字符串str2,返回第一次出现的位置指针
strstr - C++ Reference
- Returns a pointer to the first occurrence of str2 in str1, or a null pointer if str2 is not part of str1.(函数返回字符串str2在字符串str1中第⼀次出现的位置)。
- The matching process does not include the terminating null-characters, but it stops there.(字符串的⽐较匹配不包含 \0 字符,以 \0 作为结束标志)。
strstr的模拟实现:
#include <stdio.h> #include <assert.h> char* my_strstr(const char* str1, const char* str2) { assert(str1 && str2); // 防止传入空指针 // 如果 str2 是空字符串,直接返回 str1 if (*str2 == '\0') return (char*)str1; const char* cur = str1; // cur 记录当前从 str1 的哪个位置开始尝试匹配 while (*cur != '\0') { const char* s1 = cur; // 每次都从当前位置开始 const char* s2 = str2; // 每次重新从头匹配子串 while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2) { s1++; s2++; } // 如果 s2 走到了末尾,说明完全匹配 if (*s2 == '\0') return (char*)cur; // 否则从 str1 的下一个位置重新尝试 cur++; } return NULL; } int main() { char arr1[] = "abbbcdef"; char arr2[] = "bbc"; char* ret = my_strstr(arr1, arr2); if (ret == NULL) printf("找不到\n"); else printf("找到:%s\n", ret); return 0; }运行结果:
过程如下:
用
cur从str1的每个字符位置开始,每次让s1=cur、s2=str2,然后逐字符比较,如果str2全比完了都相等就返回cur,否则cur++换下一个位置继续试,直到str1走完还没找到就返回NULL。
2. strtok 函数的使⽤
char * strtok ( char * str, const char * sep);是按你给的符号(比如@或,)把字符串“切开”,一刀一刀地切出每一块。
strtok - C++ Reference
- sep参数指向⼀个字符串,定义了⽤作分隔符的字符集合
- 第⼀个参数指定⼀个字符串,它包含了0个或者多个由sep字符串中⼀个或者多个分隔符分割的标记。
- strtok函数找到str中的下⼀个标记,并将其⽤ \0 结尾,返回⼀个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以被strtok函数切分的字符串⼀般都是临时拷⻉的内容并且可修改。)
- strtok函数的第⼀个参数不为 NULL ,函数将找到str中第⼀个标记,strtok函数将保存它在字符串中的位置。
- strtok函数的第⼀个参数为 NULL ,函数将在同⼀个字符串中被保存的位置开始,查找下⼀个标记。
- 如果字符串中不存在更多的标记,则返回 NULL 指针。
strtok的模拟实现:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h> int main() { char arr[] = "hejiaqi@yeah.net@hehe"; char arr2[30] = { 0 }; strcpy(arr2, arr); const char* sep = "@."; char* ret = NULL; for (ret = my_strtok(arr2, sep); ret != NULL; ret = my_strtok(NULL, sep)) { printf("%s\n", ret); } // ret = my_strtok(arr2, sep); // printf("%s\n", ret); // ret = my_strtok(NULL, sep); // printf("%s\n", ret); // ret = my_strtok(NULL, sep); // printf("%s\n", ret); return 0; }运行结果:
ret = strtok(arr2, sep); // 第一次:切出 "hejiaqi" printf("%s\n", ret); // 打印 "hejiaqi" ret = strtok(NULL, sep); // 第二次:切出 "yeah" printf("%s\n", ret); // 打印 "yeah" ret = strtok(NULL, sep); // 第三次:切出 "net" printf("%s\n", ret); // 打印 "net"五.错误信息报告
strerror 函数的使⽤
char* strerror ( int errnum );strerror - C++ Reference
strerror 函数可以把参数部分错误码对应的错误信息的字符串地址返回来。
在不同的系统和C语⾔标准库的实现中都规定了⼀些错误码,⼀般是放在 errno.h 这个头⽂件中说明的,C语⾔程序启动的时候就会使⽤⼀个全局的变量errno来记录程序的当前错误码,只不过程序启动的时候errno是0,表⽰没有错误,当我们在使⽤标准库中的函数的时候发⽣了某种错误,就会将对应的错误码,存放在errno中,⽽⼀个错误码的数字是整数很难理解是什么意思,所以每⼀个错误码都是有对应的错误信息的。strerror函数就可以将错误对应的错误信息字符串的地址返回
strerror的模拟实现
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h> #include <errno.h> // 自己写的strerror char* my_strerror(int errnum) { // 把常见的错误码和错误信息存起来 static char* err[] = { "No error", // 0 "Operation not permitted", // 1 "No such file or directory", // 2 "No such process", // 3 "Interrupted system call", // 4 "Input/output error", // 5 "No such device or address", // 6 "Argument list too long", // 7 "Exec format error", // 8 "Bad file descriptor", // 9 "No child processes" // 10 }; if (errnum < 0 || errnum > 10) return "未知错误"; return err[errnum]; } // 自己写的perror void my_perror(const char* str) { if (str != NULL) { printf("%s: %s\n", str, my_strerror(errno)); } else { printf("%s\n", my_strerror(errno)); } } int main() { // 先打印一下错误码对应的信息 int i = 0; printf("错误码对照表\n"); for (i = 0; i <= 10; i++) { printf("%d: %s\n", i, my_strerror(i)); } printf("\n"); // 测试一下打开不存在的文件 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { printf("用strerror打印: %s\n", my_strerror(errno)); } printf("\n"); // 再用perror打印一次 pf = fopen("test.txt", "r"); if (pf == NULL) { my_perror("打开失败"); } return 0; }过程如下:
my_strerror整了个数组把错误码和错误信息对应存起来,传数字进去就返回对应的字符串。my_perror就是包了一层,传个前缀进去,它自己拿errno去查错误信息,帮你打印成 "前缀: 错误信息" 的格式,省得每次写printf。main里先打一遍 0 到 10 的错误码看看都有啥,然后打开一个不存在的文件,errno自动变成 2,分别用俩函数打印一下 "No such file or directory",看看自己写的好不好使。
内存函数
1.memcpy 使⽤和模拟实现
void * memcpy ( void * destination, const void * source, size_t num );memcpy就是按字节把一块内存的数据原封不动拷到另一块内存去。
- 函数memcpy从source的位置开始向后复制num个字节的数据到destination指向的内存位置。
- 这个函数在遇到 '\0' 的时候并不会停下来。
- 如果source和destination有任何的重叠,复制的结果都是未定义的。
memcpy代码模拟实现
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h> #include <assert.h> void* my_memcpy(void* dest, const void* src, size_t num) { assert(dest && src); void* ret = dest; char* d = (char*)dest; const char* s = (const char*)src; while (num--) { *d++ = *s++; } return ret; } int main() { int arr1[] = { 1,2,3,4,5,6,7,8,9,10 }; int arr2[20] = { 0 }; my_memcpy(arr2, arr1, 5 * sizeof(int)); for (int i = 0; i < 5; i++) { printf("%d ", arr2[i]); } printf("\n"); return 0; }运行结果:
过程如下:
main里定义俩数组,arr1有1~10,arr2开20个空间全0。调my_memcpy(arr2, arr1, 5*sizeof(int)),拷20个字节过去。
进函数先assert检查指针不是空,把arr2起始地址存ret里留着返回。把dest和src强转成char*,方便一个字节一个字节搬。while循环20次,每次把s指向的字节赋给d指向的位置,然后俩指针都往后挪。20个字节拷完刚好5个int。
返回ret回到main,打印arr2前5个数,出来就是1 2 3 4 5
代码演示过程:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h> struct S { char name[20]; int age; }; int main() { struct S arr3[] = { {"张三", 20}, {"李四", 30} }; struct S arr4[3] = { 0 }; // 把arr3的内容拷到arr4里去 memcpy(arr4, arr3, sizeof(arr3)); // 打印看看拷过去没有 for (int i = 0; i < 2; i++) { printf("名字:%s,年龄:%d\n", arr4[i].name, arr4[i].age); } return 0; }2. memmove 使⽤和模拟实现
void * memmove ( void * destination, const void * source, size_t num );memmove - C++ Reference
- 和memcpy的差别就是memmove函数处理的源内存块和⽬标内存块是可以重叠的。
- 如果源空间和⽬标空间出现重叠,就得使⽤memmove函数处理。
memmove和memcpy都是拷贝内存的,区别就一个:拷贝的两块内存如果有重叠,用memmove才安全,用memcpy会出错。比如数组
[1,2,3,4,5,6,7,8,9,10],想把前5个数[1,2,3,4,5]挪到从第3个位置开始放,就成了[1,2,1,2,3,4,5,8,9,10],这时候源头(1到5)和目标(3到7)有交叉,用memcpy拷着拷着就把后面还没拷的数给覆盖了,结果就不对。memmove能处理这种情况,它会判断一下是从前往后拷还是从后往前拷,保证数据不错。
memmove模拟实现
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <assert.h> void* my_memmove(void* dest, const void* src, size_t num) { assert(dest && src); void* ret = dest; char* d = (char*)dest; const char* s = (const char*)src; // 判断有没有重叠 // 如果 dest 在 src 后面,并且 dest 和 src 有重叠,就从后往前拷 if (d > s && d < s + num) { // 从后往前拷 d = (char*)dest + num - 1; s = (const char*)src + num - 1; while (num--) { *d-- = *s--; } } else { // 没有重叠,或者 dest 在 src 前面,从前往后拷 while (num--) { *d++ = *s++; } } return ret; } int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; // 把前5个数拷到从第3个位置开始(下标2) my_memmove(arr + 2, arr, 5 * sizeof(int)); for (int i = 0; i < 10; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; }运行结果:
过程如下:
进函数先检查两个指针不是空,然后把目标起始地址存到
ret里,最后要返回它;判断从前往后拷还是从后往前拷。判断条件是看目标地址dest是不是在源头src的后面并且两块内存有重叠。如果是,就走从后往前拷;如果不是,就走从前往后拷;从后往前拷的时候,把dest和src都移到这一块内存的最后一位,然后while循环从后往前一个一个字节搬,每搬完一次两个指针都往前挪一位;从前往后拷的时候,直接从起始位置一个一个字节往后搬就行了;循环结束,返回存好的目标起始地址ret;回到
main打印数组,数组变成1 2 1 2 3 4 5 8 9 10。
3.memset 函数的使⽤
void * memset ( void * ptr, int value, size_t num );memset - C++ Reference
- memset是⽤来设置内存的,将内存中的值以字节为单位设置成想要的内容。
- memset 是以字节为单位设置内存的。
memset模拟实现
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h> int main() { int arr[10] = { 0 }; // 整一个数组,10个位置都初始成0 // 把前20个字节全部改成1 memset(arr, 1, 20); // 打印看看变成啥样了 for (int i = 0; i < 10; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; }运行结果:
4. memcmp 函数的使⽤
int memcmp ( const void * ptr1, const void * ptr2, size_t num );memcmp - C++ Reference
memcmp就是按字节比较两块内存的前num个字节,和strcmp的区别是它遇到\0不会停,一定要把num个字节全比完。
memcmp 不同于 strcmp,memcmp 遇到 '\0' 不会停止比较。
- ⽐较从ptr1和ptr2指针指向的位置开始,向后的num个字节
- 返回值如下:
memcmp模拟实现
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h> int main() { float a[] = { 1.0, 2.0, 3.0, 4.0 }; float b[] = { 1.0, 3.0 }; // 比8个字节,看a和b谁大 int ret = memcmp(a, b, 8); printf("%d\n", ret); return 0; }运行结果:
这个代码就是拿两个数组比一下前8个字节,看谁大谁小。
a里面放了4个float:1.0, 2.0, 3.0, 4.0,b里面放了2个float:1.0, 3.0。memcmp(a, b, 8)就是从头开始比8个字节,一个float占4个字节,8个字节刚好比两个float。
先比第一个,1.0和1.0一样,再比第二个,2.0和3.0,2.0比3.0小,所以memcmp返回一个负数,ret就是负数,打印出来也肯定是负数。
函数对比
| 函数 | 功能 | 比较/拷贝单位 | 遇到\0停止? | 处理内存重叠? | 主要特点 |
|---|---|---|---|---|---|
| strstr | 找子串第一次出现的位置 | 字符 | 停止 | 不涉及 | 专门处理字符串 |
| strtok | 按分隔符切字符串 | 字符 | 停止 | 不涉及 | 会修改原字符串,用静态变量记住位置 |
| strerror | 错误码转错误信息 | - | 不涉及 | 不涉及 | 传入数字返回对应的字符串 |
| perror | 打印错误信息 | - | 不涉及 | 不涉及 | 自动拿 errno,省得自己写 printf |
| memcpy | 拷贝内存 | 字节 | 不停止 | 不处理 | 不管类型,按字节拷,不重叠时用 |
| memmove | 拷贝内存 | 字节 | 不停止 | 能处理 | 和 memcpy 一样,但能处理重叠 |
| memset | 设置内存 | 字节 | 不停止 | 不涉及 | 把每个字节设成同一个值 |
| memcmp | 比较内存 | 字节 | 不停止 | 不涉及 | 按字节比,\0也照比不误 |
字符串专用(遇到
\0就停)
strstr、strtok、strerror、perror都是处理字符串的,遇到\0就认为结束了。内存操作(不管
\0)
memcpy、memmove、memset、memcmp都是按字节操作内存的,不关心\0,说拷多少字节就拷多少字节,说比多少字节就比多少字节。
易混:
| 容易混的 | 区别 |
|---|---|
strstrvsstrtok | strstr找子串位置,strtok切分字符串 |
strerrorvsperror | strerror返回字符串,perror直接打印 |
memcpyvsmemmove | memmove能处理重叠,memcpy不行 |
