目录
一、strlen函数
1.strlen函数的使用
size_t strlen ( const char * str );
- 字符串以 '\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前面出现的字符个数(不包 含 '\0' )。
- 参数指向的字符串必须要以 '\0' 结束。
- 注意函数的返回值为size_t,是无符号的( 易错 )
- strlen函数使用时需要包含头文件 <string.h>
下面给大家展示一下最简单的strlen函数的实现
//strlen函数的使用
#include<string.h>
int main()
{
char arr[] = "abcdef";
size_t len = strlen(arr);
printf("%zu", len);
return 0;
}
解释:arr数组里存放的是字符串“abcdef\0”,strlen计算的是‘\0’之前的字符的个数,所以strlen计算出来的结果就是6.
2.⚠strlen函数使用时的注意事项
1.返回值类型:size_t
size_t是无符号整型,这里要注意的是不能使用%d来打印strlen返回值,否则可能报警告
我们可以看到如果我们这里使用%d来打印它就报警告显示size_t转换到int可能丢失数据
2.strlen与sizeof的比较
函数 作用 示例 strlen 计算‘\0’之前字符的个数(不包含‘\0’) strlen(“abcde”)
输出结果:5
sizeof 计算数组(字节)/字符串个数(包含‘\0’) sizeof(“abcde”)
输出结果:6
strlen函数使用时的误区:
这里给大家一段代码,大家思考一下它的输出结果是什么?
//strlen函数使用时的误区
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "abcd";
char arr2[] = "abcdef";
if (strlen(arr1) - strlen(arr2) > 0)
printf("arr1>arr2\n");
else
printf("arr1<arr2\n");
return 0;
}
这里我们明显看到arr1数组的元素个数要比arr2数组的元素个数要少,那么打印结果就是arr1<arr2咯?实践出真知,我们运行来看一下
这与我们分析的结果恰恰相反,可这是为什么呢?大家可以回归到上面的strlen函数使用时的注意事项,strlen函数的返回值类型是size_t,是一个无符号整型,那这里strlen(arr1)-strlen(arr2)可能是一个负数吗?很显然是不可能的,这里给大家解释一下:
按照正常逻辑 strlen(arr1)-strlen(arr2)计算出的结果是-2,而strlen的返回值类型是无符号整型,那么这里的-2要被当作无符号整型来处理,那怎么处理呢?
-2的二进制原码:10000000 00000000 00000000 00000010
-2的二进制反码:111111111 111111111 111111111 111111101
-2的二进制补码:111111111 111111111 111111111 111111110
这里编译器要把-2当作无符号整型时,就会把-2的二级制的补码当作它的原码,所以这里strlen(arr1)-strlen(arr2)的二进制值为111111111 111111111 111111111 111111110可见是一个非常大的数,所以这里打印出来的结果恰恰与我们分析的相反
那如果我们非要通过作差来写这个代码呢?我们应该如何去写?
//改善后
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "abcd";
char arr2[] = "abcdef";
if ((int)strlen(arr1) - (int)strlen(arr2) > 0)
printf("arr1>arr2\n");
else
printf("arr1<arr2\n");
return 0;
}
这里我就是通过强制类型转换来实现的,我们把size_t强制转化为int类型,而int是有符号整型,这里就不会出现上面一样的错误了,但是这里还是要强调一下,我们尽量不要去强制类型转化,因为又是强制类型转化可能导致数据的丢失。
3.strlen函数的模拟实现
方法一:计数器
//strlen函数的模拟实现
//方法一:计数器
//int my_strlen(const char*str)
//{
// int count = 0;
// while (*str)
// {
// count++;
// str++;
// }
// return count;
//}
//int main()
//{
// char str[] = "abcdef";
// int r = my_strlen(str);
// printf("%d", r);
// return 0;
//}
方法二:指针-指针
//方法二:指针-指针
//int my_strlen(char* str)
//{
// assert(str);
// char* p = str;
// while (*p != '\0')
// {
// p++;
// }
// return p - str;
//}
//int main()
//{
// char str[] = "abcdef";
// int r = my_strlen(str);
// printf("%d", r);
// return 0;
//}
方法三:递归(不创建临时变量的前提下):
//方法四:递归
//不能在函数内部创建临时变量,求字符串的长度
//size_t my_strlen(const char* str)
//{
// assert(str);
// if (*str != '\0')
// return 1 + my_strlen(str + 1);
// else
// return 0;
//}
//int main()
//{
// char str[] = "abcdef";
// size_t r = my_strlen(str);
// printf("%zu", r);
// return 0;
//}
二、strcpy函数
1.strcpy函数的使用
char* strcpy(char * destination, const char * source );
- 源字符串必须以 '\0' 结束。
- 会将源字符串中的 '\0' 拷贝到目标空间。
- 目标空间必须足够大,以确保能存放源字符串。
- 目标空间必须可修改。
- strcpy函数使用时需要包含头文件<string.h>
参数说明: char * destination是目标地址,const char * source是源头地址,strcpy函数的返回值类型是一个地址(指针)
下面给大家展示一下最简单的strcpy函数的实现
//strcpy
#include<string.h>
int main()
{
char arr1[] = "abcdef";
char arr2[20] = { 0 };
strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
解释:strcpy(arr2,arr1)就是要把arr1数组里面的内容(包含‘\0’)拷贝到arr2里面.
2.⚠strcpy函数使用时的注意事项
1.源字符串里必须以‘\0’结尾:
如果源头数组里都没有‘\0’,那么到底要拷贝多少元素到数组arr2里面呢?
2.源字符串中的‘\0’也会拷贝到arr2数组里。
3.目标空间足够大:
目标空间必须足够大,以确保能存放源字符串。
4.目标空间必须可修改:
目标空间肯定是会被修改的,因为要把arr1数组里的内容拷贝进去,所以不能用const修饰arr2,但是可以用const来修饰arr1,来确保arr1数组里的内容不被修改。
3.strcpy函数的模拟实现
#include<stdio.h>
#include<assert.h>
#include<string.h>
char* my_strcpy(char* des, const char* src)
{
char* ret = des;
assert(des && src);
while (*des++=*src++)
{
;
}
return ret;
}
int main()
{
char arr1[] = "abcdef";
char arr2[20] = "xxxxxxxxx";
char*r=my_strcpy(arr2, arr1);
printf("%s\n", r);
//printf("%s\n", arr2);
return 0;
}
补充:assert断言,它的作用是用来检测是否有误,如果有错误,就会报错,并且会把错误的地方和原因呈现出来,如果没有错误,则这句语句就跳过。assert需要包含头文件<assert.h>
三、strcat函数
1.strcat函数的使用
char* strcat(char * destination, const char * source );
- 源字符串必须以 '\0' 结束。
- 目标字符串中也得有 \0 ,否则没办法知道追加从哪里开始。
- 目标空间必须有足够的大,能容纳下源字符串的内容。
- 目标空间必须可修改。
- strcat函数使用时需要包含头文件<string.h>
参数说明: char * destination是目标地址,const char * source是源头地址,strcat函数的返回值类型是一个地址(指针)
下面给大家展示一下最简单的strcat函数的实现
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = "Hello";
char arr2[] = "world";
char*r=strcat(arr1, arr2);
printf("%s\n", arr1);
printf("%s\n", r);
return 0;
}
2.⚠strcat函数使用时的注意事项
1.源字符串必须以 '\0' 结束:不然拼接接到哪里为止不知道
2.目标字符串中也得有 \0:否则没办法知道追加从哪里开始拼接
3.目标空间必须有足够的大:这样目标字符串才能容得下拼接过后两个字符串长度的和
4.目标字符串必须为可修改的:这样strcat才有效果,源字符串可以添加const修饰
#include<stdio.h>
#include<string.h>
#include<assert.h>
char* mystrcat(char* arr1, const char* arr2)
{
assert(arr1 && arr2);
char* p = arr1;
while (*arr1)
{
arr1++;
}
while ((*arr1++ = *arr2++))
{
;
}
return p;
}
int main()
{
char arr1[20] = "hello";
char arr2[] = "world";
char* ret = mystrcat(arr1, arr2);
printf("%s", ret);
return 0;
}
3.strcat函数的模拟实现
#include<stdio.h>
#include<assert.h>
char* mystrcat(char* arr1, const char* arr2)
{
assert(arr1 && arr2);
char* p = arr1;
while (*arr1)
{
arr1++;
}//找到目标字符串中的\0,明确从那里开始追加。
while ((*arr1++ = *arr2++))
{
;
}
return p;
}
int main()
{
char arr1[20] = "hello";
char arr2[] = "world";
char* ret = mystrcat(arr1, arr2);
printf("%s", ret);
return 0;
}
四、strcmp函数
1.strcmp函数的使用
int strcmp(const char*str1,const char*str2);
参数说明和标准规定以及功能
const char*str1代表第一个字符串
const char*str2代表第二个字符串
返回值及规定:
返回值为int(整型)
如果第一个字符串大于第二个字符串,则返回值为大于0的数字
如果第一个字符串小于第二个字符串,则返回值为小于0的数字
如果第一个字符串等于第二个字符串,则返回值为等于0的数字
功能:用来比较str1和str2指向的字符串,从两个字符串的第一个字符开始比较,如果两个字符串的ASCALL码值相等,就继续比较下一个字符,直到遇到不相等的两个字符,或者字符串结束。
下面给大家展示一下最简单的strcmp函数的实现:
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abq";
int r = strcmp(arr1, arr2);
if (r > 0)
printf("arr1>arr2\n");
else if(r<0)
printf("arr1<arr2\n");
else
printf("arr1=arr2\n");
return 0;
}
2.strcmp函数的模拟实现
#include<assert.h>
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 && str2);
while (*str1 == *str2)
{
if (*str1 == '\0')
return 0;
str1++;
str2++;
}
if (*str1 > *str2)
return 1;
else
return -1;
}
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abc";
int r = my_strcmp(arr1, arr2);
if (r > 0)
printf("arr1>arr2\n");
else if (r < 0)
printf("arr1<arr2\n");
else
printf("arr1=arr2\n");
return 0;
}
五、strncpy函数
1.strncpy函数的使用
char * strncpy ( char * destination, const char * source, size_t num );
我们观察发现strncpy函数与strcpy 函数的返回值一模一样,但是strncpy函数的函数参数比strcpy 函数参数多了一个,那这个参数的意义是什么呢?
- 拷贝num个字符从源字符串到目标空间,最多拷num个。
- 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
这下大家应该对num参数的意义有了一定的了解了吧,那接下来我给大家展示一下最简单的strncpy函数的实现:
//strncpy
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "abcdefg";
char arr2[20] = { 0 };
strncpy(arr2, arr1, 5);
printf("%s", arr2);
return 0;
}
那我们再思考一下如果拷贝时源字符串个数不够的话会怎么样呢?我们来做一个简单的测试
//strncpy
int main()
{
char arr1[] = "abc";
char arr2[] = "xxxxxxxxxxxxxxx";
strncpy(arr2, arr1, 5);
printf("%s", arr2);
return 0;
}
我们打开监视窗口发现当源字符串不够5个元素时,这里他就会自动把‘\0’补充直到5个为止,那如果提前遇到‘\0’又会是怎样的?我们再来做个简单的测试,监视它看一下结果:
//strncpy
int main()
{
char arr1[] = "abc\0def";
char arr2[] = "xxxxxxxxxxxxxxx";
strncpy(arr2, arr1, 5);
printf("%s", arr2);
return 0;
}
当提前遇到‘\0’时,strcpy就会当作源字符串已经结束了,就不会再拷贝'\0'后面的元素了,并且,不够的用'\0'来补充。
2.strcpy函数与strncpy函数比较
- strcpy函数拷贝到'\0'为止,如果目标空间不够的话,容易出现越界行为
- strncpy指定了拷贝元素的个数,源字符串不一定要有'\0',同时在设计参数的时候就会多一层思考:目标空间的大小是否够用。
总结: strncpy相对于strcpy函数更加安全
3.strncpy函数的模拟实现
#include<stdio.h>
#include<string.h>
#include<assert.h>
char* my_strncpy(char* arr1, const char* arr2, size_t num)
{
assert(arr1 && arr2);
char* p = arr1;
int i = 0;
for (i = 0;i < num && arr2[i];i++)
{
arr1[i] = arr2[i];
}
if (i < num)
{
arr1[i] = 0;
}
return p;
}
int main()
{
char arr1[20] = { 0 };
char arr2[] = "abcdefg";
char* ret = my_strncpy(arr1, arr2, 3);
printf("%s", ret);
return 0;
}
六、strncat函数
1.strncat函数的使用
char * strncat ( char * destination, const char * source, size_t num );
我们这里通过比较strncat函数发现strncat函数又只是比strcat多了一个参数,那我们通过类比猜测这个参数是不是也是限制它所要拼接的长度呢?
- 将source指向字符串的前num个字符追加到destination指向的字符串末尾,再追加一个 \0 字 符
- 如果source指向的字符串的长度小于num的时候,只会将字符串中到 \0 的内容追加到destination指向的字符串末尾
- 目标空间大小必须足够大
我们来看一个最简单的例子:
//strncat
int main()
{
char arr1[] = "abcdefgh";
char arr2[30] = "xxxxx";
strncat(arr2, arr1, 3);
printf("%s", arr2);
return 0;
}
这是时候我们思考一下,源头的字符串中的'\0'会被拼接过来吗?这时候我们做个简单的测试来看一下:
//strncat
int main()
{
char arr1[] = "abc";
char arr2[30] = "xxxxx\0xxxxxxx";
strncat(arr2, arr1, 3);
printf("%s", arr2);
return 0;
}
通过监视窗口,我们发现源字符串中的‘\0’确实被拷贝过来了,但是如果我们拼接两个字符的话,那‘\0’还会被拼接过来了吗?
//strncat
int main()
{
char arr1[] = "abc";
char arr2[30] = "xxxxx\0xxxxxxx";
strncat(arr2, arr1, 2);
printf("%s", arr2);
return 0;
}
通过监视窗口,我们发现当num的个数少于源头字符串元素个数时'\0'还是会被拼接过来。
那如果num>源字符串的个数时,会补更多的‘\0’吗?我们再来举个简单的例子:
//strncat
int main()
{
char arr1[] = "abc";
char arr2[30] = "xxxxx\0xxxxxxx";
strncat(arr2, arr1, 5);
printf("%s", arr2);
return 0;
}
我们通过监视窗口可以看到, 当num>源字符串的个数时,它并不会补两个‘\0’,那么我们总结一下吧。
2.strncat函数与strcat函数比较
- 参数不同,strnact多了一个参数,这个参数用来限制拼接的元素个数
- 相对strcat函数而言,strnact更加灵活,也更加安全
3.strncat函数使用时的注意事项
总结:
- 当num<源字符串个数时,末尾都会拼接个‘\0’
- 当目标字符串中间有‘\0’,那么strncat就会从中间遇到的那个‘\0’开始拼接
- 当num>源字符串的个数时,它并不会补剩下空余的‘\0’,只会补一个‘\0’来结尾
4. strncat函数的模拟实现
#include<stdio.h>
#include<assert.h>
char* mystrncat(char* arr1, const char*arr2 , size_t num)
{
assert(arr1 && arr2);
char* p = arr1;
while (*arr1)
{
arr1++;
}
for (int i = 0;arr2[i] && i < num;i++)
{
arr1[i] = arr2[i];
}
if (num < 0)
{
arr1[i] = 0;
}
return p;
}
int main()
{
char arr1[20] = "hello";
char arr2[] = "world";
char* ret = mystrncat(arr1, arr2,3);
printf("%s", ret);
return 0;
}
七、strncmp函数
1.strncmp函数的使用
int strncmp ( const char * str1, const char * str2, size_t num );
我们通过类比strncmp函数,发现strncmp函数返回值类型与strcmp函数返回值类型相同,但是strncmp函数的参数比strcmp函数参数多一个,我们类比上面两个函数,就可以知道这个num参数的作用就是限制字符串的个数
作用:比较str1和str2的前num个字符,如果相等就继续往后比较,最多比较num个字母,如果提前发现不一样,就提前结束,大的字符所在的字符串大于另外一个。如果num个字符都相等,就是相等返回0.
规则:
如果第一个字符串大于第二个字符串,则返回值为大于0的数字
如果第一个字符串小于第二个字符串,则返回值为小于0的数字
如果第一个字符串等于第二个字符串,则返回值为等于0的数字
给大家展示一下最简单的strncmp函数的实现:
//strncmp
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "abcdefgh";
char arr2[] = "abcdew";
int r = strncmp(arr2, arr1, 6);
if (r > 0)
printf(">");
else if(r<0)
printf("<");
else
printf("=");
return 0;
}
2.strncmp函数与strcmp函数比较
- 参数不同,可以比较任意长度了
- 相对于strcmp函数,strncmp函数更加灵活,更加安全
八、strstr函数
1.strstr函数的使用
char * strstr ( const char * str1, const char * str2);
功能:strstr函数,查找str2指向的字符串在str1指针的字符串中第一次出现的位置。如果找到返回地址,找不到返回NULL
参数:str1指针,指向的被查找的字符串;str2指针,指向了要查找的字符串。
返回值:
- 如果str1指向的字符串中存在str2指向的字符串,那么返回第一次出现位置的指针
- 如果str1指向的字符串中不存在str2指向的字符串,那么返回NULL
strstr函数最简单的实现:
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "heheabcdefabcedf";
char arr2[] = "def";
char* p = strstr(arr1, arr2);
if (p != NULL)
printf("找到了,%s\n", p);
else
printf("没找到\n");
return 0;
}
2.strstr函数的模拟实现
#include<stdio.h>
#include<string.h>
char* my_strstr(const char* str1, const char* str2)
{
const char* p = str1;
const char* s1 = NULL;
const char* s2 = NULL;
if (*str2 == '\0')
return (char* )str1;
while (*p)
{
s1 = p;
s2 = str2;
while (*s1 && *s1 && *s1 == *s2)
{
s1++;
s2++;
}
if (*s2 == '\0')
return (char*)p;
p++;
}
return NULL;
}
int main()
{
char arr1[] = "heheabcdefabcedf";
char arr2[] = "def";
char* p = my_strstr(arr1, arr2);
if (p != NULL)
printf("找到了,%s\n", p);
else
printf("没找到\n");
return 0;
}
九、strtok函数
1.strtok函数的使用
char * strtok ( char * str, const char * sep);
参数说明:
- str:首次调用时传入带分割的字符串;后续调用传入NULL,表示继续分割同一个字符串。
- sep:包含所有分隔符的字符串(例如:char sep[]="@*%",@ * %分别为3个分隔符符)
功能:
- 分割str字符串:根据sep字符串中特定的分隔符,分割str字符串为多个子字符串
- 修改str字符串:strtok会直接在原始字符串中插入'\0'终止符,替换分隔符的位置,原始字符串就会被修改为多个子字符串。
使用规则:
- 首次调用:传入待分割字符串和分隔符
- 后续调用:传入NULL和相同的分隔符,继续分割
- 结束条件:当返回NULL时,表示分割完成
strtok函数的最简单实现:
//strtok-->分割
#include<stdio.h>
#include<string.h>
int main()
{
char arr[] = "asdfghjkl@qq.com";
char del[] = "@.";
char buf[20] = { 0 };
strcpy(buf, arr);
char* p = strtok(buf, del);
printf("%s\n", p); //asdfghjkl
p = strtok(NULL, del);
printf("%s\n", p); //qq
p = strtok(NULL, del);
printf("%s\n", p); //com
p = strtok(NULL, del);
printf("%s\n", p); //(null)
return 0;
}
我们如果这样写代码的话,如果字符串中有1000个分隔符,那我们是不是就要写1000行这种代码?是不是有点太“傻”了,那怎么样改进一下这个代码呢?
2.strtok函数的改进
//高级版
#include<stdio.h>
#include<string.h>
int main()
{
char arr[] = "asdfghjkl@qq.com";
char del[] = "@.";
char buf[20] = { 0 };
strcpy(buf, arr);
char* p = NULL;
for (p = strtok(buf, del); p != NULL; p = strtok(NULL, del))
{
printf("%s\n", p);
}
return 0;
}
大家看打印结果是不是一模一样的啊?
3.⚠strtok函数使用时的注意事项
- 在分割字符串的时候,会把字符串破坏,如果有的题目需要保留原字符串的话,我们就可以先用strcpy函数拷贝一份原字符串,在拷贝的那一份字符串中进行分割,这样更安全。
- 当字符串中出现多个连在一起的分割符,系统会把这多个分隔符视为一个分隔符进行分割
十、strerror函数
1.strerror函数的使用
char* strerror ( int errnum );
解释:strerror函数可以把参数部分错误码对应的错误信息的字符串地址返回来。在不同的系统和C语言标准库的实现中都规定了⼀些错误码,⼀般是放在 <errno.h> 这个头⽂件中说明的,C语言程序启动的时候就会使用一个全局的变量errno来记录程序的当前错误码,只不过程序启动的时候errno是0,表示没有错误,当我们在使⽤标准库中的函数的时候发⽣了某种错误,就会将对应 的错误码,存放在errno中,而⼀个错误码的数字是整数很难理解是什么意思,所以每⼀个错误码都 是有对应的错误信息的。strerror函数就可以将错误对应的错误信息字符串的地址返回。
参数: 这个错误码一般传递的是error这个变量的值,在C语言中有一个全局变量叫:errno,当库函数的调用发生错误的时候,就会将本次错误的错误码存放在errno这个变量中。
返回值:函数返回通过错误码得到的错误信息字符串的首字符的地址。
#include <errno.h>
#include <string.h>
#include <stdio.h>
int main()
{
int i = 0;
for (i = 0; i <= 10; i++) {
printf("%s\n", strerror(i));
}
return 0;
}
在Windows11+VS2022环境下输出的结果如下:
0:No error
1:Operation not permitted
2:No such file or directory
3:No such process
4:Interrupted function call
5:Input/output error
6:No such device or address
7:Arg list too long
8:Exec format error
9:Bad file descriptor
strerror也可以打开文件,如果文件不存在的话就会显示错误信息:
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
//C语言可以打开文件
//fopen
//如果以已读的形式打开文件,文件是必须要存在的,如果文件不存在,则打开文件失败
//fopen函数就会把错误码放在errno里
//同时函数会返回NULL
FILE* p = fopen("尺寸换米.exe", "r");
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//读文件
//关闭文件
fclose(p);
p = NULL;
return 0;
}
这里就可以看到,当文件不存在的时候,编译器就会输出 No such file or directory表示文件不存在
2.附加:perror函数
也可以了解⼀下perror函数,perror函数相当于⼀次将上述代码中的第9行完成了,直接将错误信息 打印出来。perror函数打印完参数部分的字符串后,再打印一个冒号和一个空格,再打印错误信息。
//perror
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
//C语言可以打开文件
//fopen
//如果以已读的形式打开文件,文件是必须要存在的,如果文件不存在,则打开文件失败
//fopen函数就会把错误码放在errno里
//同时函数会返回NULL
FILE* p = fopen("尺寸换米.exe", "r");
if (p == NULL)
{
perror("exe");
//exe:错误信息
}
//读文件
//关闭文件
fclose(p);
p = NULL;
return 0;
}
最终的输出结果和桑面的其实是一样的
总结:到这里我给大家总结了10个最重要的字符串函数包括:strlen函数以及模拟实现、strcpy函数以及模拟实现、strcat函数以及模拟实现、strncpy函数以及模拟实现、strncat函数以及模拟实现、strncmp函数、strstr函数以及模拟实现、strtok函数以及改进、strerror函数,相信大家看了这篇文章能够有所收获,后续我还会分享更多的知识点,也欢迎大家能够进入我的主页浏览我写的文章,也欢迎在评论区留下你的问题,如果这篇文章对你有帮助的话,请给这篇文章点赞、收藏加关注,文章制作不易,谢谢大家的支持🌹🌹🌹