在C语言中,我们经常要对字符或是字符串进行各种操作。那C语言究竟给了我们哪些方法呢,本篇文章就是让大家了解对字符和字符串处理相关的知识。
目录
1.字符函数
1.1字符分类函数
1.2字符转换函数
2.字符串函数
2.1strlen函数的使用和模拟实现
2.2strcpy函数的使用和模拟实现
2.3strcat函数的使用和模拟实现
2.4strcmp函数的使用和模拟实现
2.5strncpy函数的使用和模拟实现
2.6strncat函数的使用和模拟实现
2.7strncmp函数的使用和模拟实现
2.8strstr函数的使用和模拟实现
2.9strtok函数的使用
2.10strerror函数与perror函数的使用
1.字符函数
字符函数分为字符分类函数和字符转换函数。在使用字符函数前,我们要包含头文件ctype.h
1.1字符分类函数
C语言中有一系列函数是专门给字符进行分类的,称为字符分类函数,具体函数如下:
函数 | 参数符合就返回真,反之为假 |
isalpha | 字母字符 |
islower | 小写字母 |
isupper | 大写字母 |
isdigit | 数字字符 |
isalnum | 字母、数字字符 |
isspace | 空白字符:空格" ",换页"\f",换行"\n",回车"\r",制表符"\t",垂直制表符"\v"。 |
iscntrl | 控制字符 |
isprint | 可打印字符 |
ispunct | 标点字符 |
isgraph | 图形字符 |
这些函数的使用方法都类似,我们这里以islower和isupper举例:
1 int islower(char c);2 int isupper(char c);
写一个代码,将字符串中的小写字母转大写,其他字符不变。
#include <stdio.h>
#include <ctype.h>int main()
{char arr[] = "I am a student.";int i = 0;while (arr[i] != '\0'){if (islower(arr[i]));{arr[i] -= 32;//大小写字母ASCII码相差32,如'A'为97,'a'为65}i++;}return 0;
}
这里我们就用到了islower函数,只要其中的参数是小写字符,就会返回真。其余字符分类函数都是类似的,大家自行尝试练习。
1.2字符转换函数
C语言中一共有两个字符转换函数:
tolower(char c) | 将大写字母c转换为小写字母 |
toupper(char c) | 将小写字母c转换为大写字母 |
使用也是非常的简单,我们将上面1.1中代码的转换大小写部分用toupper进行改写:
#include <stdio.h>
#include <ctype.h>int main()
{char arr[] = "I am a student.";int i = 0;while (arr[i] != '\0'){if (islower(arr[i]));{arr[i] = toupper(arr[i]);//将字符串中的每个字符都转换为大写}i++;}return 0;
}
2.字符串函数
我们不仅仅指对字符进行操作,有时也需要对字符串进行操作。和字符串操作相关的函数称为字符串(操作)函数。
2.1strlen函数的使用和模拟实现
函数原型:
size_t strlen(const char* str);
1.strlen函数的参数是即字符串(或字符数组),即首元素地址,统计的是字符串中 '\0' 前面出现的字符个数。
2.函数的返回值是size_t,是无符号的。
3.字符串必须以'\0'结束,否则结果为随机值。
4.不能传字符,函数会将其ASCII码值当做地址,由于地址偏小,可能被操作系统所使用,返回值为error。
#include <stdio.h>
#include <string.h>int main()
{if (strlen("abc") - strlen("abcdef") > 0)//if ((int)strlen("abc") - (int)strlen("abcdef") > 0){printf("str1>str2\n");}else{printf("str1<=str2\n");}return 0;
}
上述例子中,我们能得到strlen("abc")的值是3,strlen("abcdef")的值是6,3-6的-3,所以结果为str1<=str2。
然而正确结果为str1>str2。这是因为二者的返回值是size_t类型,是无符号的一种整型,所以两个无符号的数相加减得到的数也是无符号的,结果恒为正,应为str1>str2。
5.strlen函数的模拟实现
方法一:count计数器
size_t my_strlen(const char* str)
{assert(str);int count = 0;while (*str != '\0'){str++;count++;}return count;
}
方法二:指针-指针
size_t my_strlen(const char* str)
{assert(str);char* start = str;while (*str != '\0'){str++;}return str-start;
}
方法三:函数递归
不能创建临时变量计数器的方式
size_t my_strlen(const char* str)
{assert(str);if (*str == '\0')return 0;elsereturn my_strlen(str + 1) + 1;
}
2.2strcpy函数的使用和模拟实现
函数原型:
char* strcpy(char* destination, const char* source);
#include <stdio.h>
#include <string.h>int main()
{char arr1[20] = "Wow, ";char arr2[] = "fantastic!";strcpy(arr1, arr2);printf("%s\n", arr2);return 0;
}
显然上面代码的结果为fantastic!
4.strcpy函数的模拟实现
char* my_strcpy(char* dest, const char* src)
{assert(dest&&src);char* ret = dest;while (*dest++ = *src++){;}return ret;
}
我们让src的每一位都赋值给dest,然后++,直到*src为'\0'时,赋值结束,同时也会将'\0'赋值过去。但是我们返回的是dest原来那个位置的地址,所以我们再最开始要定义一个指针ret等于dest,和dest指向相同位置,最后返回这个位置即ret。
2.3strcat函数的使用和模拟实现
函数原型:
char* strcat(char* dest, const char* src);
#include <stdio.h>
#include <string.h>int main()
{char arr1[20] = "Wow, ";char arr2[] = "fantastic!";strcat(arr1, arr2);printf("%s\n", arr1);return 0;
}
上面代码将arr2连接到arr1后面,结果显然为Wow, fantastic!
4.strcat函数的模拟实现
char* my_strcat(char* dest, const char* src)
{assert(dest && src);char* ret = dest;while (*dest != '\0')dest++;while (*dest++ = *src++){;}return ret;
}
其实和strcpy函数模拟实现相差不大,只是要将dest位置移到'\0'的位置,再将src拷贝过来就行了。
5.自己给自己追加
当一个字符串arr自己追加自己的时候,也就是my_strcat(arr,arr),这样代码是有问题的。我们可以仔细看看我们模拟实现的代码再结合当两个字符串都相同进行分析,就会发现当dest移动到'\0'位置将*src++赋值给*dest++时,字符串中的'\0'被不断覆盖,而while循环结束的条件是*src=='\0',也就是程序会不断在while循环中不停止,造成程序崩溃。下图是第一个'\0'被覆盖的情形:
但是我们库函数中的strcat是可以实现字符串的自身追加的,我们的模拟函数在这一点其实是有缺陷的,也许是因为VS中strcat的实现方法和我们不同。但是这也给我们提了个醒,当涉及到字符串自身的追加时,我们还是尽量不要使用strcat,万一不行呢?那具体用什么方法我在之后篇章会写到。
2.4strcmp函数的使用和模拟实现
函数原型:
int strcmp(const char* str1, const char* str2);
#include <stdio.h>
#include <string.h>int main()
{char arr1[] = "apple";char arr2[] = "applf";char arr3[] = "Apple";printf("%d\n", strcmp(arr1, arr2));//-1printf("%d\n", strcmp(arr2, arr3));//1printf("%d\n", strcmp(arr1, arr3));//1printf("%d\n", strcmp(arr1, arr1));//0return 0;
}
2.strcmp函数的模拟实现
int my_strcmp(const char* str1, const char* str2)
{assert(str1 && str2);while (*str1 == *str2){if (*str1 == '\0')return 0;str1++;str2++;}return *str1 - *str2;
}
虽然VS上的结果分别为-1,0,1,但是我们只需要做到标准规定的要求就行了,即保证是负数、0、正数即可。如果两者开始字母都相等,那就往后一个一个比较,直到二者都为'\0'即两字符串相同,返回0。
2.5strncpy函数的使用和模拟实现
函数原型:
char* strncpy(char* destination, const char* source, size_t num);
1.strncpy函数是将源字符串的num个字符拷贝到目标字符串中,因此我们不希望源字符串被改变,类型为const char*。
2.如果源字符串的长度小于num,则拷贝完源字符串之后,在目标空间的后边追加0,直到num个。
#include <stdio.h>
#include <string.h>int main(){char arr1[20] = "anvelope";char arr2[] = "happy";strncpy(arr1, arr2, 5);printf("%s\n", arr1);//happyopereturn 0;}
3.strncpy函数的模拟实现
char* my_strncpy(char* dest, const char* src, size_t num)
{assert(dest && src);char* str = dest;for (size_t i = 0; i < num; i++){*dest++ = *src++;}return str;
}
我们只要在strcpy的基础上再外加一个循环就行了,循环num次即可。(暂不考虑加0的情况)。
2.6strncat函数的使用和模拟实现
函数原型:
char* strncat(char* destination, const char* source, size_t num);
1.将source指向字符串的前num个字符追加到destination指向的字符串末尾,再追加⼀个 '\0' 。因此我们不希望源字符串被改变,类型为const char*。
2.如果source指向的字符串的长度小于num的时候,只会将字符串中到'\0'的内容追加到destination指向的字符串末尾。
#include <stdio.h>
#include <string.h>int main()
{char arr1[] = "anvelope ";char arr2[20] = "happy";strncat(arr2, arr1, 8);printf("%s\n", arr2);//happy anvelopereturn 0;
}
3.strncat函数的模拟实现
char* my_strncat(char* dest, const char* src, size_t num)
{assert(dest && src);char* str = dest;while (*dest != '\0')dest++;for (size_t i = 0; i < num; i++){*dest++ = *src++;}*dest='\0';return str;
}
仍然在strncat的基础上外加一层循环,最后再追加一个'\0'即可。
2.7strncmp函数的使用和模拟实现
函数原型:
int strncmp(const char* str1, const char* str2, size_t num);
2.同strcpy标准,但是只比较前num个字符:
#include <stdio.h>
#include <string.h>int main()
{char arr1[] = "anvelope ";char arr2[] = "happy";int ret = strncmp(arr1, arr2, 3);//-1printf("%d\n", ret);return 0;
}
3.strncpy函数的模拟实现
int my_strncmp(const char* str1, const char* str2, size_t num)
{assert(str1 && str2);while ((*str1 == *str2) && --num){if (*str1 == '\0')return 0;str1++;str2++;}return *str1 - *str2;
}
2.8strstr函数的使用和模拟实现
函数原型:
char * strstr ( const char * str1, const char * str2);
1.strstr函数返回字符串str2在字符串str1中第⼀次出现的位置,因此并不希望字符串被改变,顾指针类型为const char*。
2.如果str2的第一个字符为'\0',则返回str1。
#include <stdio.h>
#include <string.h>int main()
{char arr1[] = "An apple a day keeps a doctor away.";const char* p = "keep";char* ret = strstr(arr1, p);if (ret != NULL){printf("%s\n", ret);}else{printf("没找到\n");}return 0;
}
上述代码要现在arr1中找到p第一次出现的位置,即第一个字符串中k的位置,返回这个位置,得到的就是从k开始,'\0'结束的字符串,即keeps a doctor away。
3.strstr函数的模拟实现
这个函数的模拟实现比之前的字符串函数难度要高一些,我们要考虑多种情况,也需要涉及多个指针。我们先看下面一种比较简单的情况:
在这种情况中,我们需要几个指针:cur用来遍历str1,当满足相等条件时,用s1和s2来判断后面的几个字符是否都和str2相等,这是最基本的情况,我们接下来看复杂一点的情况:
这种情况和第一种有什么区别呢?主要是在于当cur遍历到第一个字符e的时候,s1和s2会判断cur后面4个字符是否和str2相等,而且这是不相等的情况,因为从第一个e开始是eees,和str2不同。此时我们应该让cur++,从第二个e的位置开始重新利用s1和s2进行比较,发现和s2相等,即返回此时的cur。我们再看一种情况:
这种情况就是,str2并没有再str1中出现,那么我们直接返回NULL就好了。
出了上面三种情况,同时我们还需要注意一些特殊的情况,就是我们是认为*s1和*s2如果相等的话就继续比下一对,只要相等就一直往下比,那就有可能出现下面这种情况:
也就是如果*s1和*s2都为'\0'的时候,s1和s2就会造成越界访问,我们应该避免这种情况。
了解了以上的情况之后,我们就可以开始写代码了
char* my_strstr(const char* str1, const char* str2)
{const char* s1 = NULL;const char* s2 = NULL;const char* cur = str1;if (*str2 == '\0')return (char*)str1;while (*cur){s1 = cur;s2 = str2;while (*s1 == *s2 && *s1 != '\0' && *s2 != '\0'){s1++;s2++;}if (*s2 == '\0')return (char*)cur;cur++;}return NULL;
}
需要注意内层while循环的条件是在*s1等于*s2的同时*s1和*s2都不能为'\0',在这种情况下如果跳出了循环是因为*s2为'\0',那就说明str2在str1中出现了,此时我们返回cur就可以了。特殊地,当s2本身里面就是空的,也就是*s2就已经为'\0',我们直接返回str1就好了,这是极其特殊的情况,我们单独处理即可。
2.9strtok函数的使用
函数原型:
char * strtok ( char * str, const char * sep);
#include <stdio.h>
#include <string.h>int main()
{char arr1[] = "has@been.slained";char arr2[30] = { 0 };strcpy(arr2, arr1);//保护arr1const char* sep = "@.";char* ret = strtok(arr2, sep);printf("%s\n", ret);char* ret = strtok(NULL, sep);//strtok会记住其位置,当第二次传参为NULL,继续寻找下一个标记printf("%s\n", ret);char* ret = strtok(NULL, sep);printf("%s\n", ret);return 0;
}
这样写的话当分隔符较多时比较复杂,此时我们可以用for循环来简化代码:
#include <stdio.h>
#include <string.h>int main()
{char arr1[] = "has@been.slained";char arr2[30] = { 0 };strcpy(arr2, arr1);const char* sep = "@.";char* ret = NULL;for (ret = strtok(arr2, sep); ret != NULL; ret = strtok(NULL, sep)){printf("%s\n", ret);}return 0;
}
2.10strerror函数与perror函数的使用
函数原型:
char * strerror ( int errnum );
我们可以多试试几个错误码,看看它所对应的错误信息分别是什么:
#include <stdio.h>
#include <string.h>
#include <errno.h>int main()
{for (int i = 0; i <= 10; i++){printf("%d: %s\n", i, strerror(i));}return 0;
}
大家可以自己在编译器上试试,在Windows11+VS2022环境下输出的结果如下:
1 No error2 Operation not permitted3 No such file or directory4 No such process5 Interrupted function call6 Input/output error7 No such device or address8 Arg list too long9 Exec format error10 Bad file descriptor11 No child processes
我们再来看一个常见的例子:
#include <stdio.h>
#include <string.h>
#include <errno.h>int main()
{//fopen以读的形式打开文件,如果文件不存在,就打开失败FILE* pf = fopen("test.txt", "r");if (pf == NULL){printf("%s\n",strerror(errno));return 1;}//读文件//......//关闭文件fclose(pf);return 0;
}
输出:
No such file or directory
当我们打开文件test.txt时打开失败,即pf为空指针的时候,我们就让它返回错误的信息。那么接下来,我们来看看perror函数。
函数原型:
void perror(const char* str);
perror函数相当于是对strerror的使用进行了封装。我们上面的代码要用printf函数来打印出错误信息,而perror函数直接将错误信息打印出来。perror函数打印完参数部分的字符串后,再打印一个冒号和一个空格,再打印错误信息。所以上述代码我们也可以写成:
#include <stdio.h>
#include <string.h>
#include <errno.h>int main()
{//fopen以读的形式打开文件,如果文件不存在,就打开失败FILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen fail!");return 1;}//读文件//......//关闭文件fclose(pf);return 0;
}
输出:
fopen fail!: No such file or directory
那以上就是字符函数与字符串函数相关的重要知识。当然函数并不止这几个,上述只是我们经常用到或比较重要的。大家在熟练运用本章的函数后可以自行拓展学习新的函数。