在编程的过程中,经常要处理字符和字符串,为了方便操作字符和字符串,c语言标准库提供了一系列库函数,接下来学习一下这些函数
c语言有一系列函数专门做字符分类的,也就是一个字符属于什么类型的字符
这些函数都需要一个头文件ctype.h
函数 | 如果它的参数符合下列条件就返回真 |
---|---|
iscntrl | 任何控制字符 |
isspace | 空白字符: 空格 ’ ’ ,换页 ‘\f’ , 换行 ‘\n’ , 回车 ‘\r’ , 制表符 ‘\t’ , 垂直制表符 ‘\v’ |
isdigit | 十进制数字 0-9 |
isxdigit | 十六进制数字,包括所有十进制数字, 小写字母a-f,大写字母A-F |
islower | 小写字母a-z |
isupper | 大写字母A-Z |
isalpha | 字母a-z或A-Z |
isalnum | 字母或者数字,a-z,A-Z,0-9 |
ispunct | 标点符号,任何不属于数字或者字母的图形字符 (可打印) |
isgraph | 任何图形字符 |
isprint | 任何可打印字符,包括图形字符和空白字符 |
控制字符说明,当c在0x00-0x1F之间或等于0x7F(DEL)时,返回非零值,否则返回零
这些函数的使用方法类似,只讲解一个:
int islower (int c);
islower函数能够判断参数部分的c是否是小写字母
通过返回值来说明是否是小写字母,如果是小写字母就返回非0的整数,如果不是小写字母,则返回0
练习
写一个代码,将字符串中的小写字母转大写,其他字符不变
int i = 0;
char str[] = "test string.\n";
char c;
while (str[i]) {
c = str[i];
if (islower(c)) {
c -= 32;
}
putchar(c);
i++;
}
输出
c语言提供了两个字符转换函数:
int tolower ( int c ); //将参数的大写字母转小写
int toupper ( int c ); //将参数的小写字母转大写
有了转换函数,就可以用这种方法实现上面的功能
int i = 0;
char str[] = "test string.\n";
char c;
while (str[i]) {
c = str[i];
if (islower(c)) {
c = toupper(c);
}
putchar(c);
i++;
}
函数原型
size_t strlen ( const char * str);
\0
作为结束标志,strlen函数返回\0之前字符个数,不包含\0使用
const char* str1 = "abcdef";
const char* str2 = "bbb";
if(strlen(str2)-strlen(str1)>0)
{
printf("str2>str1\n");
}
else
{
printf("srt1>str2\n");
}
上面由于strlen返回的是无符号,所以短的减长的会是一个无符号,大于0,输出str2>str1
输出:
strlen的模拟实现
方式1,计数器:
//计数器的方式
int my_strlen(const char* str) {
assert(str);
int count = 0;
while (*str) {
count++;
str++;
}
return count;
}
方式2,指针:
//指针-指针的方式
int mystrlen(const char* str) {
assert(str);
char* p = str;
while (*p) {
p++;
}
return p - str;
}
方式3 递归:
int my_strlen(const char* str) {
assert(str);
if (*str == '\0') {
return 0;
}
else {
return 1 + my_strlen2(str+1);
}
}
char* strcpy ( char* destination, const char* source );
复制源指针的字符串到目标指针,遇到空字符结束
\0
结束模拟实现
char* my_strcpy(char* destination,const char* source) {
char* ret = destination;
assert(destination);
assert(source);
while (*source) {
*destination = *source;
destination++;
source++;
}
*destination = '\0';
return ret;
}
拼接源字符串到目标字符串,目标字符串的结尾空字符被新字符第一个字符覆盖
模拟实现
char* my_strcat(char* dest,const char* src) {
char* ret = dest;
assert(dest);
assert(src);
while (*dest) {
dest++;
}
while (*src) {
*dest = *src;
dest++;
src++;
}
*dest='\0';
return ret;
}
这个函数开始比较每个字符串的第一个字符,如果相等继续比较下一对,直到不同或者空字符结束
模拟实现
int my_strcmp(const char* str1,const char* str2) {
assert(str1);
assert(str2);
while (*str1 == *str2) {
if (*str1 == '\0') {
return 0;
}
str1++;
str2++;
}
return *str1 - *str2;
}
char* strncpy ( char* destination, const char* source, size_t num );
拷贝原字符串前num个字符到目标字符串,如果在复制num个字符后,原字符串的末尾被找到,目标指针用0填充直到写入num个字符
使用
char string[32] = "Cats are nice";
strncpy( string, "Dog", 3 );
printf ( "After: %s\n", string );
输出:
char* strncat ( char* destination, const char* source, size_t num );
char str1[20];
char str2[20];
strcpy (str1,"To be ");
strcpy (str2,"or not to be");
strncat (str1, str2, 6);
printf("%s\n", str1);
拼接str2前6个字符到str1末尾,也就是or not
输出:
int strcmp ( const char* str1, const char* str2, size_t num ) ;
比较str1和str2的前num个字符,如果相等继续往后比较,最多比较num个,如果提前发现不一样就提前结束,如果num个字符都相等,就返回0
char* strstr ( const char* str1, const char* str2) ;
char str[] ="This is a simple string";
char * pch;
pch = strstr (str,"simple");
printf("%p\n", pch );
输出:
char* my_strstr(const char* str1, const char* str2) {
char* start1 = (char*)str1;
char* s1, *s2;
if (!*str2)
return ((char*) str1);
while (*start1) {
s1 = start1;
s2 = (char*) str2;
while (!(*s2-*s1) && *s1 && *s2) {
s1++;
s2++;
if (!*s2) {
return start1;
}
}
start1++;
}
return NULL;
}
使用
char arr[] = "[email protected]";
char* sep = ".@";
char* s = NULL;
//分开输出
s = strtok(arr,sep);
printf("%s ",s);
s = strtok(NULL, sep);
printf("%s ", s);
s = strtok(NULL, sep);
printf("%s ", s);
s = strtok(NULL, sep);
printf("%s ", s);
//循环
for (s = strtok(arr, sep); s != NULL; s = strtok(NULL, sep)) {
printf("%s ", s);
}
char* strerror ( int errnum ) ;
可以吧参数部分错误码对应的错误字符串返回
在不同的系统和c标准库都规定了一些错误码,一般放在 errno.h
头文件,用一个全局的变量errno来记录错误码,刚开始是0,表示没有错误,数字很难理解错误信息,所以可以通过数字打印错误信息
//打印0-10的错误信息
for (int i = 0; i <= 10; i++) {
printf("%d :%s\r\n", i, strerror(i));
}
输出 (win10 vs2022):
使用
FILE* pfile = fopen("exist.ent","r");
if (pfile == NULL) {
printf("error opening file exist.ent:%s",strerror(errno));
}
由于目录不存在此文件,会打印错误信息,不存在该文件
输出
也可以使用perror函数,可以直接将错误信息打印出来,不需要错误码,还会先输出冒号和空格,再打印错误信息
FILE* pfile = fopen("exist.ent","r");
if (pfile == NULL) {
//printf("error opening file exist.ent:%s",strerror(errno));
perror("error opening file exist.ent");
}
输出
上面的strstr函数的使用使用的是一种BF算法,从T字符串和S字符串的第一个开始比较,如果相等继续比较第二个,不相等,比较T字符的第二个和S字符串的第一个,它的时间复杂度是 O (m*n),m和n可看做是两个字符串的长度。这种方法是可以优化的,如果S字符串的长度是5,当T字符串剩下的长度不足5时,肯定是匹配失败的,这时,可以提前返回失败
这里讲解另一种匹配思路,KMP方法
简介
kmp算法是字符匹配算法,核心是利用匹配失败后的信息,尽量减少子串的匹配次数达到快速匹配的目的。具体通过next()的函数实现,本身包含了模式串的局部匹配信息,时间复杂度为O(m+n)
KMP的i值不会回退,只回退j,j也不是一定回退到0位置
上面的一个主串和子串,按照BF匹配,匹配到5下标,一个是d,一个是c,所以需要回退,j回退到0,i回退到刚开始匹配的下标的下一个,也就是1下标,继续开始匹配
但我们再想一想,既然能匹配到最后一个字符,说明两个串前面有一部分是相同的,例如上图的第一个蓝色下划线ab,子串中有两个ab,之所以用第一个ab,是因为中间有可能有其他字母漏匹配,以第一次匹配的为准。
在这种情况下,i没必要回退,继续匹配下一个字符d,j也没必要回退到0下标,因为ab和上面匹配到的ab是一样的,j完全可以回退到ab后的2下标继续匹配,这时同时移动两个下标,两个字符串完全匹配上了,就用没有回退的方式找到了子串的位置
上面可能是一个特定的例子,但可以总结出这种思路,根据不同的结果调整j返回的下标,,这个就是next数组,,它保存了每个结果匹配不成功j返回的下标,不同的j值对应了一个k值,k就是返回的位置
k值的计算规则是这样:
规则: 找到匹配成功部分两个相等的真子串 (不包括本身),一个以下标0开始,另一个以j-1下标结尾。若找到,k值找到的子串长度,没找到填0
不管什么数据,next[0] = -1,next[1] = 0
以下面的练习来说明next数组的填写规则
接下来看2下标,按照规则1的说法,需要找两个真子串,一个以下标0开始,一个以j-1,也就是找以a开始以b结尾的两个真子串,只有一个ab,所以填0
然后看3下标,就是找以0下标开始,2下标结束的两个子串,也就是以a开始,a结尾的子串,有两个a,长度是1,填1
到下标4的时候,找以a开始b结尾的两个子串,0和1下标的ab和2和3下标的ab符合条件,ab长度是2,填2
按照上述规则继续往下,得出下面结果:
那么如何将这种方法转换为程序呢?这里我们分为两种情况
第一种情况,要填的下标的前一个字母和匹配的第一个字串的下一个相等,下图所示
我们要填第9个下标,9下标需要根据8下标填,之前匹配到的字串是abc (绿色下划线),可以发现8下标和k下标的字母都是a,说明9下标肯定可以匹配abca,所以在8下标的基础上加1。
j看做需要填的下标,可以得出规则,k是next[j-1],如果next[j-1] == next[k] ,next[j] = next[j-1] +1
第二种情况,要填的j-1下标的字母和匹配到的第一个字串的下一个字符不一致
需要填6下标,i下标和k下标一个是a,一个是c,不一致。所以k需要回退,回退到k的位置,k的字符是a还是不一致,继续回到0下标,0下标时a,和i下标一致,所以填1,如果一直不相等,会回到0下标,值是-1的话,加1,填为最小值0就行
规则,如果next[j-1] != next[k] , k = next[k],一直重复,直到找到 next[k] == next [j-1],如果值为-1,填为0
得出以上两种情况,就可以写代码了
代码
//获取next数组
void GetNext(char* sub,int* next) {
next[0] = -1;
next[1] = 0;
int lensub = strlen(sub);
int i = 2; //当前下标
int k = 0; //前一项k值
while (i<lensub) {
if (k==-1 || sub[i - 1] == sub[k]) {
next[i] = k + 1;
i++;
k++;
}
else {
k = next[k];
}
}
}
//kmp
int KMP(const char* str,const char* sub ) {
assert(str!=NULL && sub!=NULL);
int lenstr = strlen(str);
int lensub = strlen(sub);
if (lenstr == 0 || lensub == 0) return-1;
int* next = (int*)malloc(sizeof(int) * lensub);
assert(next != NULL);
GetNext(sub,next);
int i = 0; //遍历主串
int j = 0; //遍历子串
while (i<lenstr && j<lensub) {
if (j==-1 || str[i] == sub[j]) {
i++;
j++;
}
else {
j = next[j];
}
if (j >= lensub) {
//找到了,返回下标
return i - j;
}
}
//没找到,返回-1
return -1;
}
测试
printf("%d\r\n", KMP("ababcedfdab", "abab")); //0
printf("%d\r\n", KMP("ababcedfdab", "ababf")); //-1
printf("%d\r\n", KMP("ababcedfdab", "edfd")); //5
输出