前言:我们知道在C语言的库中有许许多多的库函数,今天我就来介绍一下自己对两大类库函数中一些常用函数的认识和理解,希望对大家有帮助。
说明:下文中会花较大篇幅实现这些库函数的模拟,请大家不要觉得库函数直接用就好了,模拟没意义。模拟实现不仅可以增强我们对库函数的理解,还能够让我们理解其中实现的原理以及我们可能忽略的一些重要细节,我会在模拟实现各行代码需要处给上注释,所以还请各位能够好好学习一下这些库函数的模拟实现。
目录
目录
一:六大常见字符串函数的操作与模拟
先说明一下:下列这些库函数的使用都需要引用头文件<string.h>,并且希望各位在使用以及模拟实现的过程中打开监视窗口,多多调试看内部的变化,一定会有不一样的收获。
1. strlen函数
(1) 概述
我首先来介绍一下strlen函数:
1. 定义: size_t strlen( const char *string )
2. 返回值:这个函数返回字符串中的字符数,不包括终端的NULL('\0')
3. 参数:由定义知道这个函数的的参数是 string, 其表示一个以空值('\0')结尾的字符串
4. 作用:获取字符串的长度
(2) 模拟实现
strlen函数相对简单,这里不做过多说明,下面用两种不同的方法实现对其的模拟应用。
/*int my_strlen(const char* arr)//方法(1)——计数器
{
assert(arr != NULL);//断言arr不为空指针,防止下列代码出错(先直接应用)
int count = 0;
while (*arr != '\0')
{
count++;
arr++;
}
return count;
}*/
int my_strlen(char* arr)//方法(二)——函数递归
{
assert(arr != NULL);
if (*arr != '\0')
return 1 + my_strlen(arr + 1);
else
return 0;
}
int main()//strlen函数实现的中心思想是指向首元素的指针不断后移,找字符串末尾的'\0'
{
char arr[10] = "ABCDEF";
int ret = my_strlen(arr);
printf("%d\n", ret);
return 0;
}
2. strcpy函数
(1) 概述
接下来介绍的是strcpy函数了,其作用是拷贝字符串,下表详细介绍:
1. 定义: char *strcpy( char* strDestination, const char* strSource )
2. 返回值:这个函数返回目标字符串(strDestination)起始位置的地址
3. 参数:
4. 作用:将 源字符串strSource(包括终止空字符‘\0’)复制到 目标字符串strDestination 指定的位置。如果源字符串和目标字符串重叠,则未定义 strcpy 的行为。
strDestination strSource 目标字符串 以'\0'结尾的源字符串
(2) 模拟实现
char* my_strcpy(char* dest, const char* sour)
{
assert(dest && sour);
char* ret = dest;//存放dest指针的起始状态,以便返回
//(strcpy函数返回的是目标字符串的起始地址),返回值的设定是为了实现链式访问)
//链式访问eg: printf("%s\n", my_strcpy(arr1, arr2))
/*while (*sour != '\0')//1.普通思路
{
*dest = *sour;
dest++;
sour++;
}
*dest = '\0';*/
while (*dest++ = *sour++)//2.简洁代码(后置加加,先将*sour赋值给*dest,再将指针后移)
{
; //循环条件表达式结果为0('\0')时,跳出循环,并且也将'\0'拷贝了,十分巧妙
}
return ret;
}
int main()//实现思想:将源字符串的元素依次拷贝到目标字符串中,拷贝完'\0'后终止
{
char arr1[10] = "XXXXXXX";
char arr2[10] = "ABCDEFGE";
my_strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
注意:strcpy函数会将源字符串末端的'\0'也拷贝到目标字符串。
3. strcat函数
(1) 概述
接下来介绍的是strcat函数了,其与strcpy函数十分相似,下表详细介绍:
1. 定义:char *strcat( char *strDestination, const char *strSource )
2. 返回值:这个函数返回目标字符串(strDestination)起始位置的地址
3. 参数:
strDestination strSource 目标字符串 以'\0'结尾的源字符串 4. 作用:strcat函数将 源字符串strSource追加(可以理解为拷贝)到目标字符串strDestination,并用空字符('\0')终止生成的字符串。strSource 的初始字符会覆盖 strDestination 的终止空字符。
5. 说明:这里有一点要强调的是,如果使用strcat函数时用字符串自己的内容给自己追加时(即strcat(arr1,arr1)),会导致Bug的发生,故这里提醒一下如果要对字符串追加自己的一部分时,strcat不适用,但是可以使用函数strncat,这里不具体讲解(留给大家自己去了解),但是下面在讲完函数strstr后会展示一道与其有关的小题,大家可以了解一下。
(2) 模拟实现
char* my_strcat(char* dest, const char* sour)
{
assert(dest && sour);
char* ret = dest;//存放首地址作为返回值
while (*dest)
{
dest++;//寻找目标字符串的'\0'
}
while (*dest++ = *sour++)//找到后开始追加,与strcpy相似
{
;
}
return ret;
}
int main()
{
char arr1[20] = "abcdef";//目标空间要足够大!!!
char arr2[10] = "ABC";
my_strcat(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
注意:源字符必须以'\0'结束;目标空间足够大并且可修改(不能是常量字符串)。
4.strcmp函数
(1) 概述
接下来介绍的是strcmp函数了,下表详细介绍:
1. 定义:int strcmp( const char* string1, const char* string2 );
2. 返回值:这个函数的返回值是一个整数,指示字符串 1 与字符串 2 的字典关系 (字符串逐字母比较)
注意:VS编译器下,< 0对应值规定为 -1,> 0对应的值规定为 1。
3. 参数:
返回值 字符串关系 < 0 str1 <str2 0 str1 = str2 > 0 str1 > st2
string1 string2 要比较的以'\0'结尾的字符串
要比较的以'\0'结尾的字符串
4. 作用:strcmp函数可以对两个以'\0'结尾的字符串进行比较
(2) 模拟实现
int my_strcmp(const char* arr1, const char* arr2)
{
assert(arr1 && arr2);
while (*arr1 == *arr2)首字母相等
{
if (*arr1 == '\0')//两字符串都为空字符串或首位均为'\0'
{
return 0;
}
arr1++;
arr2++;//继续向后比较
}
return (*arr1 - *arr2);//不相等直接返回差值判断大小
}
int main()
{
char arr1[10] = "azed";
char arr2[10] = "azee";
int ret = my_strcmp(arr1,arr2);
printf("%d\n", ret);
return 0;
}
注意:我在此再次说明一下,在VS编译器下,< 0对应返回值规定为 -1,> 0对应返回值规定为 1;但是我在模拟实现的过程中没有单纯的以VS环境下编写。
5.strtok函数
(1) 概述
接下来介绍的是strtok函数了,下表详细介绍:
1. 定义:char *strtok( char* strToken, const char* strDelimit )
2. 返回值:被分割字符串的首元素地址
3. 参数:
strToken strDelimit 包含标志(分隔)符的字符串
分隔符集(自己定义的) 4. 作用:当遇到分隔符时会将之前的字符串分割并将该分隔符替换成NULL
(2) strtok函数的使用
int main()
{
char arr[] = "ab.cd,sedr@dsf";//strToken
const char* Del = ",.@";//strDelimit
char* ret = strtok(arr, Del);
for (ret;ret != NULL;ret = strtok(NULL, Del))
//将分隔符转换成NULL后再将此位置作为起始位置继续向后找分隔符
{
printf("%s\n", ret);//打印出所有被分割的字符串
}
return 0;
}
6. strstr函数(重点)
相较于上面五个函数而言,我觉得strstr函数是最重要也是模拟实现起来最难的一个,据我现在了解,它的实现与我们后面要学习的一些知识有关,还请各位重视起来,接下来就要开始介绍它了。
(1) 概述
1. 定义:char *strstr( const char* string, const char* strCharSet )
2. 返回值:这个函数返回一个指针,指向string字符串中第一次出现的strCharSet首字符的位置,如果 strCharSet 未出现在字符串中,则返回 NULL。如果 strCharSet 指向长度为零的字符串,则该函数返回string字符串。
3.参数:
string strCharSet 要搜索的以'\0'结尾的字符串(相当于母串) 要查找的以'\0'结尾的字符串(相当于子串) 4. 作用:在字符串string中查找字符串strCharSet
(2) strstr函数的使用
int main()
{
char arr1[10] = "abbbcdef"; //string
char arr2[5] = "bbc"; //strCharSet
char* ret = strstr(arr1, arr2); //在"abbbcdef"中查找"bbc"
if (ret == NULL) //查不到子串就返回NULL
{
printf("找不到了\n");
}
else
{
printf("%s\n", ret);//找到了返回一个字符指针,打印从该位置开始的字符串
}
return 0;
}
(3) 模拟实现strstr函数
char* my_strstr(char* str1, char* str2)
{
assert(str1 && str2);
char* s1 = str1;
char* s2 = str2;
//str1与str2作为一个起点,不移动,能够使s1,s2在匹配过程中未完全成功时返回初始位置重新开始匹配
char* cur = str1; //cur用来记录匹配成功的起始位置!!!
while (*cur) //*cur=='\0'时目标字符串遍历完毕,跳出循环
{
s1 = cur; //与cur++搭配使用,使得首位匹配失败后能继续向后匹配(或)匹配了但未成功,将s1退回到开始匹配处的后一位(cur+1)
s2 = str2; //匹配了但未成功,将s2回归首位重新进行匹配
while (*s1 == *s2) //匹配首位成功后继续向后匹配
{
s1++;
s2++;
}
if (*s2 == '\0')//子串移动到\0处即匹配成功,返回匹配的起始位置(cur)
{
return cur;
}
cur++; //与(char* cur = str1)配合,每次匹配不成功就将s1后移一位
}
return NULL;
}
int main()
{
char arr1[10] = "abbbcdef";
char arr2[5] = "bbc";
char* ret = my_strstr(arr1, arr2); //在"abbbcdef"中查找"bbc"
if (ret == NULL) //查不到子串就返回NULL
{
printf("找不到了\n");
}
else
{
printf("%s\n", ret);
}
return 0;
}
这个模拟实现只是在一个字符串中查找另一个字符串的经典普通的实现方法,还有一种较为高效的实现方法——KMP算法,有兴趣的可以去了解一下。这样我对这个函数的理解分享就到这里结束了,希望对大家有帮助。
(4) 小题一道:
写一个函数,判断一个字符串是否为另外一个字符串旋转之后的字符串
eg:
1.AABCD左旋一个字符得到ABCDA
2.AABCD左旋两个字符得到BCDAA
3.AABCD右旋一个字符得到DAABC
int Is_turn(char* arr1, char* arr2)
{
assert(arr1 && arr2);
int len1 = (int)strlen(arr1);
int len2 = (int)strlen(arr2);
if (len1 != len2)//长度不等时一定不满足条件
return 0;
strncat(arr1, arr1,len1);//用strncat在arr1后面追加一个arr1
char* ret = strstr(arr1, arr2);//追加完后,如果arr2是arr1的字串,则arr2一定是由arr1旋转得到的
if (ret == NULL)
return 0;
else
return 1;
}
int main()//应用strstr与strncat函数
{
char arr1[20] = "AABCD";
char arr2[20] = "ABCDA";
int ret = Is_turn(arr1, arr2);
if (ret == 1)
printf("yes\n");
else
printf("no\n");
return 0;
}
这是实现这道题的一种比较好的方法,利用到了这篇文章中讲到的两个库函数,如果你用正常的思路去写这个代码的话,会复杂很多。
我对这些常用的字符串操作函数就介绍到这里,希望大家可以好好理解一下,同时有问题的地方也请各位能在评论区留言,我们一起进步,谢谢大家。接下来就是对三个内存操作函数的介绍了。
二:四大内存操作函数的介绍与模拟
接下来就是对四个内存操作函数(memcpy,memmove,memcmp,memset)的介绍了。
提示:由于这是内存操作函数,所以在调试过程中观察内存变化十分重要,我也会对内存的变化进行一定的展示,但最重要的还是各位自己调试感受一下。
1. memcpy函数
(1) 概述
这个函数与strcpy相似,区别在于它规定了具体拷贝的字节数,下面具体说明:
1. 定义:void *memcpy( void *dest, const void *src, size_t count )
2. 返回值:memcpy返回指针dest(指向dest字符串的起始位置)
3. 参数:
dest src count 新的缓冲区 要从中复制的缓冲区
要复制的字符(字节)数
4. 作用:memcpy 函数将 src 的计数字节(即count,由使用者自己决定)复制到 dest。
(2) memcpy函数的使用
内存变化展示:
memcpy函数调用前:
memcpy函数调用后:
告诫:能够清晰的观察前后两次变化的前提条件是知道调试和小端存储的概念。
观察分析前后两次变化可以知道,memcpy函数是逐字节复制实现其功能的,知道这个后我们就可以很好的对其进行模拟实现了。
(3) memcpy函数的模拟实现
void* my_memcpy(void* dest, const void* src, size_t count)//void*类型可以接收所有类型的指针
{
assert(dest && src);
void* ret = dest;//保存返回值
while (count--)
{
*(char*)dest = *(char*)src; //强制转换成char*类型,解引用后访问一个字节
dest = (char*)dest + 1;
src = (char*)src + 1; //dest,src指针都后移一个字节继续将src的内容复制到dest中
}
return ret;
}
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[10] = { 0x11111111,0x22222222,0x33333333,0x1144 };//0x表示16进制数
int sz = sizeof(arr1) / sizeof(arr1[0]);
my_memcpy(arr2, arr1, 13);
int i = 0;
for (i = 0;i < sz;i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
注意:这个模拟实现无法完成对两个有重叠部分字符串的拷贝(主要是自己对自己复制)(下面还会介绍)。
执行结果:
到这里,我对memcpy函数的介绍就结束了,但是还有一点是,在memcpy函数出现的早期,其无法完成对有重叠部分的两个字符串的拷贝,所以出现了另一个完善这个功能的函数——memmove,其作用与memcpy几乎完全一致(在当前编译器下已经一样了),接下来就来介绍一下这个函数。
2. memmove函数
(1) 概述
1. 定义:void *memmove( void *dest, const void *src, size_t count )
2. 返回值:memcpy返回指针dest(指向dest字符串的起始位置)
3. 参数:
dest src count 目标对象 源对象 要复制的字符(字节)数
4. 作用:memmove 函数将count个字节数从 src 复制到 dest。如果源区域(src)的某些区域与目标区域(dest)重叠,memmove 可确保在覆盖之前复制重叠区域中的原始字节(即可以实现对重叠部分的复制)。
(2) memmove函数的使用
(3) memmove函数的模拟实现
这个函数的模拟实现是基于memcpy函数模拟实现之上的,我们需要考虑的就是如何使重叠部分也完成拷贝。
画图分析:
如图,当源空间的起始位置指向3时,向后拷贝五个整型数据(20个字节)到目标空间时,目标空间的起始地址有以下三种情况,同时对应着三种不同的拷贝方式:
1. 当dest位于dest1左边时,要使拷贝不覆盖,就要以源空间首元素3的第一个字节为起点并且不断后移,将各个字节拷贝到目标空间的对应字节处。
2. 当dest位于dest1与dest2之间时,由分析可知,要使拷贝不覆盖,就要以源空间末元素7的最后一个字节为起点并且不断前移,将各个字节拷贝到目标空间的对应字节处。
3. 当dest位于dest2的右边时,很显然,无论从左向右拷贝还是从右向左拷贝,只要确认好起点,将源空间的字节逐个拷贝到目标空间的对应字节处即可。
这里为了方便实现,我将方式2与方式3并为一种(即都是从右向左拷贝),而方式一则不同(从左向右拷贝),当然还有另一种思路大家可以自己思考实现一下。
代码实现及相关注释:
void* my_memmove(void* dest, const void* src, size_t count)
{
assert(dest && src);
void* ret = dest;//保存返回值
if (dest < src)//从左向右拷贝
{
while (count--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;//强转为char*,解引用后逐字节拷贝
}
}
else//从右向左拷贝
{
//在一串连续的数据中,要从第一个字节跳转到最后一个字节,就让第一个字节的地址加上(数据总字节数-1)
while (count--)//所以这里count进入循环时恰好是(count-1),使得跳转到空间的最后一个字节
{
*((char*)dest + count) = *((char*)src + count);
}
}//count在循环中一直减1,使指针从两空间的最后一个字节处不断前移,完成逐字节拷贝
return ret;
}
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr1) / sizeof(arr1[0]);
my_memmove(arr1 + 2, arr1, 20); //对自己的一部分(将3,4,5,6,7改成1,2,3,4,5)进行拷贝更改
int i = 0;
for (i = 0;i < sz;i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
这样,memmove函数的模拟实现就结束了。
3. memcmp函数
(1) 概述
这个函数与strcmp相似,区别在于它规定了进行比较的字节数,下面具体说明:
1. 定义:int memcmp( const void *buf1, const void *buf2, size_t count )
2. 返回值:memcpy返回一个表示buf1与buf2关系的值
返回值 两空间关系 < 0 buf1 <buf2 0 buf1 = buf2 > 0 buf1 > buf2 3. 参数:
buf1 buf2 count 缓冲区1 缓冲区2 要进行比较的字符(字节)数
4. 作用:memcmp函数从 buf1 和 buf2 的第一个计数字节开始进行比较,若相等则继续向后一字节进行比较,直至某字节大小不同或比较了count字节,并返回一个指示它们关系的值。
(2) memcmp函数的使用与模拟
int my_memcmp(void* arr1, void* arr2, int count)
{
assert(arr1 && arr2);
char* s1 = arr1;
char* s2 = arr2;
while (*s1 == *s2 && --count)//(--count):比较一个字节时不用移动s1与s2
{
s1++;
s2++;
}
return *s1 - *s2;
}
int main()
{
int arr1[5] = { 1,2,3,5,1 };
int arr2[5] = { 1,2,3,5,257 };//要知道十进制怎么转为十六进制(可以在调试过程中认真分析)
//int ret = memcmp(arr1, arr2, 17);
int ret = my_memcmp(arr1, arr2, 17);
printf("%d\n", ret);
return 0;
}
4. memset函数
(1) 概述
1. 定义:void *memset( void *dest, int c, size_t count );
2. 返回值:memset 返回 dest 的值。
3. 参数:
4. 作用: memset函数可以将指针dest指向的目标空间的count个字节数设置为字符c。
dest c count 指向起始位置的指针 要设置的字符 要设置的字符(字节)数
(2) memset函数的使用
结语
这样,我就把六个字符串操作函数与四个内存操作函数介绍完了,内容还是比较丰富且重要的。总结完后我自己是收获满满的,也希望各位可以认真阅读一番,得到一些不一样的收获。再见,各位。