由浅到深认识C语言(10):字符串处理函数
该文章Github地址:https://github.com/AntonyCheng/c-notes
在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址:https://blog.csdn.net/AntonyCheng/article/details/136555245),该模板集成了最常见的开发组件,同时基于修改配置文件实现组件的装载,除了这些,模板中还有非常丰富的整合示例,同时单体架构也非常适合SpringBoot框架入门,如果觉得有意义或者有帮助,欢迎Star & Issues & PR!
上一章:由浅到深认识C语言(9):动态内存分配
10.字符串处理函数
对字符串的处理一般在嵌入式编程,应用编程和网络编程中会大量用到;
字符串主要有拷贝,连接,比较,切割,变换等操作;
要求熟练使用常见字符串处理函数,并且会编写典型的字符串操作函数;
字符串操作函数有如下:
strlen
长度的测量;strcpy/strncpy
字符串拷贝;strcat/strncat
字符串连接;strcmp/strncmp
字符串比较;
字符串头文件: #include<string.h>
关于 Visual Studio 的安全模式需要补充的解释,在 Visual Studio 中默认的一些输入输出操作均为安全模式下的操作,即 函数名_s 的结构,该结构对于字符串来说影响极大,因为字符串的长度是不好把控的,很容易造成内存的溢出,所以为了解决此问题,在相关函数操作字符串时,会跟一个限制大小的 int 参数,即接下来操作的字符数组(字符串)的长度不能超过该长度;
10.1.测字符串长度函数
原型: int strlen(const char *str)
参数说明: str
是被测量的字符串首元素地址,const
是一个关键字,在 10.9 中会有介绍;
返回值: 返回字符串的长度,不包含 \0
和 null 字符;
这个函数虽然传的是 * 类型,但是不会修改其指向的内容;
示例如下:
#include<stdio.h>
#include<string.h>void test() {char buf1[128] = "hello world";char buf2[] = "hello world";char buf3[] = "hello\0world";char buf4[] = "hello\123\\world";printf("sizeof(buf1) = %d\n", sizeof(buf1));printf("strlen(buf1) = %d\n", strlen(buf1));printf("sizeof(buf2) = %d\n", sizeof(buf2));printf("strlen(buf2) = %d\n", strlen(buf2));printf("sizeof(buf3) = %d\n", sizeof(buf3));printf("strlen(buf3) = %d\n", strlen(buf3));printf("sizeof(buf4) = %d\n", sizeof(buf4));//hello--5个 \123--1个 \\--1个 world--5个 \0--1个printf("strlen(buf4) = %d\n", strlen(buf4));//hello--5个 \123--1个 \\--1个 world--5个 return;
}int main(int argc, char* argv[]) {test();return;
}
打印效果如下:
sizeof 和 strlen 的主要区别
sizeof
计算的是字符串中所有的元素,且加上最后一位隐藏掉的 \0
;
strlen
计算的是字符串中可见范围的元素,遇到 \0
就结束,不管其是否是最后的隐藏位;
实例:重写函数
自己定义一个 strlen 函数测量字符串长度;
#include<stdio.h>
#include<string.h>int my_strlen(char* p) {int count = 0;int turn = 1;while (turn) {if (*(p + count) != '\0') {count++;}else {turn = 0;}}return count;
}int main(int argc, char* argv[]) {char ch[] = "hello\123\\ world";int length1 = strlen(ch);int length2 = my_strlen(ch);printf("strlen = %d\n", length1);printf("my_strlen = %d", length2);return 0;
}
打印效果如下:
10.2.字符串拷贝函数
原型一: char* strcpy(char* dest,const char* src)
- 功能:把 src 所指向的字符串赋值到 dest 所指向的空间中;
- 返回值:返回 dest 字符串的首地址;
- 注意:遇到
\0
会结束,只是\0
也会被拷贝过去,需要保证 dest 足够大;
示例如下:
由于 VS 中该函数存在安全问题,所以需要按照编译器给定的安全模式输出,参数会不一样;
#include<stdio.h>
#include<string.h>void test() {char src[] = "hello world";char dest[128] = "";strcpy_s(dest,128 ,src); //安全模式下,还需要在中间插入需要传的字节大小,我们直接使用dest的字节大小printf("dest = %s", dest);
}int main(int argc, char* argv[]) {test();return;
}
打印效果如下:
有 \0
的情况:
#include<stdio.h>
#include<string.h>void test() {char src[] = "hello\0world";char dest[128] = "";strcpy_s(dest,128 ,src);printf("dest = %s", dest);
}int main(int argc, char* argv[]) {test();return;
}
打印效果如下:
原型二: char* strncpy(char* dest,const char* src,int num)
- 功能:把 src 所指向的字符串赋值到 dest 所指向的空间中;
- 参数说明:num 表示从第一位字符开始所拷贝字符的个数,若数字过大,则遇 \0 结束;
- 返回值:返回 dest 字符串的首地址;
- 注意:遇到
\0
会结束,但是\0
不会被拷贝过去,需要保证 dest 足够大;
实例:重写函数
自己定义一个 strcpy 函数来拷贝字符串;
#include<stdio.h>
#include<string.h>void my_strcpy(char* p1,char* p2) {int i = 0;int turn = 1;while (turn) {if (*(p2 + i) != '\0') {*(p1 + i) = *(p2 + i);i++;}else {*(p1 + i) = '\0';turn = 0;}}
}int main(int argc, char* argv[]) {char ch_1[] = "hello wo\0rld";char ch_2[128] = "";char ch_3[128] = "";my_strcpy(ch_2, ch_1);printf("sizeof(ch_2) = %d\n", sizeof(ch_2));printf("%s\n", ch_2);strcpy_s(ch_3,128,ch_1);printf("sizeof(ch_3) = %d\n", sizeof(ch_3));printf("%s\n", ch_3);return 0;
}
打印效果如下:
10.3.字符串拼接函数
原型一:char* strcat(char* dest,const char* src);
-
**功能:**将 src 的字符串拼接到 dest 的末尾(这里指的是 dest 中第一个 \0),且需要 dest 有足够的空间;
-
示例如下:
由于 VS 中该函数存在安全问题,所以需要按照编译器给定的安全模式输出,参数会不一样;
#include<stdio.h> #include<string.h>void test() {char ch1[] = "world";char ch2[128] = "hello ";strcat_s(ch2 ,128 ,ch1);printf("%s", ch2); }int main(int argc, char* argv[]) {test();return 0; }
打印效果如下:
原型二:char* strncat(char* dest,const char* src,int num);
-
**功能:**将 src 的字符串中的前 n 个字符拼接到 dest 的末尾(这里指的是 dest 中第一个 \0),且需要 dest 有足够的空间;
-
示例如下:
#include<stdio.h> #include<string.h>void test() {char ch1[] = "world";char ch2[128] = "hello ";strncat_s(ch2, 123, ch1, 3);printf("%s", ch2); }int main(int argc, char* argv[]) {test();return 0; }
打印效果如下:
实例:重写函数
自己定义一个 strcat 函数来连接字符串;
#include<stdio.h>
#include<string.h>void my_strcat(char* p2, char* p1) {int count = 0;while (*(p2 + count) != '\0') {count++;}int i = 0;int b = 1;while (b) {if (*(p1 + i) != '\0') {*((p2 + count) + i) = *(p1 + i);i++;}else {b = 0;}}
}int main(int argc, char* argv[]) {char ch1[] = "hello\0 world";char ch2[128] = "hello C ";char ch3[128] = "hello C ";my_strcat(ch2, ch1);strcat_s(ch3, 128, ch1);printf("ch2 = %s\n", ch2);printf("ch3 = %s", ch3);return 0;
}
打印效果如下:
10.4.字符串比较函数
原型一:int strcmp(const char* s1,const char* s2);
-
**功能:**将 s1 和 s2 指向的字符串逐个字符进行比较,用于整个字符串的比较;
-
返回值:
-
>0(一般是 1 ) 表示 s1 > s2 即两字符串不相同
-
<0(一般是 -1 ) 表示 s1 < s2 即两字符串不相同
-
=0 表示 s1 = s2 即两字符串相同
-
-
示例如下:
#include<stdio.h> #include<string.h>void test() {char s1[] = "hello world";char s2[] = "hello zorld";char s3[] = "hello aorld";char s4[] = "hello world";printf("'w' or 'z' = %d\n",strcmp(s1, s2));printf("'w' or 'a' = %d\n",strcmp(s1, s3));printf("'w' or 'w' = %d\n",strcmp(s1, s4));return; }int main(int argc, char* argv[]) {test();return 0; }
打印效果如下:
原型二:int strncmp(const char* s1,const char* s2,int num);
-
**功能:**将 s1 和 s2 指向的字符串前 n 个字符逐个进行比较,用于部分字符串的比较;
-
返回值:
-
>0(一般是 1 ) 表示 s1 > s2 即两部分字符串不相同
-
<0(一般是 -1 ) 表示 s1 < s2 即两部分字符串不相同
-
=0 表示 s1 = s2 即两部分字符串相同
-
实例:重写函数
自己定义一个 strcmp 函数来比较字符串;
#include<stdio.h>
#include<string.h>int my_strcmp(char* str1, char* str2) {int count1 = 0;int count2 = 0;int length1 = 0;int length2 = 0;int i1 = 0;int i2 = 0;while (*(str1 + i1) != '\0') {i1++;length1++;}while (*(str2 + i2) != '\0') {i2++;length2++;}i1 = 0;i2 = 0;if (length1==length2) {while (*(str1 + i1) != '\0') {if (*(str1 + i1) > *(str2 + i2)) {return 1;}if (*(str1 + i1) < *(str2 + i2)) {return -1;}i1++;i2++;}return 0;}else if (length1 > length2) {while (*(str2 + i2) != '\0') {if (*(str1 + i1) > *(str2 + i2)) {return 1;}if (*(str1 + i1) < *(str2 + i2)) {return -1;}i1++;i2++;}return 1;}else {while (*(str1 + i1) != '\0') {if (*(str1 + i1) > *(str2 + i2)) {return 1;}if (*(str1 + i1) < *(str2 + i2)) {return -1;}i1++;i2++;}return -1;}
}void test() {char str1[] = "abc";char str2[] = "abc";char str3[] = "aac";char str4[] = "acc";char str5[] = "abca";char str6[] = "aaca";printf("abc abc = %d\n", strcmp(str1, str2));printf("abc abc = %d\n", my_strcmp(str1, str2));printf("abc aac = %d\n", strcmp(str1, str3));printf("abc aac = %d\n", my_strcmp(str1, str3));printf("abc acc = %d\n", strcmp(str1, str4));printf("abc acc = %d\n", my_strcmp(str1, str4));printf("abc abca = %d\n", strcmp(str1, str5));printf("abc abca = %d\n", my_strcmp(str1, str5));printf("abc aaca = %d\n", strcmp(str1, str6));printf("abc aaca = %d\n", my_strcmp(str1, str6));
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
10.5.字符串变换函数
strchr字符查找
原型:char* strchr(const char* str1,char ch);
**功能:**在字符串 str1 中查找字符 ch 出现的的地址;
**返回值:**返回 ch 第一次出现的位置地址,找不到则返回空值;
示例如下:
我们运用 strchr
函数将 “hello world” 里的 ‘l’ 全部替换为 ‘a’;
#include<stdio.h>
#include<string.h>void test() {char str[] = "hello world";char* ret = NULL;while (1) {ret = strchr(str, 'l');if (ret == NULL) {break;}*ret = 'a';}printf("%s", str);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
strstr字符串查找
原型:char* strstr(const char* src,const char* dest);
**功能:**在字符串 src 中查找 dest 字符串出现的地址;
**返回值:**返回 dest 第一次出现的位置地址,找不到则返回空值;
示例如下:
利用 strstr 函数将 “www.dog.cat.dog.com” 中的 “dog” 用 * 屏蔽掉;
#include<stdio.h>
#include<string.h>void test() {char str1[] = "www.dog.cat.dog.com";char* ret = NULL;while (1) {ret = strstr(str1, "dog");if (ret == NULL) {break;}memset(ret, '#', strlen("dog"));}printf("%s", str1);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
10.6.字符串处理函数
memset字符填充
原型:void* memset(void* str,char c,int n);
**功能:**将 str 所指向的内存区的前 n 个全部用 c 填充,用于清楚指定空间,常用于 malloc 申请空间后的初始化操作;
**返回值:**返回 str 的地址;
atoi/atol/atof字符转换
头文件:#include<stdlib.h>
原型:
int atoi(const char* str);
long atol(const char* str);
double atof(const char* str);
功能:
将 str 所指向的 数字字符串转化为 int/long/double;
示例如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>void test() {char str1[] = "123.456";char str2[] = "123";printf("%f\n", (double)atoi(str2)+atof(str1));
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
实例:重写函数
自己定义一个 strcmp 函数来比较字符串;
#include<stdio.h>int my_atoi(char* str) {int sum = 0;while (*str != '\0'&& *str >= '0' && *str <= '9') {sum = sum * 10 + *str - '0';//这里要理解 ascii 中字符数与字符0的差值就是真实的数值str++;}return sum;
}int main(int argc, char* argv[]) {char str[] = "123";printf("%d\n", my_atoi(str) + 1);return 0;
}
打印效果如下:
strtok字符串切割
原型:char* strtok(char* str,const char* string);
**功能:**strtok 函数可以将一个字符串 s 以 string字符串==(字符串就代表要用双引号包裹)==为分界线进行切割;
注意:当函数在参数 s 字符串中发现参数 string 中包含的分割字符时,则会将该字符改为 \0 字符,当连续出现多个时只会替换第一个为 \0,即调用一次只能切割一次,在第二次调用时,string 字符串不变,s 字符串一定要设置成 NULL,因为第一次返回的地址就是下一次要切割的地址,传空值就能够接着该地址往下切割,每次调用成功则返回指向被分割出片段的指针,一般将切割后的独立字符串首地址存放在指针数组中;
这也是唯一一个要对原字符串进行改变的函数;
示例如下:
在 Visual Studio 中,该函数是一个安全性问题函数
char *strtok_s( char *strToken, const char *strDelimit, char **buf);
最后一个参数表示将剩余的字符串存储在 buf 指针数组中,而不是静态变量中,从而保证了安全性。
#include<stdio.h>
#include<string.h>int test() {char str[] = { "xixixi,hehehe,lalala" };char* protect[] = { NULL };char* arr[32] = { NULL };//第一次切割int i = 0;arr[i] = strtok_s(str, ",", protect);//第二次……切割while (arr[i] != NULL) {i++;arr[i] = strtok_s(NULL, ",", protect);}//输出结果i = 0;while (arr[i] != NULL) {printf("第%d次输出为:\n", i + 1);printf("%s\n", arr[i]);i++;}printf("原函数为:%s", str);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
10.7.sprintf组包
图解 printf:
**意义:**将零散的数据组成一个完整的整体;
格式:
int sprintf(str,"格式",数据);
//str:用于存放组好的报文
//"格式":按照格式组包
//数据:各个零散的数据
//返回值:返回的是组好的报文的实际长度(不包含 \0)
示例一如下:
在 Visual Studio 中,该函数是一个安全性问题的函数,需要在第二个参数中加入适当的长度限制,保证内存不溢出,通常可以直接使用存放该报文的数组长度;
#include<stdio.h>void test() {int year = 2022;int mon = 3;int day = 24;//要求:将以上变量组成一个"2022年3月24日"字符串char str[128] = "";sprintf_s(str, 128, "%d年%d月%d日", year, mon, day);printf("%s\n", str);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
示例二如下:
#include<stdio.h>void test() {char name[] = "陈一";int age = 20;char sex[] = "男";char addr[] = "四川省";char str[128] = "";sprintf_s(str, 128, "姓名:%s;年龄:%d;性别:%s;地址:%s", name, age, sex, addr);printf("%s\n", str);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
示例三如下:
将 int 型整数转换成字符串;
#include<stdio.h>void test() {int a = 100;int b = 200;char str[128] = "";sprintf_s(str, 128, "%d + %d = %d", a, b, a + b);printf("%s\n", str);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
10.8.sscanf解包
图解 scanf:
**意义:**将一个完整的整体拆解成零散的数据;
格式:
int sscanf(str,"格式",地址数据);
//str:存放报文的字符串地址
//"格式":按照格式组包
//数据:各个零散的需要获取数据的地址
注意:sscanf若遇到不满足条件的字符(条件的反面)时,会直接结束获取进程,即使该字符后有满足的字符;
示例如下:
#include<stdio.h>void test() {char str[] = "2022年3月24日";int year = 0;int mon = 0;int day = 0;sscanf_s(str, "%d年%d月%d日", &year, &mon, &day);printf("%d , %d , %d\n", year, mon, day);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
要注意提取的格式,%c、%d、%f、%lf 的提取类似,但是 %s 提取字符串就会有问题,因为提取的原理是对于字符数组进行逐个遍历,寻找适合要求的部分并进行取值,例如 %d 的范围是 1-9,%c 的范围是单个字符,%f 的范围是 float型,%lf 的范围是 double型,而如果我们要遍历一个字符串的话,它的范围就是所有字符(但会遇到空格结束),但是遍历内容全是字符,所以总是会拿到所有的内容,而不是我们想要的内容;
高级用法
用法一:使用 %*s 、%*d 跳过提取内容,即不要提取到的内容;
%*s 和 %s 的相同点在于都能提到数据,不同点在于前者不能被取到,而后者可以被取到,即能够拿出来赋值;
示例如下:提取 "1234 5678"
中的 "5678"
;
#include<stdio.h>void test() {int data1 = 0;sscanf_s("1234 5678", "%*d %d", &data1);//也可以这样//sscanf_s("1234 5678", "1234 %d", &data1);printf("data1 = %d\n", data1);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
所以我们可以用以上的方法和 %s 的特性来做一些复杂的操作:
提取 "abcdef 1234 sndhcbakdh 8763 ds 6.999"
中的数学数据;
#include<stdio.h>void test() {int data1 = 0;int data2 = 0;float data3 = 0;sscanf_s("abcdef 1234 sndhcbakdh 8763 ds 6.999", "%*s %d %*s %d %*s %f", &data1, &data2, &data3);printf("data1 = %d\n", data1);printf("data2 = %d\n", data2);printf("data3 = %f\n", data3);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
用法二:使用 %ns 、%nd 来提取指定宽度 n 的字符串或数据;
示例如下:
#include<stdio.h>void test() {int data = 0;sscanf_s("12abc5678", "%*5s%d", &data);printf("data = %d\n", data);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
**用法三:**支持集合的操作(正则表达式);
-
%[a-z]:提取 a-z 的字符串;
示例如下:
#include<stdio.h>void test() {char str[16] = "";sscanf_s("abcDeFgH","%[a-z]",str,16);printf("str = %s\n", str); }int main(int argc, char* argv[]) {test();return 0; }
打印效果如下:
-
%[aBc]:提取 aBc 中的任意一个;
示例如下:
#include<stdio.h>void test() {char str[16] = "";sscanf_s("aBcBcacBefacc","%[aBc]",str,16);printf("str = %s\n", str); }int main(int argc, char* argv[]) {test();return 0; }
打印效果如下:
-
%[^abc]:提取非 abc 中任何一个的字符;
示例如下:
#include<stdio.h>void test() {char str[16] = "";sscanf_s("ABCabcDeFgH", "%[^abc]", str, 16);printf("str = %s\n", str); }int main(int argc, char* argv[]) {test();return 0; }
打印效果如下:
高级用法案例
案例一:
需求: 现在有 data=0 和 str[16]="" ,需要从 “12345678” 中提取到 34 赋值给整型 data,再提取到 78 赋值给字符串 str ,一并输出;
#include<stdio.h>void test() {int data = 0;char str[16] = "";sscanf_s("12345678", "%*2d%2d%*2d%s", &data, str, 3);printf("data = %d\n", data);printf("str = %s\n", str);}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
案例二:
需求:给定一个形如 xxx@xxx.com
的网址,其中 xxx 不定长,提取两部分 xxx 分别放入两个字符数组中;
#include<stdio.h>void test() {char str1[16] = "";char str2[16] = "";sscanf_s("1999998888@qq.com", "%[^@]%*[@]%[^.]", str1, 16, str2, 16);printf("str1 = %s\n", str1);printf("str2 = %s\n", str2);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
案例三:
需求:在歌词格式中里提取歌词时间(不要毫秒)和内容(格式:[分:秒:毫秒][分:秒:毫秒]歌词内容
);
#include<stdio.h>void test() {int minute1 = 0;int second1 = 0;int minute2 = 0;int second2 = 0;char song[128] = "";char file[] = "[1:13:46][2:11:53]这是歌词的内容!";sscanf_s(file, "[%d:%d:%*d][%d:%d:%*d]%s", &minute1, &second1, &minute2, &second2, song, 128);printf("[%d:%d—%d:%d]:%s", minute1, second1, minute2, second2, song);}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
10.9.const关键字
const修饰普通变量
const修饰普通变量,表示该普通变量只读,即该变量只能取值,不能被赋值,类似于 Java 中的 final,但是有区别,Java中是绝对不能改变的一个值,但是在C语言中,在编译器固定只读变量地址的前提下,如果知道其地址,这个值也是可以改的,被 const 赋值后的变量指针类型为 const int* 类型,我们只需要将其强制转换成 int* 即可;
const修饰的变量一定要初始化,虽然可以通过地址类型的强制转换去更改内容,但是尽量不要去做;
只读示例如下:
改变权限示例如下:
#include<stdio.h>void test() {const num = 10;//将指针类型从const int*强制转换为int*,然后再取内容进行赋值*(int*)&num = 11;printf("num = %d\n", num);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
所以从底层来讲,该变量不能被修改的原因是此变量指针的指针类型不可重新指向另外的地址,只需要强制转换成可重定向的指针类型,就能临时性地永久改变该地址的内容(临时性指的是下一次需要改变时仍要重新强制转换,永久指的是从地址层改变该地址的内容);
const修饰指针星花
格式:const int* p
const修饰指针星花重在修饰 * 而不是 p ,所以修饰之后的效果是用户不能借助 *p 更改空间的内容,但是 p 可以指向其他空间,即 *p 只读,p 可读可写;
*p 只读示例如下:
p 可读可写示例如下:
#include<stdio.h>void test() {int num = 10;const int* p = #int num2 = 20;p = &num2;printf("num = %d\n", *p);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
const修饰指针变量
格式:int* const p
const修饰指针变量重在修饰 p ,而不是 * ,所以修饰后的效果是用户可以通过 *p 修改 p 的所指向空间的内容,但是不能再更改 p 的指向,即 *p 可读可写,p 只读;
p 只读示例如下:
*p 可读可写示例如下:
#include<stdio.h>void test() {int num1 = 10;int* const p = &num1;*p = 20;printf("num = %d\n", *p);
}int main(int argc, char* argv[]) {test();return 0;
}
打印效果如下:
const同时修饰指针星花和变量
格式:const int* const p
从上两点可以看出,此时 *p 和 p 都是只读的;
*p 只读示例如下:
p 只读示例如下: