前言:
在上篇文章中,博主已经介绍了部分字符串函数(上篇链接:C/C++学习之路之字符串函数、字符函数、内存操作函数(上)_暮影从柯的博客-CSDN博客)。这期博主将介绍剩下的字符串函数,以及字符函数和内存操作函数。
目录
一、strstr()介绍及其模拟实现
1.库函数strlen()介绍
//头文件引用:<string.h>
const char* strstr(const char* str1, const char* str2)
{
//函数功能:在str1指向的字符串中寻找str2指向的字符串第一次出现的地址
//参数str1 - 主字符串
//参数str2 - 子字符串
//返回类型char* - 子串在主串中第一次出现的地址
}
//注意事项:
//主串中找到子串,返回找到的地址;找不到,返回NULL
示例:
2.模拟实现
const char* my_strstr(const char* str1, const char* str2)
{
const char* p1;//指向主串
const char* p2;//指向子串
const char* cp;//指向主串匹配的起点
assert(str1 && str2);
if (*str2 == '\0')
return str1;
cp = str1;
while (*cp)//一次匹配
{
p1 = cp;
p2 = str2;
while (*p1 && *p2 && *p1 == *p2)//匹配过程
{
p1++;
p2++;
}
if (*p2 == '\0')//匹配成功
return cp;
cp++;
}
return NULL;
}
解释:
1.原理:我们采用的是暴力算法实现strstr。我们先定义一个指针cp,cp表示每次主串子串匹配时主串开始的位置。再定义两个指针p1,p2,分别指向主串和子串匹配的字符。之后开始匹配,若该次匹配失败,则cp++,指向主串下次匹配的位置,再让p1=cp,p2回退到指向第一个字符,再次匹配。直到匹配成功,返回地址;或cp指向'\0',表示结束,此时匹配失败,返回NULL。
2. 代码解释:若str2指向的是'\0',则为空字符串,直接返回str1。先让cp指向主串的第一个字符。外面的循环,也就是第一个while,表示一次匹配,当cp不指向'\0'就进行一次匹配。之后让p1=cp表示配对的起点,p2指向子串的第一个字符。里面的循环,也就是第二个while,表示字符配对。当*p1==*p2,表示指向字符相同,p1、p2都+1,继续配对下一个字符。当至少存在*p1=='\0'、*p2=='\0'、*p2!=*p1其中一种情况是跳出循环。此时再检查*p2是否为'\0',*p2=='\0'表示匹配成功,返回匹配起点cp。若不等于'\0',就说明是其他两种情况,都属于匹配失败。此时cp++,指向下一次匹配的起点。当最后都没有匹配成功时就说明找不到,返回NULL。
对于strstr()的实现,还可以用KMP算法。但本篇篇幅有限,若评论区有较多人想要了解或学习KMP算法,我会尽快写一篇博客讲解。
二、strtok()介绍
//头文件引用:<string.h>
char* strtok(char* str, const char* delimiters)
{
//函数功能:根据分隔符对字符串进行分隔
//参数str - 被分隔的字符串
//参数delimiters - 指向分隔符的集合
//返回类型char* - 指向分隔后的字符串
}
//函数原理及用法:第一次调用时,传入需要分隔的字符串和分隔符集合,函数会从第一个字符开始浏览,当找到第一个非分隔符的字符时,记为起点,之后继续浏览,找到分隔符时,将分隔符改为'\0',返回起点的地址。
之后的调用中,第一个参数只需要传入NULL,而函数会从上次的分隔符后一位开始浏览,找到起点终点,返回起点的地址。
当函数遇到'\0'后,返回起点的地址,并且,在之后的调用中,函数都只会返回NULL。
示例:
三、字符函数
字符函数包括字符分类函数和字符转换函数。他们都需要引用头文件<ctype.h>,并且他们的参数和返回类型都是int。
我们先讲一下字符分类函数。
字符分类函数就是你输入一个整型数字(输入字符也行,因为字符在内存中是以ASCII码值存储的),函数会按照ASCII码值找到该数字对应的字符,如果该字符符合函数要求,返回真(非0数字),不符合,返回假(0)。
由于他们的用法类似,只是对参数字符的需求不一。我们用一张表格表示:
函数 | 如果它的参数满足下列条件就返回真 |
isalnum | 任何控制字符 |
isalpha | 字母a~z或A~Z |
islower | 小写字母a~z |
isupper | 大写字母A~Z |
iscntrl | 任何控制字符 |
isdigit | 十进制数字0~9 |
isxdigit | 十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F |
isblank | 制表符'\t'、空格' ' |
isspace | 空白字符:空格' ',换页'\f',换行'\n',回车'\r',制表符'\t'或者垂直制表符'\v' |
isprint | 任何可打印字符,包括图形字符和空白字符 |
ispunct | 标点符号,任何不属于数字或者字母的图形字符(可打印) |
isgraph | 任何图形字符 |
还有字符转换函数:tolower()、toupper()。
当参数为'A'~'Z'时,tolower会返回参数的小写的ASCII码值。若参数不为'A'~'Z',则返回参数的值。
同理,当参数为'a'~'z'时,toupper会返回参数的大写的ASCII码值。若不满足,则返回参数的值。
示例:
刚才讲的这些都是关于字符和字符串的函数。而对于某些操作,例如复制,有时候也需要在其他类型上面实现。所以下面我会介绍一些内存操作函数,这些函数可以对内存字节进行直接操作。
四、memcpy()介绍及其模拟实现
1.库函数memcpy()介绍
//头文件引用:<string.h>
void* memcpy(void* destination, const void* source, size_t num)
{
//函数功能 - 将source指向的前num个字节的内容复制到destination指向的空间
//参数destination - 复制的目标空间
//参数source - 被复制的内容
//参数num - 被复制的字节个数
}
//注意事项:
//source和destination的空间必须≥num,防止出现溢出
//source和destination指向的空间不能有重叠,否则复制的结果都是未定义(等下解释)
//复制的单位是字节,不同类型的数据其所占空间大小不同
示例:
2.模拟实现
void* my_memcpy(void* dest, const void* src, size_t sz)
{
assert(dest && src);
char* p = (char*)dest;//用p保存dest并用来遍历复制,dest不变,作为返回值
while (sz--)//每次循环复制一个字节,循环sz次
{
*p = *(char*)src;
p++;
src = (char*)src + 1;
}
return dest;
}
解释:memcpy有一个非常重要的点是它是以字节为单位复制的,而对于一个字节的内容进行操作,我们自然而然的就想到了char*。在一次复制中,我们需要把src先转化为char*,再进行复制,之后以char*的类型++,表示跳到下一个字节。由于char*类型转换是临时的,所以我们每次复制都需要进行类型转换。当然,你也可以像我一样,创建一个char*指针存储src,这样直接用该指针进行++就行。
根据我们写的函数,我们就可以解释为什么source(src)和destination(dest)指向的空间不能重叠。先看个图:
这是怎么回事呢?
这是因为,复制完两个字节后,src指向的空间,从4变成了之前复制的2,下一个字节的5变成了3,dest指向的6和7之后自然也会被复制成2和3。
那如果,我们就是想像预期那样进行复制呢?这就要说到下一个函数,memmove()了。
对于这种空间重叠的情况,C语言规定了另外一种函数,memmove(),来满足使用。接下去博主就来介绍一下这个函数。
五、memmove()介绍及其模拟实现
1.库函数memmove()介绍
//头文件引用:<string.h>
void* memmove(void* destination, const void* source, size_t num)
{
//函数功能:与memcpy类似,并且该函数的source和destination两个内存块可以重叠
//参数destination - 目标内存块
//参数source - 源内存块
//参数num - 复制的字节数
}
//注意事项:
//源内存块和目标内存块的空间要足够大
示例:
2.模拟实现
这次我们先讲一下memmove能实现上述功能的原理。对于memcpy()所举的例子,我们知道用我们写的my_memcpy()是实现不了的。但是,如果我们将src和dest位置交换一下,你会发现,虽然src和dest的内存块依旧存在重叠,但却能得到我们预期的值。
这是因为,我们的memcpy()是从前往后复制的,即src从源内存块的第一个字节,复制到最后一个字节。当dest在src前面时,dest的改变并不会影响到src要复制的值,因为此时src前面的内容已经复制完毕了。而当dest在src的后面时,我们dest的改变使得原先src的值发生改变。
也就是说,当dest在src后面,我们从源内存块的最后一个字节开始复制,值也从目标内存块的最后一个字节开始存入,是不是就能避免这种情况呢?
理解了这个原理,我们就能写出代码了。
void* my_memmove(void* dest, const void* src, size_t num)
{
assert(dest && src);
char* dest2 = (char*)dest;
char* src2 = (char*)src;
if (dest > src)//源字符串在前
{
dest2 += num - 1;
src2 += num - 1;
while (num--)
{
*dest2-- = *src2--;
}
}
else//源字符串在后
{
while (num--)
*dest2++ = *src2++;
}
return dest;
}
(其实只要没有重叠部分,两种复制方法都可以,像上面的代码这么分有些果断,不过两种方法的所占时间和空间差不多,为了容易理解就采用了这种写法。)
六、memcmp()介绍
//头文件引用:<string.h>
int memcmp(const void* ptr1, const void* ptr2, size_t num)
{
//函数功能:比较两个内存块前num个字节
//参数ptr1 - 指向第一个内存块
//参数ptr2 - 指向第二个内存块
//参数num - 内存块比较的字节个数
}
//注意事项:
//比较ptr1和ptr2指向的内存块的第一个字节内数据大小,如果相等继续比较下一个字节
//当前num个字节都相等时,返回0
//若出现ptr1指向的内存块中某个字节的数据大于ptr2对应的字节的数据,返回>0的数
//若相反,返回<0的数
示例:
解释:
虽然a>b,但memcmp()比较的是单个字节内数据的大小。根据数据在内存中的存储知识,我们知道:
又因为我们是在小端存储模式的环境下编译的,则a、b变量在内存中的形式为:
当使用memcmp()时,在第一个字节上a<b,所以返回的是一个<0的数。
七、memset()介绍
//头文件引用:<string.h>
void* memset(void* ptr, int value, size_t num)
{
//函数功能:将ptr指向的内存块的前num个字节的值改为value
//参数ptr - 指向要设置内存的内存块
//参数value - 每个字节要设置的值
//参数num - 要设值的字节数
}
//注意事项:
//以字节为单位修改内存的值
//每个字节设置的值都相同
示例:
memset()是以字节为单位设置值,则我们用memset()来初始化字符数组会比较方便。
八、结语
本篇介绍的字符串函数、字符函数、内存操作函数与上篇介绍的字符串函数,都是比较实用的函数。熟练掌握这些函数的原理和用法,我们写程序就会事半功倍。
那以上呢就是《C/C++学习之路之字符串函数、字符函数、内存操作函数(下)》的全部内容,如果对你有帮助,请不要吝啬的点赞啦,每一个点赞都是我更新的动力。如有不足,也请各位读者在评论区指出,我会认真听取每一个建议,并作出相应的改变。
谢谢观看!