【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断言检查destsrc是不是空指针,防止程序崩溃;

我用第一个while循环用来找目标字符串的'\0',找到之后dest就指向'\0'的位置;

我用第二个while循环从'\0'位置开始把源字符串拷过去,'\0'也一起拷,遇到'\0'就停;

最后返回ret,就是目标字符串的起始地址,方便链式调用。


自己给自己追加,会发生什么?

结果:不要这样干,结果是未定义的。

strcat的工作流程是:

strcat的工作流程是先找到目标字符串的'\0',然后从那个位置开始把源字符串拷贝过去。如果destsrc指向同一个字符串,找'\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的使用和注意事项

strncpystrcpy的区别就是多了一个 num数,可以控制拷贝多少个字符。

  1. 拷贝 num 个字符:从源字符串里拷 num 个到目标空间。

  2. 源字符串长度小于 num:拷完源字符串后,目标后面会补'\0',一直补到count个。

  3. 源字符串长度大于等于 num:只拷前 num个,不会自动补'\0',用完后记得自己补。

  4. 目标空间必须足够大且可修改,源和目标不能重叠

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\0xxxxxxxxxxstrlen碰到第一个'\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; }

运行结果:

过程如下:

curstr1的每个字符位置开始,每次让s1=curs2=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去查错误信息,帮你打印成 "前缀: 错误信息" 的格式,省得每次写printfmain里先打一遍 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函数处理。

memmovememcpy都是拷贝内存的,区别就一个拷贝的两块内存如果有重叠,用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的后面并且两块内存有重叠。如果是,就走从后往前拷;如果不是,就走从前往后拷;从后往前拷的时候,把destsrc都移到这一块内存的最后一位,然后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就停

strstrstrtokstrerrorperror都是处理字符串的,遇到\0就认为结束了。


内存操作(不管\0

memcpymemmovememsetmemcmp都是按字节操作内存的,不关心\0,说拷多少字节就拷多少字节,说比多少字节就比多少字节。

易混:

容易混的区别
strstrvsstrtokstrstr找子串位置,strtok切分字符串
strerrorvsperrorstrerror返回字符串,perror直接打印
memcpyvsmemmovememmove能处理重叠,memcpy不行