字符串查找函数
strstr
函数功能
查找子串,查找一个字符串中是否包含子串。
函数参数
char * strstr ( const char *str1, const char * str2); # char* 函数返回值,返回字符串中子串的起始地址,若找不到,则返回NULL; # char* str1 要搜索的字符串; # char* str2 子串
函数使用
#include <stdio.h> #include <string.h> //strstr对应头文件 int main() { char arr1[] = "abbbcdef"; char arr2[] = "bcd"; char* ret = strstr(arr1, arr2); printf("%s\n", ret); return 0; }
模拟实现(重要)
#include <stdio.h> #include <assert.h> char* my_strstr(const char* str1, const char* str2) { assert(str1 && str2); //如果中途匹配失败需要回到str2的起始地址,所以用其他变量标识str2,保证str2的首地址不会丢失 const char* p1 = str1; const char* p2 = str2; const char* mark = str1; //用来标记每次第一个字符成功匹配的位置 while (*mark != '\0') { p1 = mark; //从mark处开始往后匹配 p2 = str2; //每次匹配后p2回到str2开头 while (*p1 == *p2 && *p1 != '\0' && *p2 != '\0') //一直往后匹配,直到遇到不相等的字符 { p1++; p2++; } if (*p2 == '\0') //如果不相等处p2为'\0'(子串全部匹配成功),则返回Mark处地址 return (char*)mark; mark++; //否则,说明这一次匹配失败,从mark后面一个字节处开始重新匹配 } return NULL; //字符串找完都没有子串就返回空指针 } int main() { char arr1[] = "abbbcdef"; char arr2[] = "bcdq"; char* ret = my_strstr(arr1, arr2); printf("%s\n", ret); return 0;
注意事项
- 被查找的字符串和子串都不能是空串,且都以’\0’结尾。
- 如果查找成功,返回字符串中子串所在位置的首地址,如果查找失败,则返回NULL。
注:我们上面模拟实现的查找子串的函数效率比较低,如果要追求高效率,则需要使用KMP算法,有关KMP算法的相关知识,我会在后面的文章中进行介绍。
strtok
函数功能
字符串分割,把一个字符串按照分割标志分割为几个字符串。
函数参数
char * strtok ( char * str, const char * sep ); # char* 函数返回值,strtok函数会找到str中的下一个标记,并将其用'\0'结尾,返回一个指向这个标记的指针; # char* str 指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记; # char* sep 一个字符串,定义了用作分隔符的字符集合;
函数使用
#include <stdio.h> #include <string.h> int main() { char* sep = "@."; char email[] = "1684277750@qq.com"; char tmp[20] = ""; //由于strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都会临时拷贝一份,操作拷贝的数据 strcpy(tmp, email); printf("%s\n", strtok(tmp, sep)); //第一次第一个参数传递被切割字符串的首地址 printf("%s\n", strtok(NULL, sep)); //第二次及以后第一个参数传递空指针(strtok会记住上一次切割的位置) printf("%s\n", strtok(NULL, sep)); return 0; }
这里我们知道目标字符串会被分隔符切割为三个字符串,所以这里我们调用了三次strtok函数,但是当我们不知道目标字符串的内容时,这种方法显然就不能用了;那么我们该如何正确的使用strtok函数呢?
我们知道,strtok函数第一次调用时需要传递目标字符串的地址,其余调用都只需要传递NULL即可,那么我们可以利用这个特点结合for循环的特性来正确调用strtok函数。
#include <stdio.h> #include <string.h> int main() { char* sep = "@."; char email[] = "1684277750@qq.com"; char tmp[20] = ""; //由于strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都会临时拷贝一份,操作拷贝的数据 strcpy(tmp, email); char* ret = NULL; //用来保存strtok函数的返回值 for (ret = strtok(tmp, sep); //初始化部分:第一次传递tmp的地址 ret != NULL; //判断部分:只要strtok的返回值ret不为空,说明继续分割 ret = strtok(NULL, sep)) //调整部分:第二次及以上传递NULL { printf("%s\n", ret); //ret不为空,说明字符串没被分割完,则打印 } return 0; }
注意事项
sep参数是个字符串,定义了用作分隔符的字符集合;第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标 记;
strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注: 由于strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都会临时拷贝一份,操作拷贝的数据 )
strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置;
strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记;
如果字符串中不存在更多的标记,则返回 NULL 指针;
strerror
函数功能
C语言有一系列的库函数,当这些库函数调用失败时,会返回相应的错误码,而strerror函数的作用就是获取错误码对应的错误信息的首地址,让使用者知道程序发生错误的原因。
函数参数
char * strerror ( int errnum ); # char* 函数返回值,返回错误码对应的错误信息的字符串的地址; # int errnum 错误码
函数使用
#include <stdio.h> #include <string.h> //strerror的头文件 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; }
这里有一个问题,C语言中那么多的错误信息,那么我们需要记住每一个错误信息对应的错误码吗?其实,C语言中设置了一个全局的用于存放错误码的变量errno,只要调用C语言库函数发生错误,那么errno就会记录相应的错误码,所以strerror函数和errno一般都是配合使用的。
#include <stdio.h> #include <string.h> //strerror对应头文件 #include <errno.h> //errno对应头文件 int main() { FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { printf("%s\n", strerror(errno)); return 1; } else { printf("文件打开成功!\n"); } return 0; }
字符函数
字符分类函数
字符转换函数
函数 | 返回值 |
tolower | 返回对应小写字母的ASCII值 |
toupper | 返回对应大写字母的ASCII值 |
内存操作函数
前面我们学习的strcpy、strcat、strcmp、strncpy、strncat、strncmp等函数都是字符串函数,只能对字符串进行相关操作,如果要对其他数据类型,如整形、字符、结构体等进行类似操作的话,就需要学习内存操作函数,常见的内存操作函数有memcpy、memmove、memcmp、memset。
memcpy
函数功能
内存拷贝,将一块内存中num个字节的内容拷贝到另一块内存中,常用来处理不重叠内存数据的拷贝。
函数参数
void * memcpy ( void * destination, const void * source, size_t num ); # void* 函数返回值,返回dest内存空间的地址; # void* destination 目标内存空间地址; # void* source 源空间地址; # size_t num 要拷贝的字节数;
函数使用
#include <stdio.h> #include <string.h> //memcpy对应头文件 int main() { int arr1[] = { 1,2,3,4,5,6 }; int arr2[10] = { 0 }; memcpy(arr2, arr1, 6 * sizeof(int)); for (int i = 0; i < 6; i++) { printf("%d ", arr2[i]); } return 0; }
模拟实现
#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; //强转为char*类型后赋值 //这里不要写成(char*)dest++,在某些编译器下会报错,因为强制类型转是一种临时效果 dest = (char*)dest + 1; src = (char*)src + 1; } return ret; } int main() { int arr1[] = { 1,2,3,4,5,6 }; int arr2[10] = { 0 }; my_memcpy(arr2, arr1, 6 * sizeof(int)); for (int i = 0; i < 6; i++) { printf("%d ", arr2[i]); } return 0; }
注意事项(重要)
在C语言标准中,memcpy只负责处理内存不重叠的数据,内存重叠的数据的拷贝是memmove函数负责实现的,即下面这种情况在C语言标准中memcpy函数是不能实现的:
memcpy(arr1 + 2, arr1, 4 * sizeof(int));
从上面我们memcpy的模拟实现中也可以看出,memcpy是从前向后拷贝的,这就导致在拷贝重叠内存数据时会发生数据覆盖(即arr1[2]中的数据在前面赋值中被改为1,导致将arr[2]中的数据赋给arr[4]时不是4,而是1),但是在VS下的memcpy函数是具备拷贝重叠数据的能力的,也就是说,VS下的memcpy函数同时实现了memmove函数的功能,但是其他编译器下的memcpy函数是否也具备memmove函数功能是未知的,所以我们在处理重叠内存数据拷贝的时候尽量还是使用memmove函数,以免发生错误。
memmove
函数功能
内存移动,将一块内存数据中的内容移动覆盖至另一块内存数据,常用来处理重叠内存数据的拷贝。
函数参数
# memmove 函数的参数和 memcpy 函数完全相同 void * memmove ( void* destination, const void * source, size_t num );
函数使用
#include <stdio.h> #include <string.h> //memmove对应头文件 int main() { int arr1[] = { 1,2,3,4,5,6 ,7,8 }; int arr2[10] = { 0 }; memmove(arr1 + 2, arr1, 4 * sizeof(int)); for (int i = 0; i < 8; i++) { printf("%d ", arr1[i]); } return 0; }
模拟实现(重要)
思路分析:
在memcpy中我们提到在拷贝重叠内存的数据时会发生内存覆盖的情况,其实这种覆盖分为两种情况:
(1):dest的地址大于src的地址
如图,如果这时我们从前往后移动的话,那么4就会覆盖掉6,从而导致将6赋给8变成4赋给8,所以我们应该从后往前移。
(2):dest的地址小于src的地址
如果这时我们从后往前移动的话,那么7就会覆盖掉5,导致将5赋给2的时候变成7赋给2,所以这里我们应该从前往后移动。
代码实现:
#include <stdio.h> #include <assert.h> void* my_memmove(void* dest, void* src, size_t num) { assert(dest && src); void* ret = dest; //保存目标空间的地址 if (dest < src) { //前 -> 后 等价于memcpy while (num--) { *(char*)dest = *(char*)src; dest = (char*)dest + 1; src = (char*)src + 1; } } else { //后 -> 前 while (num--) { *((char*)dest + num) = *((char*)src + num); } } return ret; } int main() { int arr[] = { 1,2,3,4,5,6,7,8,9 }; my_memmove(arr + 1, arr + 3, 4 * sizeof(int)); for (int i = 0; i < 9; i++) { printf("%d ", arr[i]); } return 0; }
memcmp
函数功能
内存比较,比较两块内存中前num个字节的大小。
函数参数
int memcmp ( const void * ptr1, const void * ptr2, size_t num ); # int 函数返回值; # void* ptr1 void* ptr2 要比较的两块内存; # size_t num 要比较的字节数;
函数返回值
>0 : ptr1 大于 ptr2; =0 : ptr1 等于 ptr2; <0 : ptr1 小于 ptr2;··
函数使用
#include <stdio.h> #include <string.h> //memcmp对应头文件 int main() { int arr1[] = { 1,2,3,4,5 }; int arr2[] = { 1,2,3 }; int ret = memcmp(arr1, arr2, 3 * sizeof(int)); printf("%d\n", ret); return 0; }
模拟实现
#include <stdio.h> #include <assert.h> int my_memcmp(const void* ptr1, const void* ptr2, size_t num) { assert(ptr2 && ptr2); size_t count = 0; //用来标记相等的字节数 while (*(char*)ptr1 == *(char*)ptr2 && count < num) //一直循环,直到找到不相等的字节数据 { count++; //进入一次循环表示有一对字节相等,count++ ptr1 = (char*)ptr1 + 1; ptr2 = (char*)ptr2 + 1; } if (count < num) //如果count小于num,说明两块内存的前num个字节中有不相等的数据 return *(char*)ptr1 - *(char*)ptr2; //直接返回数据的差值 else return 0; //count等于num,说明相等 } int main() { int arr1[] = { 1,2,3,4,5 }; int arr2[] = { 1,2,3 }; int ret = my_memcmp(arr1, arr2, 3 * sizeof(int)); printf("%d\n", ret); return 0; }
memset
函数功能
内存设置,把一块内存中num个字节的内容设置为指定的数据。
函数参数
void *memset( void *dest, int c, size_t count ); # void* 函数返回值,返回目标空间的地址; # int c 函数参数,指定你想要初始化的数据; # size_t count 函数参数,指定初始化的字节数
函数使用
#include <stdio.h> #include <string.h> //memset对应头文件 int main() { char str[] = "hello world"; memset(str, 'x', 5); printf("%s\n", str); return 0; }
模拟实现
#include <stdio.h> #include <assert.h> void* my_memset(void* dest, int c, size_t num) { assert(dest != NULL); void* ret = dest; //记录目标空间的地址 while (num--) //循环将num个字节的内容初始化为指定值 { *(char*)dest = c; dest = (char*)dest + 1; } return ret; } int main() { char str[] = "hello world"; my_memset(str, 'x', 5); printf("%s\n", str); return 0; }