前言:第一部分先简单介绍一下常用字符串函数和内存函数,第二部分再重点介绍重要函数的的模拟实现。若日后再发现某些好用或者有意思的库函数,都会在本文中进行更新。
(1)函数声明:
size_t strlen ( const char * str );
(2)函数介绍:
'\0'
作为结束标志,strlen函数返回的是在字符串中 '\0'
前面出现的字符个数(不包含 ‘\0’ )。'\0'
结束。size_t
,是无符号的#include
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;
}
结果看似会输出str1 > str2
,但实际输出的是str2 > str1
。
解释:strlen(str2)
的结果为3,strlen(str1)
的结果为6,3 - 6
的结果若用整型表示确实是-3,但strlen函数的返回值为无符号整型,而从无符号的视角来看,-3的原码、反码和补码相等,故直接将-3转换为对应的十进制数,这将是一个非常大的正数,故最终输出结果为str2 > str1
。
(1)函数声明
char* strcpy(char * destination, const char * source );
(2)函数介绍
'\0'
结束。'\0'
拷贝到目标空间。(1)函数声明
char * strcat ( char * destination, const char * source );
(2)函数介绍
'\0'
结束。'\0'
会被覆盖。后面会在模拟实现部分进一步说明。(1)函数声明
int strcmp ( const char * str1, const char * str2 );
(2)函数介绍
(1)函数声明
char * strncpy ( char * destination, const char * source, size_t num );
(2)函数介绍
'\0'
),直到num个(1)函数声明
char * strncat ( char * destination, const char * source, size_t num );
(2)函数介绍
'\0'
为最后一个追加的字符(1)函数声明
int strncmp ( const char * str1, const char * str2, size_t num );
(2)函数介绍
'\0'
时,一定会得出比较的结果。(1)函数声明
char * strstr ( const char *str1, const char * str2);
(2)函数介绍
(1)函数声明
char * strtok ( char * str, const char * sep );
(2)函数介绍
'\0'
,并返回一个指向以该'\0'
为结束标志的字符串的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)(3)使用示例
int main()
{char* p = "abc@def#ghi$jk";const char* sep = "#$@";char arr[30];char* str = NULL;strcpy(arr, p);//将数据拷贝一份,处理arr数组的内容for (str = strtok(arr, sep); str != NULL; str = strtok(NULL, sep)){printf("%s\n", str);}
}
运行结果:
说明:一般通过for循环来使用这个函数,因为在使用时第一次需要传需查找的字符串的地址,往后几次传的则是空指针。
第一次循环:arr中字符串的内容变为"abc\0def#ghi$jk"
,返回字符串"abc\0"
的地址;
第二次循环:arr中字符串的内容变为"abc\0def\0ghi$jk"
,返回字符串"def\0"
的地址;
第三次循环:arr中字符串的内容变为"abc\0def\0ghi\0jk"
,返回字符串"ghi\0"
的地址;
第四次循环:arr中字符串的内容为"abc\0def\0ghi\0jk"
,返回字符串"jk\0"
的地址;
(1)函数声明
char * strerror ( int errnum );
(2)函数介绍
#include
int main()
{printf("%s\n", strerror(0));printf("%s\n", strerror(1));printf("%s\n", strerror(2));printf("%s\n", strerror(3));printf("%s\n", strerror(4));printf("%s\n", strerror(5));return 0;
}
输出结果:
上面的代码段仅展示用,如上述,错误码其实是存储在变量errno
中,所以实际在使用时,是将errno
直接作为函数strerror
的参数,请看:
(3)使用示例:
#include
int main()
{//打开文件FILE* pf = fopen("test.txt", "r");if (pf == NULL){printf("%s\n", strerror(errno));return 1;}//关闭文件fclose(pf);pf = NULL;return 0;
}
若没有相关文件,程序运行结果就为:
(1)函数声明
void * memcpy ( void * destination, const void * source, size_t num );
(2)函数介绍
(1)函数声明
void * memmove ( void * destination, const void * source, size_t num );
(2)函数介绍
(1)函数声明
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
(2)函数介绍
(1)函数声明
void *memset( void *dest, int c, size_t count );
(2)函数介绍
int arr[10] = {0};
memset(arr,1,40);
代码的本意是将arr中的元素全置为1,但实际效果是将每个元素的每个字节都置为了1,及十六进制下每个元素的值为01 01 01 01
,转换为十进制将会是一个比较大的数。
通过调试我们可以清楚看到:
前言:在了解了一些库函数的用法后,去尝试模拟实现还是比较有价值的,就像阅读一本书其实就是在和作者交流一样,尝试模拟实现库函数就像和函数设计者对话,感受他们在编写函数时所展现出的编程思想。那么,让我们开始吧。
(1)实现思路:根据上面对函数功能的介绍,我们只需统计一个字符串中'\0'
之前出现的字符的次数即可。而统计的方法具体又可以分为三种:
return (1 + sim_strlen(str+1))
(PS:其中str为字符串,sim_strlen为模拟的函数)//1 循环迭代的方法
size_t sim_strlen1(const char* str)
{assert(str);int count = 0;while (*str){count++;str++;}return count;
}//2 递归调用
size_t sim_strlen2(const char* str)
{assert(str);if (*str)return sim_strlen2(str + 1) + 1;elsereturn 0;
}//3 指针相减
size_t sim_strlen3(const char* str)
{assert(str);const char* p = str;while (*p++){;}return (p - str - 1);
}
(1)实现思路:根据对函数功能的描述,进行对源字符串(source)中字符的逐个拷贝,考虑到对最后'\0'
的拷贝,我们可以将赋值语句直接作为循环的条件,具体请看实现代码:
(2)实现代码:
char* sim_strcpy(char* destination, const char* source)
{char* ret = destination;assert(destination && source);while (*destination++ = *source++){;}return ret;
}
说明:我们直接将赋值语句*destination++ = *source++
作为了循环的条件,这样就能在末尾'\0'
的拷贝完成后循环刚好结束,若写成:
while (*destination && *source){*destination++ = *source++;}
在循环结束时还需要额外进行一次'\0'
的拷贝。
(1)实现思路:对两个字符串中的字符逐个进行对比,按规定返回对比结果,即:
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字
(2)实现代码:
int sim_strcmp(const char* s1, const char* s2)
{assert(s1 && s2);while (*s1 == *s2 && *s1 != '\0' && *s2 != '\0'){s1++;s2++;}return *s1 - *s2;
}
说明:
"\0"
,则不进行循环,直接返回两字符串中当前字符相减的结果(大于0的数字或小于0的数字);补充:也可以将返回的小于零的数字具体为-1;将大于零的数字具体为1
如下VS编译器中strcmp的源码:
int __cdecl strcmp (const char * src,const char * dst)
{int ret = 0 ;while((ret = *(unsigned char *)src - *(unsigned char *)dst) == 0 && *dst){++src, ++dst;}return ((-ret) < 0) - (ret < 0); // (if positive) - (if negative) generates branchless code
}
最后的返回语句通过两个逻辑表达式的结果做差,实现了将返回值具体化。
(1)实现思路:先在目标字符串(destination)中找到追加的位置,即目标字符串结尾’\0’处,然后从’\0’开始进行字符串的追加即可。
(2)实现代码如下,请看:
char* sim_strcat(char* destination, const char* source)
{//1. 找到目标空间的\0//注意这里就不能把*destination++直接作为循环条件,会跳过一个\0char* ret = destination;while (*destination){destination++;}//2. 追加字符串while (*destination++ = *source++){;}return ret;
}
补充:上面在介绍strcat函数的时候说过,若想实现对自身的拷贝,不能使用这个函数。因为由模拟实现的过程可以看出,我们是从目标字符串中'\0'
的位置开始进行追加的,若目标字符串与源字符串是同一个字符串,那么在追加的时候'\0'
就被覆盖了,也就是说,追加的字符串(源字符串)中已经没有字符串结束标志'\0'
了,此时若进行追加将可能造成程序的死循环。
示意图参考:
(1)实现思路:从子串的第一个字符与主串的第一个字符进行对比,若相同,则进行下一个字符的对比,直至子串到达'\0'
,表示匹配成功;若不同,子串退回到第一个字符,从主串的下一个字符开始与主串进行对比;若直至主串到达'\0'
都没有匹配成功,则返回一个空指针。
(PS:这里采用的是BF算法,可用KMP算法进行优化,后面会再单独写一篇对其进行介绍)
参考示意图:
"abbca"
首字符的地址//BF匹配
char* sim_strstr(const char* s1, const char* s2)
{assert(s1 && s2);if (*s2 == '\0') //子串为\0{return (char*)s1;}const char* p1 = s1;const char* p2 = s2;const char* start = s1; //用于记录开始比较的位置while (*p1){ //以防主串可能小于子串,循环条件不能只为*p1 == *p2while (*p1 == *p2 && *p1 != '\0' && *p2 != '\0') //单字符匹配成功则匹配下一个字符{p1++;p2++;}//跳出这层循环只有如下两种可能:if (*p2 == '\0') //匹配成功,返回开始比较的位置return (char*)start;//单趟匹配失败,子串退回到第一个字符,进行从主串的下一个字符开始与主串匹配p1 = start + 1;start = p1;p2 = s2;}return NULL; //跳出外层循环说明匹配失败
}
这里先对第一部分的介绍做一个补充,由第一部分的介绍,我们知道函数声明为:
void * memcpy ( void * destination, const void * source, size_t num );
说明:因为我们不确定将来要用函数来处理什么样类型的数据,所以将该函数参数设计为了void*
型,用以接收不同类型的数据。最后一个参数num则是实现数据拷贝的关键,因为这里涉及到关于char*
类型指针的妙用。
(PS:关于这部分知识点的详细介绍感兴趣的朋友们可以参考一下博主的这篇文章:【逐步剖C】-第八章-指针进阶-上,在文章的最后讲解模拟实现qsort库函数时进行了较详细的讲解,这里就不再做过多说明啦)
(1)实现思路:
将数据转换为char*型,然后再根据第三个参数num就能够借助循环来实现逐字节地进行内容的拷贝。
(2)实现代码如下,请看:
void* memcpy(void* dest, const void* src, size_t nums)
{int i = 0;for (i = 0; i < nums; i++){*((char*)dest + i) = *((char*)src + i);}return (char*)dest;
}
前言:由第一部分的介绍我们知道,用memcpy
函数拷贝自身内容的行为是未定义的。即可能会造成错误。
例如:数组arr的中的内容为1 2 3 4 5 6 7 8 9 10;当想将1 2 3 4 5(源空间)拷贝到3 4 5 6 7(目标空间)的位置上时:
memmove
函数与memcpy
函数的区别就在于:memmove
函数会根据函数的拷贝情况决定是 “从前向后” 拷贝,还是 “从后向前” 拷贝;而memcpy
函数只能实现 “从前向后” 拷贝。
memmove函数决定拷贝方式的标准为:
当目标空间的地址小于源空间的地址时,采用的是 “从前向后” 拷贝的方式。
示意图参考:
3拷贝到1的位置,4拷贝到2的位置…以此类推。
当目标空间的地址大于源空间的地址时,采用的是 “从后向前” 拷贝的方式。
示意图参考:
5拷贝到7的位置,4拷贝到6的位置…以此类推。
(1)实现思路:由上面介绍可知,“从前向后” 拷贝的过程其实就和memcpy函数一模一样;那么“从后向前” 拷贝本质上就只是对循环的改变,即让循环从最后一个字节开始往第一个字节拷贝内容。
(2)实现代码如下,请看:
void* memmove(void* dest, const void* src, size_t nums)
{//本质上分两种情况int i = 0;if (dest < src) //从前向后{for (i = 0; i < nums; i++){*((char*)dest + i) = *((char*)src + i);}}else //从后向前{for (i = nums - 1 ; i >= 0; i--) {*((char*)dest + i) = *((char*)src + i);}//要换20个字节,但注意是从nums-1开始,到最后i=0时进行最后一次拷贝}return (char*)dest;
}
本章完。
看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或有误地方的地方还恳请过路的朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹
下一篇:7 Seata简介