这次我们就紧接着上次,再更多的认识,使用一些函数,并对部分函数进行模拟实现。
目录
函数介绍
strstr
对strstr的简单认识
如何来理解strstr呢,我们可以通过命名来推理:
strcpy -- string copy 是字符串拷贝。
strcmp -- string compare 是字符串比较。
strstr -- string string 两个字符串,难道是在字符串里找到字符串?我们往下看:我们观察它的参数const char* strstr(const char* str1, const char* str2),我们可以看到,strstr确实是在字符串中寻找字符串,它会返回str1中str2首次出现的位置,若没有,则返回空指针,那么我们接下来就来使用它:
#include<string.h> int main() { char str1[] = "abcdefghidefg"; char str2[] = "def"; char* ret = strstr(str1, str2); if (NULL == ret) { printf("没找到\n"); } else { printf("%s\n", ret); } return 0; }
运行结果如下:
我们可以发现,strstr在同时存在两个需要寻找的字符串时返回首次该字符串出现的地址。
紧接着我们对其进行模拟实现。
模拟实现strstr
那么这个函数应该如何进行模拟实现呢?
我们先对简单的情况进行分析:我们发现,若是只创建两个参数的指针变量,那么在两个指针移动完之后,我们就失去了一开始进行匹配的位置:
那么,我们就需要再创建一个指针cp记录str1开始匹配的位置,但如果一次匹配不成功的话,str2的起始位置也会失去,那么我们就干脆不对原位置进行处理,再创建两个指针s1,s2来进行移动操作:
那我们进行编写:
#include<stdio.h> #include<assert.h> 我们就先来对参数进行创建,我们接收的两个字符串都是不会进行修改的,那么我们就在前面加上const进行修饰。 const char* my_strstr(const char* str1, const char* str2) { const char* cp;//让cp记录开始匹配的位置 const char* s1;//让s1遍历str1指向的字符串 const char* s2;//让s2遍历str2指向的字符串 assert(str1 && str2);//断言 if (str2 == '\0')//如果传的是空字符串,那么直接返回str1 return str1; cp = str1; while (*cp)//判断str1是否为NULL { s1 = cp; s2 = str2; while (*s1 && *s2 && *s1 == *s2)//如果相等开始匹配,如果遇到'\0'就退出 { s1++; s2++; } if (*s2 == '\0') return cp; cp++;//不相等,找下一个 } return NULL; } int main() { char str1[] = "abbbcdef"; char str2[] = "bbc"; const char* ret = my_strstr(str1, str2); if (NULL == ret) { printf("没找到\n"); } else { printf("%s\n", ret); } return 0; }
运行结果如下:
我们虽然对strstr进行了模拟实现,但是它的算法不够高效,有兴趣的读者可以去研究一下KMP算法,也是解决在字符串中找字符串的问题。
strtok
对strtok的简单认识
strtok是来干什么的呢?我们在中进行查看:strtok - C++ Reference (cplusplus.com)
,我们可以发现,strtok是来切割字符串的,比如:
xmluoli@163.com假设这是一个邮箱,那么@和.就是分隔符,我们可以通过strtok来进行切割得到分隔符分割后的字符串。那么如何来对strtok进行使用呢,我们先对其参数进行分析:
它的参数char * strtok ( char * str, const char * delimiters ),从下面的描述来看,delimisters是分隔符字符串的集合,是将分隔符,比如"@.",放入其中,在使用strtok后,它会将第一个分割符改成'\0',并且记录下该位置。听起来有点难以理解,我们直接来进行实验:
#include<string.h> int main() { char arr[] = "xmluoli@163.com"; char* p = "@."; char* s = strtok(arr, p); printf("%s\n", s); return 0; }
运行结果如下:
我们在对其参数的认识当中得知,strtok是会将分割符改成'\0'的,那么会还原吗?我们进行调试:
我们看到strtok是不会对修改的字符串进行还原的,所以我们在使用它的时候,还是先进行拷贝,对拷贝的字符串进行切割,我们接着对它一大段话进行解读:
如果strtok函数第一个参数不为NULL,那么函数将找到strtok的第一个标记,strtok将会保存它在字符串当中的位置。如果strtok函数为NULL,那么就会重保存的位置开始,寻找下一个分隔符,如果找不到其他分隔符,返回NULL。既然如此,我们就来进行实验:#include<string.h> int main() { char arr[] = "xmluoli@163.com"; char buf[200] = { 0 }; strcpy(buf, arr); char* p = "@."; char* s = strtok(buf, p); s = strtok(NULL, p); printf("%s\n", s); return 0; }
运行结果如下:
我们可以看到,确实如此。
那么接下来就会出现一个问题,如果需要切割很长的字符串,难道需要重复写调用函数的代码吗?我们就对代码进行改造:#include<string.h> int main() { char arr[] = "xmluoli@163.com"; char buf[200] = { 0 }; strcpy(buf, arr); char* p = "@."; char* s = NULL; for (s = strtok(buf, p); s != NULL; s = strtok(NULL, p))//for循环的初始化只进行一次 { printf("%s\n", s); } return 0; }
运行结果如下:
这样,字符串再长,也不需要对代码进行修改了。
strerror
对strerror的简单认识
strerror是将错误码翻译成错误信息,返回的是错误信息字符串的起始地址。那么什么是错误码呢?这个大家应该比较熟悉就比如说404,就是找不到该网站。那么,在C语言中使用库函数的时候,会将错误码放在errno这个变量里面,errno是全局变量,可以直接使用:
那么我们接下来就来看一些错误码所对应的信息:
#include<string.h> int main() { int i = 0; for (i = 0; i < 10; i++) { printf("%d: %s\n", i, strerror(i)); } return 0; }
运行结果如下:
不得不说,这个函数在今后的使用一定会有很大的用处,我们可以在编写代码的时候加上这一句,对于寻找和修改bug就会有极大的帮助。
字符函数
在认识了一些字符串函数之后,我们来认识一些字符函数。
字符分类函数
对于字符分类函数小编就不一一详细介绍了,我们就通过islower来举个例子。
通过上面我们可以知道,islower判断如果是小写字母返回一个非零的值,如果不是小写字母返回非零的值,我们编写代码:
#include<ctype.h> int main() { int ret = islower('a'); printf("%d\n", ret); ret = islower('X'); printf("%d\n", ret); return 0; }
运行结果如下:
我们刚入门的时候判断字母大小写需要写很多的if...else语句来进行判断,而现在我们就可以利用这些函数来简便我们的工作量了。
字符转换函数
字符转换函数其实也就只有两个函数:
tolower
toupper这两个函数也是比较好理解的,一个是把大写字母转化为小写字母,一个是把小写字母转化成大写字母,我们直接进行应用:
#include<ctype.h> int main() { int ret = tolower('A'); printf("%c\n", ret); ret = toupper(ret); printf("%c\n", ret); return 0; }
运行结果如下:
这样我们平时需要使用转换大小写也方便了很多。
应用
下面我们就对认识的函数进行应用,我们把一个字符串"Hello World!"当中的所以大写字母全部转化成小写字母,那么应该如何进行实现呢?我们只需要创建一个指针来遍历这个字符串,然后遇到大写字母进行转换就可以了:
#include<ctype.h> int main() { char arr[] = "Hello World!"; char* p = arr; while (*p) { if (isupper(*p)) { *p = tolower(*p); } p++; } printf("%s\n", arr); return 0; }
运行结果如下:
到这里认识的函数都与字符相关,但是C语言中又不仅仅是字符,所以我们接下来就来认识与内存相关的函数 。
memcpy
对memcpy的简单认识
在认识了strcpy后,这个函数也很好理解,就是拷贝,不过这个函数是对内存进行拷贝:
我们可以看到它的参数:void * memcpy ( void * destination, const void * source, size_t num ),其中void*可以接收任意类型的数据。3个参数一个void*的目的地,一个void*的源头,还有一个需要拷贝的字节大小,那么我们就直接来对其进行应用:
#include<string.h> int main() { int arr1[10] = { 0 }; int arr2[] = { 1,2,3,4,5 }; memcpy(arr1, arr2, 20); return 0; }
我们进行调试:
我们可以看到,memcpy确实将arr2中的数据拷贝过去了。那么我们紧接着进对其进行模拟实现。
模拟实现memcpy
如何来模拟实现memcpy呢?模拟实现memcpy我们首先从参数上下手:
我们知道,memcpy是可以拷贝任意类型的数据的,所以我们自然要将源头和目的地都设置成void*来方便接收,紧接着就是字节的处理了,既然它是拷贝字节,那我们就一个字节一个字节进行拷贝,那么,在进行拷贝的时候,我们就需要强转成char* 来进行处理了,需要注意的是,我们在指针移动时,前面将dest和src的强转赋值是临时的,我们之后在移动指针往后的时候是不能dest++,也就是对void*进行++处理的,我们需要再次进行强制类型转换 :
#include<stdio.h> #include<assert.h> void* my_memcpy(void* dest, const void* src, size_t num) { assert(dest && src);//进行断言 void* ret = dest; while (num--) { *(char*)dest = *(char*)src; dest = (char*)dest + 1; src = (char*)src + 1; } return ret; } int main() { int arr1[10] = { 0 }; int arr2[] = { 1,2,3,4,5 }; my_memcpy(arr1, arr2, 20); return 0; }
我们进行调试:
内部拷贝问题
这时就有读者想问了,那么我可以把一个数组里面的内容在该数组内进行拷贝呢?就比如说我创建一个数组arr[1,2,3,4,5,6]。我可以把1,2,3拷贝到3,4,5上吗?那我们就通过实验进行验证:
#include<stdio.h> #include<assert.h> void* my_memcpy(void* dest, const void* src, size_t num) { assert(dest && src);//进行断言 while (num--) { *(char*)dest = *(char*)src; dest = (char*)dest + 1; src = (char*)src + 1; } } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; my_memcpy(arr + 2, arr, 20); return 0; }
我们进行调试:
调试后我们发现,得到的并不是我们想要的,它在重复的拷贝1,2,这是为什么呢?
所以我们得出结论,对于重叠的内存的拷贝我们是不能使用my_memcpy的,这里我们如果使用库函数memcpy,会发现还是将1,2,3,4,5拷贝过去了。在C语言规定中,memcpy是不能对重叠的内存进行拷贝的。这说明编译器对memcpy进行了优化,我们模拟实现的memcpy是办不到的,那么我们应该使用什么呢?就是接下来的memmove。
memmove
对memmove的简单认识
memmove与memcpy的参数都是相同的:
我们直接使用:
#include<string.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; memmove(arr + 2, arr, 20); return 0; }
进行调试:
在完成使用之后,我们就对其进行模拟实现。
模拟实现mommove
那么memmove该如何进行模拟实现呢?我们只需要解决掉覆盖的问题就可以了,我们会发现,之前我们想把前面的内容拷贝到后面从前往后拷会重叠。那我们不妨从后往前拷,那么想把后面的内容拷贝到前面的思想也是一样的,我们就从前往后拷就行了:
当源头和目的地不重叠的时候,两种方式都是可以的,总而言之,源头是不能被修改,这样,我们就分为了4处:前不重叠,前重叠,后重叠,后不重叠,为了方便,我们就让前半部分采用从后往前拷贝,后半部分采用从前往后拷贝,那我们就开始编写函数 :
#include<assert.h> void* my_memmove(void* dest,const void* src, size_t num) { assert(dest && src);//断言 void* ret = dest; if (dest < src)//void*可以比较大小 { //前->后 for (size_t i = 0; i < num; i++) { *(char*)dest = *(char*)src; dest = (char*)dest + 1; src = (char*)src + 1; } } else { //后->前(我们需要让源头指针指向最后一个字节) while (num--) { //num已经变成19 *((char*)dest + num) = *((char*)src + num); //每次num变化,不需要过多操作 } } return ret; } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; my_memmove(arr, arr + 2, 20); int sz = sizeof(arr) / sizeof(arr[0]); for (int i = 0; i < sz; i++) { printf("%d ", arr[i]); } return 0; }
运行结果如下:
memset
对memset的简单认识
memset,如同其名,内存设置,它是将所指定的内存设置成你想设置的,我们往下看:
我们可以看到,它的第一个参数是想要设置的起始位置,第二个参数是设置的内容,第三个是以字节为单位设置个数,那么我们直接运用它:
#include<string.h> int main() { char arr[] = "hello world."; memset(arr + 6, 'x', 3); printf("%s\n", arr); return 0; }
运行结果如下:
这里需要强调的是第三个参数是以字节为单位设置个数,我们来举一个例子:
#include<string.h> int main() { int arr[10] = { 0 }; memset(arr, 1, 40); printf("%s\n", arr); return 0; }
进行调试:
在内存中查看:
这时我们就会发现,想把arr当中的元素全部改成1是无法实现的,我们在内存里可以看到,它是把所有的字节都改成01了。
memcmp
对memcmp的简单认识
memcmp与strcmp类似,memcmp是进行内存块比较,我们可以看到:
memcmp是以一个字节一个字节进行比较的,参数也是类似的,这里我们还是需要注意一下返回值:
那么,我们直接使用:
#include<string.h> int main() { int arr1[] = { 1,2,3,4,5,6 }; int arr2[] = { 1,2,4 }; int ret = memcmp(arr1, arr2, 12); printf("%d ", ret); return 0; }
运行结果如下:
memcmp想要模拟实现的话与strncmp是及其相似的,这里小编就不过多演示了。
小结
今天就先到这里啦,对于字符串与内存函数的认识到这里就结束了呢,下一次就是结构体啦!不断不说,一些函数的使用,对于我们编写代码的效率的提升大大提高 。但小编的技术还不够,还需要不断地努力,终有一天,会抵达我幻想的那片天空,好啦,我们下次见!