在本篇博客中,将会继续介绍字符串与内存函数,侧重点仍是深入理解,模拟实现。附上篇博客链接。
C语言进阶 字符串与内存函数 介绍+模拟 (1)
遇到一个新函数,无从下手的时候,MSDN:
可以看到,函数的参数是两个cosnt char *
,返回值是个char * ,他的简介短短一句,找到一个子串,可以猜测一下,他的作用是查找,具体怎么查找,查找什么,我们可以举个例子:
看到例子,再联合介绍中的子串,我们可以猜测,函数的作用是查找一个字符串中包不包含另一个字符串,如果包含,返回该位置的地址。
其实这个函数的作用就是判断一个字符串是不是另一个字符串的子串,如果是的话就返回被查找字符串从该子串开始的地址,如果没有就返回空。
例如:char a[] = "123456"; char b[] = "23"
,那么如果我们使用strstr函数进行查找,并且打印查找到的字符指针,打印出来的就应该是23456:
确实如此,我们理解的还是不错的,那要是我传入的是1234567呢?那就返回空呗,查找函数没那么多讲究,找不到,我就返回空给你,但是要实现他,还是有很多需要注意的地方。
如果我被查找的字符串是 “qweaaqwert”,子串是"qwert",那么肯定传回来的是"qwert"对吧,可是放到我们自己写,应该怎么实现这个东西呢?
模拟实现:
不着急写,我们先分析:
用这样dec,str这两个指针(其实是三个),就可以解决我们的问题,在循环遍历的过程中,如果,dec指向的值与str指向的值相等,那么就都后移一位,如果后面都相等,str指向到了’\0’的位置,那么就说明你找到了s2是s1的子串,就可以返回了,如果找到后面,s2还没有到尾,就不相等了:
这个时候,为了保证我下一次遇到相等时str不会变成’\0’,就要让str指向s2字符串的开头,重新进行比对,这时出现了一个小问题str是返回到最开头了,但是你dec呢,已经走到a后面了,这可不行,我们的想法是他最好从第三个位置重新开始和s2进行匹配,拿这时候,我们就要再维护一个指针cur,这个指针就要保存你每次比对之前的那个位置,如果进行比对,还发现不完全相等,那么就让cur++,因为不相等,就意味着cur在这个保存这个位置已经没用了,cur可以往后走了,再开始循环的时候,让dec=cur,这样就能实现不漏过任何一个可能包含的点了。
这个过程说起来有点麻烦,做了个视频,方便大家理解:
strstr
下面大家看看代码:
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
const char* dec = str1;
const char* str = str2;
const char* cur = str1;
while (*cur)//*cur为零代表str1走完了
{
dec = cur;
str = str2;
while (*dec && *str && (*dec == *str))//无论是不等还是任意等于0,都应该退出循环
{
dec++;
str++;
}
if (*str == '\0')//如果str2遍历到0了,那就说明找到了
return (char *)cur;
cur++;
}
return NULL;//没找到就返回空
}
经过刚才的思考以后,相信这个代码对大家来说还是很容易能看懂的。
另附系统源代码:
char * __cdecl strstr (
const char * str1,
const char * str2
)
{
char *cp = (char *) str1;
char *s1, *s2;
if ( !*str2 )
return((char *)str1);
while (*cp)
{
s1 = cp;
s2 = (char *) str2;
while ( *s2 && !(*s1-*s2) )
s1++, s2++;
if (!*s2)
return(cp);
cp++;
}
return(NULL);
}
strstr还有很多种写法,有兴趣的话可以了解一下。
先看看这个函数的两个参数,一个strToken,用来存放分隔符,一个strDelimit,用来传字符串,这个函数的作用呢,就是把一个整块的字符串利用分隔符分隔开,例如:
int main()
{
char a[] = "abc/qwe/rtu/iop";
char c[2] = {'/'};
printf("%s", strtok(a, c));
return 0;
}
那这个函数的作用就是找到分隔符,把分隔符替换为’\0’,然后返回字符串的初始地址,那现在这个字符串就变成了"abc/0que/rtu/iop",那么我再输出,就是:
这个函数就只有这么简单吗,当然不是,注意,MSDN中给出的两段英文,第一段中有很多NULL,这些NULL的意思是,当你第一次调用传入的是一个字符串的地址,一个分隔符字符串的地址,当你第二次调用的时候,可以在原本字符串的位置放一个NULL,第二个参数还是传分隔符字符串,这样他就会从上一次调用函数的位置往后再重复调用一次,例如:
由此可见,strtok函数中应该有一个静态的指针,一直保存着字符串的位置。刚才说MSDN中第一段有很多NULL,第二段是一个警告,警告我们不用多线程的调用,怎么说呢,就是用完一个再用另一个,不要出现满篇都是strtok的情况。
现在看来,strtok的主要功能有两个,第一,找到字符串中的分隔符,并替换成'\0',第二,保存分隔符后字符串的地址,方便下一步的使用
。
现在介绍strtok函数的一种用法,strtok函数是一次只能取一段字符串,如果我想全取出来,那就只能一遍一遍的调用,写一大长串不仅不好控制,还很难看,所以我们可以使用循环的方法:
int main()
{
char a[] = "abc/qwe/rtu/iop";
char c[2] = "/";
char* str = NULL;
for (str = strtok(a, c); str != NULL; str = strtok(NULL, c))
{
printf("%s", str);
}
return 0;
}
这样我们就去除了字符串中的分隔符,把字符串打印了出来,当然,代码形式不固定,想到这个循环的思想即可。
模拟实现:
虽然这个函数看起来有点复杂,但是我们也要尝试一下模拟实现,可能不是很对,但是要做出尝试。
首先函数的参数,一个char*,一个const char*,一个传字符串,一个传分隔符,然后呢,我们要先找到分隔符的位置,然后进行替换,由于你每个字符都要找一遍分隔符数组,所以采用一个嵌套的while循环,找到以后对应字符替换成’\0’,然后返回,这里说还是不太明白,再给大伙放个视频:
strtok
原理差不多就这样,我们看代码
static char* ret = NULL;
char* my_strtok(char* str1, const char* str2)
{
char* s1, * s2,*str = NULL;
if (str1 == NULL)//如果为空,就从ret这个静态全局变量取上一个的地址
{
s1 = ret;
s2 = (char *)str2;
str = ret;
}
else//否则,正常进行初始化
{
s1 = str1;
s2 = (char*)str2;
ret = str1;
str = ret;
}
if (s1 == NULL)//如果s1 == NULL说明 ret 为空,则返回空值,说明这轮调用结束了
return NULL;
while (*s1)
{
s2 = (char*)str2;//每回循环结束让s2回到原本的位置,s1++后再逐个比较
while (*s1 != *s2 && *s2!='\0'&& *s1!='\0')
{
s2++;
}
if (*s1 == *s2 && *s1 != '\0')
{
*s1 = '\0';
ret = ++s1;
return str;
}
s1++;
}
ret = NULL;//上个循环的结束条件是s1 ==0,也就是说已经走到尽头了,避免无限循环,把ret赋值NULL,下次调用就会直接结束
return str;
}
这个代码不是很简洁,但是效果还是不错的
就像系统的strtok一样,完成了分割字符串的功效。
这个代码并不够好,希望大家能提出自己的意见,让我改正一下这个代码,这回就不放源代码了,不是C语言的内容。
这个函数比较简单,也不是我们今天模拟实现的重点,简单说说怎么用,先看MSDN:
其实,在我们每回调用库函数出错误或是失败的时候,都会有一个错误码,这是一个全局变量,而strerror的作用呢,就是找到错误码的错误信息,把他显示出来。他的参数为一个整型,也就是我们只需要传入一个数即可,而且调用这个函数需要两个库 1.stdio.h,2.errno.h:
这就是从0-9错误码的信息。
当然,这个函数不是这么用的,真正的用法是帮助我们看看函数哪里出错了,例如
int main()
{
int* a = (int*)malloc(40000000000000);
printf("%d\n", errno);
printf("%s\n", strerror(errno));
return 0;
}
我们都知道,malloc是向堆申请的空间,堆的空间很大,但是没有这么大,那当我们调用malloc申请空间的时候必然出错,空间不够嘛,这时候我们打印一下错误信息,之前说过errno是一个全局变量,他记录了你产生错误的错误码,我们把他的错误信息打印出来:
果不其然,没有足够的空间,这就是strerror的使用,源代码也不放了,不是C语言的内容。
现在我们进入了最后一个板块,内存操作函数,这里的库函数大多是直接对内存进行操作,而没有字符串的限制,我们可以将其理解为,通用型的函数。
可以看到啊,memcpy的参数是两个void* 类型的指针,一个size_t,好像有点熟悉:
这不就跟strncpy差不多嘛,实际上也是如此,功能都一样,区别是memcpy能拷贝任意类型的数据,而且传入的也不是要拷贝的字符个数,而是要拷贝过去的字节数,例如要拷贝四个整型,就传进去16:
可以看到,我们把b中的0,全拷贝到了a中,这就是memcpy,之前strcpy需要注意的就不一一说了,但是有一点需要注意,memcpy不再是字符串函数,那就不会以’\0’作为结束标志,只有他拷贝了足够的字节数,才会停止,我们直接模拟实现一下,理解一下这种通用函数的思想:
模拟实现
虽然一说通用型函数,大家都感觉很难,一头雾水的样子,但只要我们往实现的方案上想一想,其实要实现memcpy还是不难的,首先他的参数和返回值:
void* my_memcpy(void* dest, const void* src, size_t cnt)
就像MSDN一样,我们把他写出来,那么具体怎么实现呢?通用型函数,就是什么类型你都能用,只要你传进去cnt,我就把cnt个字节的数据拷贝到你的目标空间里面去。
假如这是我们要拷贝的空间,传入的cnt是28,我们如何去做,那就一个字节一个节的放到目标空间中,一一对应,这样对应28次,该空间的内容就完全拷贝到内存中了,那怎么一个字节一个字节的对应呢,还记得我们判断大小端的方法吗?对了,就是用一个char * 类型的指针,这样就可以实现一一对应赋值了
就这样一一对应赋值,实现memcpy的功能,我们看看代码该怎么写:
void* my_memcpy(void* dest, const void* src, size_t cnt)
{
assert(dest && src);
void* ret = dest;
while (cnt--)
{
*(char*)dest = *(char*)src;
(char*)dest = (char*)dest + 1;
(char*)src = (char*)src + 1;
}
return ret;
}
代码很简单,由于是void类型的指针,想一个字节一个字节的操作,就强制类型转换成char类型的指针,然后依次赋值即可,让我们看看效果:
代码不难理解,主要是要学会这个通用的思想。
MSDN:
可以看到,这个函数和memcpy有点像啊,参数和介绍都有点相似,实际上呢,memcpy和memmove各有分工,memcpy呢是两个不同空间的拷贝,memmove则是对一个空间使用的,例如:
这是一片空间,我想把前两个元素拷贝到后两个元素的空间中,也就是重叠区间的拷贝,这时候就要使用memmove函数了:
又比如我们想把a[4]拷贝到a[3]中:
基本的使用就是这样,但是如何实现这个memmove,我们还需思考一下。
模拟实现:
要分为两种情况,是从前往后拷贝还是从后往前拷贝的关键是,你要拷贝进去的那个字符串数据不能被覆盖,这里应该分为三种情况来讨论:(此时cnt的值是16)
那么还有这样一种情况,src < dest,这时候我们就要重新分析了。 经过实验之后我们发现,5和6在没有拷贝的情况下被覆盖了,那从前往后就是行不通的,应该从后往前拷贝。 从前往后的代码很简单,稍微有点难度的是从后往前的,由于你是从后往前拷贝,所以我们要找到的是两个区域的最后一个字节,所以加了cnt,因为我们while里面是cnt–,例如原先cnt是20,现在是19,而dest+19,那就正好是dest区域的最后一个字节了,这样一一对应就可以了。 MSDN: 注意,循环的终止条件不是移动到’\0’了,而是cnt == 0. memset函数是内存设置函数,他的作用就是给一片内存赋值,先看看MSDN: 字符串和内存函数到这就告一段落了,知识点很多,希望大家多多复习,上手操作才能提高,虽然代码量有点多,但是只要理解其中原理,了解其中思想,还是很容易写出来了,如果感觉博主总结的还不错,可以留下一个免费的赞,谢谢。
这是情况一,dest在src之前,我们首先明确一点:dest是目标空间的指针,src是要拷贝的指针,那在dest
还是先把6拷贝到4的位置,5拷贝到3的位置,即是应该从前往后拷贝,还是从后往前拷贝,我们来分析一下:
如果从后往前拷贝:6拷贝到4,5拷贝到3:
这时候我们发现我红色的数据区被覆盖了,而且我的3和4还没有进行拷贝就被覆盖了,那就说明,从后往前是不行的。
我们再试另一种,从前往后:
先把3拷贝到1,4拷贝到2:
这个时候我们发现,虽然在对5进行拷贝,3被覆盖了,但是3已经进行过拷贝了,此时被覆盖并没有什么影响:
3,4,5,6的数据成功的被拷贝进了dest指向的区域,所以在dest情况二
这里挑一种情况给大家分析,然后大家试着自己分析另一种:
我们先试试从前往后覆盖,即为3拷贝到5,4,拷贝到6:
情况三
这也是一种比较特殊的情况,src区域和dest区域没有重叠的情况。这时候我们从前往后,或者从后往前都无所谓,因为没有产生覆盖。
知晓这三种情况以后,让我们上代码!void* my_memmove(void* dest, const void* src, size_t cnt)
{
assert(dest && src);
void* ret = dest;
if (dest < src)
{
while (cnt--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else//从后往前
{
while (cnt--)
{
*( (char*)dest + cnt) = *((char*)src + cnt);
}
}
return ret;
}
♨️♨️memcmp
memcmp函数其实和strcmp函数相差不多,只不过strcmp函数是对字符串用的,一个字符一个字符的比对,但是memcmp函数是对任意数据用的,而且是一个字节一个字节的比对,结尾标志也不是’\0’而是你输入的字节数。
可以试验一下:
我们对两个数组的第一个元素进行比对,都是 00 00 00 00,结果为0,是相等的意思。
但是当我们比对十六个字节的时候,a是
00 00 00 00 01 00 00 00 02 00 00 00 00 03 00 00 00
b是
00 00 00 00 01 00 00 00 01 00 00 00 00 01 00 00 00
这样一个字节一个字节的对比,发现a比b大,那结果就是1.
模拟实现
实际上这个和我们之前的那个strcmp相差不多,稍微改改就能用:int my_memcmp(const void* s1, const void* s2,size_t cnt)
{
assert(s1 && s2);
while (*(char*)s1 == *(char*)s2)
{
if (cnt == 0)
return 0;
s1= (char*)s1+1;
s2= (char*)s2+1;
cnt--;
}
int ret = *(char*)s1 - *(char*)s2;
return ((-ret < 0)) - (ret < 0);
}
♨️♨️memset
dest就是目标空间,c就是你要给这片内存赋什么值,count就是这片空间有多大:
我们通过内存可以看到,a的前二十个字节,每一个字节都被赋值为了五,而不是一个元素一个元素的赋值。
模拟实现
这个函数比较简单,模拟实现的原理也就是一个一个字节的后移,也算留一个课下习题,希望大家回去试一试(答案在评论区)。总结: