前言:第一部分先简单介绍一下常用字符串函数和内存函数,第二部分再重点介绍重要函数的的模拟实现。若日后再发现某些好用或者有意思的库函数,都会在本文中进行更新。
一、常用库函数介绍
1. strlen
(1)函数声明:
size_t strlen ( const char * str );
(2)函数介绍:
- 字符串以
'\0'
作为结束标志,strlen函数返回的是在字符串中'\0'
前面出现的字符个数(不包含 ‘\0’ )。 - 参数指向的字符串必须要以
'\0'
结束。 - 注意函数的返回值为
size_t
,是无符号的
一个容易犯的使用错误,请看:
#include <stdio.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;
}
结果看似会输出str1 > str2
,但实际输出的是str2 > str1
。
解释:strlen(str2)
的结果为3,strlen(str1)
的结果为6,3 - 6
的结果若用整型表示确实是-3,但strlen函数的返回值为无符号整型,而从无符号的视角来看,-3的原码、反码和补码相等,故直接将-3转换为对应的十进制数,这将是一个非常大的正数,故最终输出结果为str2 > str1
。
2. strcpy
(1)函数声明
char* strcpy(char * destination, const char * source );
(2)函数介绍
- 函数的功能是:将source指向的字符串中的内容拷贝到指针destination中并返回。
- 源字符串(source指向的字符串)必须以
'\0'
结束。 - 函数会将源字符串中的
'\0'
拷贝到目标空间。 - 目标空间(destination指向的空间)必须足够大,以确保能存放源字符串。
- 目标空间必须可变。
3. strcat
(1)函数声明
char * strcat ( char * destination, const char * source );
(2)函数介绍
- 函数的功能是:将source指向的字符串中的内容追加到指针destination中并返回。
- 源字符串(source指向的字符串)必须以
'\0'
结束。 - 目标空间(destination指向的空间)必须足够大,以确保能追加源字符串的内容。
- 目标空间必须可修改
- 注意当字符串需要自己给自己追加时,不能使用这个函数。因为字符串中原来的结束条件
'\0'
会被覆盖。后面会在模拟实现部分进一步说明。
4. strcmp
(1)函数声明
int strcmp ( const char * str1, const char * str2 );
(2)函数介绍
- 函数的功能是:对两个字符串中的内容进行逐字符比较,以整数的形式放回比较的结果
- 标准规定返回的结果为:
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字
5. strncpy
(1)函数声明
char * strncpy ( char * destination, const char * source, size_t num );
(2)函数介绍
- 函数的功能是:在函数strcpy的基础上实现可指定拷贝的字节数,即从源字符串拷贝num个字符到目标空间。
- 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边继续拷贝0(
'\0'
),直到num个
6. strncat
(1)函数声明
char * strncat ( char * destination, const char * source, size_t num );
(2)函数介绍
- 函数的功能是:在函数strcpy的基础上实现可指定追加的字节数,即从源字符串追加num个字符到目标空间。
- 若追加的字节数大于源字符串的长度,则以源字符串为基准,即源字符串中的
'\0'
为最后一个追加的字符
7. strncmp
(1)函数声明
int strncmp ( const char * str1, const char * str2, size_t num );
(2)函数介绍
- 函数的功能是:在函数strcmp的基础上实现可指定比较的字节数
- 这里无需过度关注比较的字节数是否超过了两个字符串的长度,因为最多在比较到最长那一个字符串的结束标志
'\0'
时,一定会得出比较的结果。
8. strstr
(1)函数声明
char * strstr ( const char *str1, const char * str2);
(2)函数介绍
- 函数的功能是:在主串(str1)中匹配子串(str2),若匹配成功则返回子串在主串的位置(第一个匹配的字符的地址,通过该地址能得到主串后面所有的内容),若匹配失败则返回一个空指针。
如:str1:abbcdaada;str2:cda;调用函数后返回cdaada
9. strtok
(1)函数声明
char * strtok ( char * str, const char * sep );
(2)函数介绍
- 函数的功能是:将一个带有指定分隔符的字符串,以分隔符为基准,将一个字符串分成多个字符串。
- sep参数是个字符串,定义了用作分隔符的字符集合
- 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标
记 - strtok函数找到str中的一个标记后会将其替换为
'\0'
,并返回一个指向以该'\0'
为结束标志的字符串的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。) - 当strtok函数的第一个参数不为 NULL 时 ,函数将找到str中的第一个标记,strtok函数内部将保存它在字符串中的位置,以便函数自身进行进一步地查找。
- 当strtok函数的第一个参数为 NULL 时,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
- 如果字符串中不存在更多的标记,则返回 NULL 指针。
(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"
的地址;
10. strerror
(1)函数声明
char * strerror ( int errnum );
(2)函数介绍
- 函数的功能是:返回错误码所对应的错误消息的字符串的地址,通过打印函数即可输出错误信息。
C语言的库函数在运行的时候,如果发生错误,就会将错误码存在一个变量中,这个变量是:errno;错误码是一些数字:1 2 3 4 5 不同的错误码对应不同的错误信息,如:
#include <stdio.h>
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 <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
若没有相关文件,程序运行结果就为:
11. memcpy
(1)函数声明
void * memcpy ( void * destination, const void * source, size_t num );
(2)函数介绍
- 函数的功能是:从source的位置开始向后复制num个字节的数据到destination的内存位置。
- 内容的拷贝是逐字节进行的。
- 这个函数在遇到 ‘\0’ 的时候并不会停下来。
- 如果source和destination有任何的重叠,复制的结果都是未定义的,即无法自身拷贝自身。
12. memmove
(1)函数声明
void * memmove ( void * destination, const void * source, size_t num );
(2)函数介绍
- 和memcpy的区别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
- 如果源空间和目标空间出现重叠,就得使用memmove函数处理
13. memcmp
(1)函数声明
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
(2)函数介绍
- 函数的功能是:比较从ptr1和ptr2指针开始的num个字节
- 标准规定返回的结果为:
第一个指针中的内容大于第二个指针中的内容,则返回大于0的数字
第一个指针中的内容大于第二个指针中的内容,则返回0
第一个指针中的内容大于第二个指针中的内容,则返回小于0的数字
14. memset
(1)函数声明
void *memset( void *dest, int c, size_t count );
(2)函数介绍
- 函数的功能是:将目标空间(dest)中的内容,根据给定的字节数(count)改为指定的内容(c)
- 需要注意的是,该函数是以字节为单位进行更改的。
若写成如下形式,就会出现问题,请看:
int arr[10] = {0};
memset(arr,1,40);
代码的本意是将arr中的元素全置为1,但实际效果是将每个元素的每个字节都置为了1,及十六进制下每个元素的值为01 01 01 01
,转换为十进制将会是一个比较大的数。
通过调试我们可以清楚看到:
- 所以一般都以0作为指定更改的内容。
二、重要库函数的模拟实现
前言:在了解了一些库函数的用法后,去尝试模拟实现还是比较有价值的,就像阅读一本书其实就是在和作者交流一样,尝试模拟实现库函数就像和函数设计者对话,感受他们在编写函数时所展现出的编程思想。那么,让我们开始吧。
1. 模拟strlen
(1)实现思路:根据上面对函数功能的介绍,我们只需统计一个字符串中'\0'
之前出现的字符的次数即可。而统计的方法具体又可以分为三种:
- 创建计数变量,利用循环进行计数,直到遇到’\0’循环停止,返回计数变量;
- 利用递归,只要字符串中当前字符不是’\0’,则进行递归调用,即返回当前字符加上下一次递归调用的结果,即:
return (1 + sim_strlen(str+1))
(PS:其中str为字符串,sim_strlen为模拟的函数) - 创建一个指针,通过循环找到’\0’后进行指针的相减运算来得到两指针间的元素个数。
(2)实现代码如下,请看:
//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;
else
return 0;
}
//3 指针相减
size_t sim_strlen3(const char* str)
{
assert(str);
const char* p = str;
while (*p++)
{
;
}
return (p - str - 1);
}
2. 模拟strcpy
(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'
的拷贝。
3. 模拟strcmp
(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的数字); - 若不是上述情况,则逐字符进行对比,相等则跳过,直到不同的字符,返回两字符串中当前字符相减的结果(大于0的数字或小于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
}
最后的返回语句通过两个逻辑表达式的结果做差,实现了将返回值具体化。
4. 模拟strcat
(1)实现思路:先在目标字符串(destination)中找到追加的位置,即目标字符串结尾’\0’处,然后从’\0’开始进行字符串的追加即可。
(2)实现代码如下,请看:
char* sim_strcat(char* destination, const char* source)
{
//1. 找到目标空间的\0
//注意这里就不能把*destination++直接作为循环条件,会跳过一个\0
char* ret = destination;
while (*destination)
{
destination++;
}
//2. 追加字符串
while (*destination++ = *source++)
{
;
}
return ret;
}
补充:上面在介绍strcat函数的时候说过,若想实现对自身的拷贝,不能使用这个函数。因为由模拟实现的过程可以看出,我们是从目标字符串中'\0'
的位置开始进行追加的,若目标字符串与源字符串是同一个字符串,那么在追加的时候'\0'
就被覆盖了,也就是说,追加的字符串(源字符串)中已经没有字符串结束标志'\0'
了,此时若进行追加将可能造成程序的死循环。
示意图参考:
5. 模拟strstr
(1)实现思路:从子串的第一个字符与主串的第一个字符进行对比,若相同,则进行下一个字符的对比,直至子串到达'\0'
,表示匹配成功;若不同,子串退回到第一个字符,从主串的下一个字符开始与主串进行对比;若直至主串到达'\0'
都没有匹配成功,则返回一个空指针。
(PS:这里采用的是BF算法,可用KMP算法进行优化,后面会再单独写一篇对其进行介绍)
参考示意图:
- 假设子串为abb,主串为aabcabbca
第一趟:
子串第二个字符与主串不匹配,子串退回到第一个字符,从主串的下一个字符与主串进行对比,
故第二趟对比表现在图中就是:
子串的第三个字符与主串不匹配,子串退回到第一个字符,从主串的下一个字符与主串进行对比,之后的每一趟就都是这个过程:
第三趟:
第四趟:
第五趟:
匹配成功,返回子串在主串中的位置,也就是该位置首字符的地址,在上图中返回的就是字符串"abbca"
首字符的地址
(2)实现代码,请看:
//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 == *p2
while (*p1 == *p2 && *p1 != '\0' && *p2 != '\0') //单字符匹配成功则匹配下一个字符
{
p1++;
p2++;
}//跳出这层循环只有如下两种可能:
if (*p2 == '\0') //匹配成功,返回开始比较的位置
return (char*)start;
//单趟匹配失败,子串退回到第一个字符,进行从主串的下一个字符开始与主串匹配
p1 = start + 1;
start = p1;
p2 = s2;
}
return NULL; //跳出外层循环说明匹配失败
}
6. 模拟memcpy
这里先对第一部分的介绍做一个补充,由第一部分的介绍,我们知道函数声明为:
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;
}
7. 模拟memmove
前言:由第一部分的介绍我们知道,用memcpy
函数拷贝自身内容的行为是未定义的。即可能会造成错误。
例如:数组arr的中的内容为1 2 3 4 5 6 7 8 9 10;当想将1 2 3 4 5(源空间)拷贝到3 4 5 6 7(目标空间)的位置上时:
- 将1拷贝到3的位置,没问题;
- 将2拷贝到4的位置,没问题;
- 想将3拷贝到5的位置时就出现问题了,因为数组中原来的3因为前面的拷贝而被覆盖了。
出现问题的原因是:memcpy所实现的拷贝都是 “从前向后” 的拷贝(即从源空间的第一个位置开始覆盖目标空间中对应第一个开始的内容);而如上例子若想正确拷贝需要 “从后向前” 拷贝(即从源空间的最后一个位置开始覆盖目标空间中对应最后一个开始的内容),即拷贝的过程应为: - 将5拷贝到7的位置,没问题;
- 将4拷贝到6的位置,没问题;
- 将3拷贝到5的位置,没问题;
- 将2拷贝到4的位置,没问题;
- 将1拷贝到3的位置,没问题;
- 拷贝完成,最终结果为:1 2 1 2 3 4 5 8 9 10
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;
}
本章完。
看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或有误地方的地方还恳请过路的朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹