在#include<string.h>的这个头文件里面,除了前面给大家介绍的两大类——长度固定的字符串函数和长度不固定的字符串函数。还有一些函数以其独特的用途占据一席之地。
今天要给大家介绍的是下面这三个字符串函数:strstr,strtok,strerror。
学习指南:对于strstr函数有能力的小伙伴可以掌握一下它的模拟实现,但是strtok函数和strerror函数只需要知道它们的底层原理和基本使用即可。
———————————————————————————————————————————
附(难度评析表):
学习难度:
strstr: ⭐⭐⭐⭐⭐
strtok: ⭐⭐⭐
strerror: ⭐
理解难度:
strstr: ⭐ ⭐ ⭐(不考虑Kmp的部分,加上Kmp它完全可以达到五颗星的理解难度)
strtok: ⭐ ⭐ ⭐ ⭐
strerror: ⭐
目录
一、strstr函数:
1. 介绍:
2. 模拟实现:
二、strtok函数:
1. 介绍:
2. strtok函数的应用场景与工作原理:
应用场景:
工作原理:
(第一种情况)——str参数不为NULL指针:
(第二种情况)——str参数为NULL指针:
三、strerror函数:
1. 介绍:
2. 关于错误码:
3. perror函数的使用:
一、strstr函数:
1. 介绍:
函数原型:
const char * strstr ( const char * str1, const char * str2 );
函数名 | 功能描述 | 头文件 |
strstr | 在str1字符串查找str2字符串 | #include<string.h> |
返回值说明:返回一个指针,该指针指向了str2在str1中首次出现的位置。如果str2不在str1里面,则返回一个NULL指针。
2. 模拟实现:
思路解析:
养成编程的好习惯:我们让str1和str2始终指向各自的字符串的首元素地址不动它们,这样一来可以避免在之后的查找遍历过程中,因为找不到str1和str2首元素的地址而无法回溯的问题。然后分别定义两个指针s1,s2遍历str1数组和str2数组。
如果对应位置两个字符串的值相等(即:s1 == s2),我们就让s1和s2同时往下走(s1++,s2++)。反之让s1回到最开始查找的位置的下一个位置(该位置我们可以用一个cp指针进行标记)。然后s2回到str2的位置(即子串的第一个元素的位置)。
然后重复上面的步骤,大概思路就是这样的。另外的话考虑到如果str1字符串剩余待检索字符不足str2的实际长度,那也就没必要继续检索了,所以在这之前我们可以先求一下str2数组的长度(这个过程可以用strlen函数,也可以自己写代码)。详情请见代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
//模拟实现strstr函数的功能:(暴力求解)const char* my_strstr(const char* str1, const char* str2)
{const char* cp = str1;//记录开始遍历时的位置const char* s1 = str1;//遍历str1的指针const char* s2 = str2;//遍历str2的指针int cnt = 0;while (*s2++){cnt++;}while (*(cp+cnt-1)){s1 = cp;s2 = str2;while (*s2 && *s1 && *s1 == *s2){s1++;s2++;}if (*s2 != '\0'){cp++;}else{return cp;}}return NULL;
}int main()
{const char* str1 = "abbbcef";const char* str2 = "bc";const char* ret = my_strstr(str1, str2);if (ret){printf("%s", ret);}else{printf("str1里面找不着str2");}return 0;
}
上面的代码实际上一种暴力求解的思路,当然也有地方把这也叫作BF算法,这个是解决这种问题的通法。但是不是最高效的算法,这个在一个字符里面找另一个字符普遍认为最高效是KMP算法。这个算法这里博主也不过多介绍了。但是直接给代码很多小伙伴是很难理解的,而且也不是靠言语一两句话就可以讲明白的。
所以感兴趣的小伙伴可以自己下去了解一下,这里推介一个宝藏UP主的KMP算法课程,UP主很有耐心给大家介绍了该算法,虽然时间有点长,但是我觉得很适合小白来学习这个算法。
———————————————————————————————————————————
附(高质量KMP算法讲解):
【完整版】终于有人讲清楚了KMP算法,Java语言C语言实现_哔哩哔哩_bilibili
二、strtok函数:
1. 介绍:
函数原型:
char * strtok ( char * str, const char * sep );
函数名 | 功能描述 | 头文件 |
strtok | 按照指定分隔符对字符串进行切割 | #include<string.h> |
参数说明:
(一)第一个参数str是待分割字符数组的首元素的地址,另外值得一提的是strtok函数会修改str的内容,所以要求str的内容必须是可以被修改的。如果str作为字符指针本身其被const关键字所修饰,亦或者是保存常量字符串首元素地址的指针,那这种传参将不被允许,是一种错误的传参方式。如图所示:
但是当你的源文件以.c作为后缀名的时候,这个传参将会被允许,但也只是表面上的允许,实际运行时会出错。这是由于编译器对于.c为后缀的源文件编译检查不够严格导致的。
(二)第二个参数sep是分隔符的集合,strtok函数允许分隔符有多个。另外sep可以是一个const char*类型的字符指针,也可以是一个字符数组的首元素的地址。
注:怎么理解上面的传参关系:我觉得可以用下面这一句话来总结概括:你可以让有大能力的人做小事,但是你不能让有小能力的或者几乎没有能力的人去做大事。
2. strtok函数的应用场景与工作原理:
应用场景:
strtok函数是用来进行字符串切割的,通过这个函数我们可以对诸如电子邮箱的信息进行一个切割提取,就比如对于一个电子邮箱:zhangsan@year.net,如果我希望能够提取到zhangsan,year,net这样的有用信息的话就可以用这样的函数。
再还有就是在计算机网路里面,对于诸如主机ID——192.168.101.32(底层使用点分十进制方式对无符号整数进行处理的结果)这样的进行一个信息提取我们也能用strtok函数。
再其次就是在密码学里面,对于很长很长的密码报文,我们可以定义一些特别的分割符来对密码报文进行一个切片的处理,以达到化繁为简的目的……
工作原理:
(第一种情况)——str参数不为NULL指针:
strtok函数处理的对象主要是str,也就是第一个参数,如果第一个参数str不是NULL指针,它会根据str的内容,同时对照sep分割符集合里面的分隔符的种类。在发现首个分隔符之后,将其置为'\0',并且记下该分隔符在数组中的位置,做一个标记,为下一轮的分割字符串做准备(后面会证明这一点)。是的,你没听错,这个函数具有记忆功能!!!
之后该函数会返回该标记开始时的位置。注意这里说的是标记开始时的位置,而并非标记本身位置。eg:对于字符串:zhangsan@year.net,如果我们的分隔符sep有“@”和“.”,那对于分隔符“@”而言,它开始时的位置应该是字母z所在的位置。而对于分隔符“.”而言,它开始时的位置应该是字母y所在的位置。
我们这里就以上面的字符串zhannsan@year.net为例,来使用strtok函数对它进行第一次分割,如图所示:
但是也有可能str里面本身就没有sep分隔符里面的任何一个分隔符,那么这个函数将返回str本身。同时标记数组末尾,以表示分割已完成。后面继续分割将返回空指针。
至于如何进行下一轮的分割我们在下面会讲,这里先给大家演示一下str数组里面不存在sep分隔符的话会出现的情况。如图所示:
注:这里的sep数组里面保存的只有空格和"\0"。
(第二种情况)——str参数为NULL指针:
如果第一个参数str用户传过去的是空指针的话,那strtok函数将会从上一次标记的位置开始,去寻找新的分割符。将其置为'\0'并标记该位置。通过这种方式来实现对同一个字符串的第二轮分割。
还是以str = "zhangsan@year.net",sep = "@."为例,我们在进行了第一轮分割的基础之上,进行第二次分割和第三次分割应该这样去传参和使用。如图所示:
之前我们有提到说:该函数具有记忆功能,在这里就得到了很好的一个体现。第一次分割结束之后就记下了@在数组中的位置,第二次分割就从该位置开始往下找新标记,找到.,然后记下.在数组中位置。依次类推,最后会标记在数组末尾,以表示分割已完成。
再继续进行分割则会返回空指针,那是不是这样的呢,我们也可以来测试一下:
大概就是这么一回事,那有小伙伴们可能又要问了,那如果在此之前strtok函数没有标记位置呢。换句话说,strtok函数没有进行哪怕任何一次的有效分割,一上来str参数就传个NULL指针过去会怎么样。这里直接说结论:这个时候会导致程序的崩溃!!!所以在实际使用过程中应当避免这样的行为。
三、strerror函数:
1. 介绍:
函数原型:
char * strerror ( int errnum );
函数名 | 功能描述 | 头文件 |
strerror | 打印错误码所对应的错误信息 | #include<string.h> |
返回值说明:返回错误信息字符串的起始地址。
2. 关于错误码:
首先错误码是一个整数,不同的错误码标识程序在运行过程中不同的错误信息。错误码有很多,大概100来条左右。我们可以通过下面这个代码来看一下前十个错误码(0~9)所代表的含义。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>int main()
{for (int i = 0; i < 10; i++){printf("%d.%s\n", i, strerror(i));}return 0;
}
运行截图:
在实际开发过程中,如果仅仅是用strerror函数来查看错误码所对应的错误信息,这是没有什么实际意义的。实际上每一个程序都有一个全局变量——errno。这个全局变量是保存在头文件#include<string.h>里面(更准确一点来说是在头文件errno.h里面)。保存了当前程序错误码的信息。
当程序状态异常时,编译器会根据当前状态自动更新errno的值,这个全局变量联合strerrno函数来使用可以帮助开发人员更快更准确地确定程序的问题所在。
下面是一个使用示范:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>int main()
{//打开文件:FILE* fin = fopen("Unexist.txt", "r");if (fin == NULL){printf("发生错误:%s", strerror(errno));return errno;}//读文件://......//关闭文件:fclose(fin);fin = NULL;return 0;
}
运行截图:
注:上面的代码阅读需要一定的C语言文件操作的基本知识。C语言打开文件的函数是fopen,打开文件的方式则有r,w,a三种方式,分别表示只读,只写和追加。如果用户以只写和追加的方式来打开文件,如果当前文件不存在系统会自动创建文件。而当用户用只读的方式打开文件时,如果文件不存在则会出现错误。
3. perror函数的使用:
提到程序错误信息的打印,就不得不提及一下perror函数了。perror函数是stdio.h标准输入输出流头文件里面的一个标准库函数。
函数原型如下所示:
void perror ( const char * str )
该函数的功能是向屏幕打印错误信息,该函数的使用不需要用户对errno这个全局变量有所了解。他会自己检索底层的错误信息后并告诉我们的用户。
除此之外,函数的参数是用户输入的字符常量,这是用户希望打印的自定义信息。之后perror函数向显示器设备打印信息将按照如下格式进行:自定义信息:错误信息。这里值得一提的是打印格式中的“冒号”是编译器自己会带上的,不需要用户主动添加!
我们下面使用perror函数来改造上面的代码,以实现和使用strerror函数一样的效果:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>int main()
{//打开文件:FILE* fin = fopen("date.txt", "r");if (fin == NULL){perror("发现错误");return errno;}//读文件://......//关闭文件:fclose(fin);fin = NULL;return 0;
}
运行截图: