> 文档中心 > 面试官居然让我写strlen函数?|详解字符串函数与内存函数【C语言/进阶】

面试官居然让我写strlen函数?|详解字符串函数与内存函数【C语言/进阶】

文章目录

  • 前言
  • 1. 字符串函数
    • 1.1 strlen
    • 1.2 strcpy
    • 1.3 strcat
    • 1.4 strcmp
    • 1.5 strnpy
    • 1.6 strncat
    • 1.7 strncmp
    • 1.8 strstr
    • 1.9 strtok
    • 1.9 strerror
    • 1.11 memcpy
    • 1.12 **memmove**
    • 1.13 memcmp
    • 1.14 memset
    • 2. 字符函数
  • 结语

前言

请原谅我的文章跟雅鲁藏布江一样长,但跟它一样,蕴藏着宝藏。

下面重点介绍处理字符和字符串的库函数的使用和注意事项

1. 字符串函数

注意:

  1. NULL是空指针,它是定义在stdio.h头文件中的宏,值为0
  2. NUL和null一般情况下指的是\0(图片里有出现,翻译为终止空字符)

1.1 strlen

size_t strlen ( const char * str );

在这里插入图片描述

要点

  1. 字符串已经’\0’ 作为结束标志,strlen函数返回的是在字符串中’\0’ 前面出现的字符个数(不包含’\0’ )

  2. 参数指向的字符串必须要以’\0’ 结束

    //strlen统计的是第一个'\0'前的元素的个数int main(){char a[] = "abc\0defg";printf("%d\n", strlen(a));return 0;}

在这里插入图片描述

  1. 注意函数的返回值数据类型为size_t,是unsigned int型。Why?长度不可能为负。在这里知道它是unsigned 型即可(64位可能为unsigned long int)。

    #include int main(){const char*str1 = "abcdef";const char*str2 = "abc";if(strlen(str2)-strlen(str1)>0){printf("str2>str1\n");}else{printf("srt1>str2\n");}return 0;}

    如果它的返回值不是unsigned int型,而是int型,结果是哪个呢?

  2. 模拟实现strlen函数

    1. 上面我们知道,strlen函数是统计第一个\0之前的元素个数,那么根据此原理,可以通过循环实现该功能。

      注:当我们不希望函数的参数即源字符串不被修改,需要用const修饰形参。assert(断言),作为初学者,我们应该使用它以避免可能发生传入空指针的情况。

      //1. 常规//2. 递归//不创建临时变量//3. 指针-指针#include#includesize_t my_strlen(const char* str){assert(str);//断言:提醒用户传参为非空指针//等价于assert(str != NULL);//NULL在stdio.h库中,它是一个宏,值为0int count = 0;//计数器while (*str)//'\0'的ASCII值为0{count++;str++;//指针后移}return count;}int main(){char a[] = "abcdefg";printf("%d\n", my_strlen(a));return 0;}
    2. 或许有一天,面试官会问你:那有没有一种方法,可以不通过创建临时变量得到字符串的长度呢?不通过临时变量,那就是只通过str这个指针变量自己运算,当达到某种条件,返回1/0,我们想到递归。

      #include#includesize_t my_strlen(const char* str){assert(str);//每当指针指向的不是\0,返回1+my_strlen(指向下一个),直到遇到\0,返回0if (*str){return 1+ my_strlen(++str);//注意是前置++哦}return 0;}int main(){char a[] = "abcdefg";printf("%d\n", my_strlen(a));return 0;}
    3. 学习指针的时候我们知道,指针-指针=两指针之间的元素个数。我们可以用让一个指针指向起始位置,然后让另一个指针移动到\0的位置,返回指针之差即为字符串长度

      注意:在保存初始位置时,变量start的类型要和形参一致,因为我们用const修饰变量,是为了更安全地使用它,假若将这个安全的变量交给一个不安全( 没有const修饰)的变量,它的内存访问权限就被放大了,也就是说它又不安全了(相当于形参的const白修饰了)。后面也有同样的例子。

      #include#includesize_t my_strlen(const char* str){assert(str);const char* start = str;//保存初始位置while (*str){str++;}return str - start;//返回元素个数}int main(){char a[] = "abcdefg";printf("%d\n", my_strlen(a));return 0;}

1.2 strcpy

char* strcpy(char * destination, const char * source );

在这里插入图片描述

要点

  1. 源字符串必须以’\0’ 结束

  2. 最后将源字符串中的’\0’ 拷贝到目标空间

  3. 目标空间必须足够大,以确保能存放源字符串。因为strcpy不会为程序员检查。

  4. 目标空间必须可修改。什么意思呢?形参中只有源字符串被const修饰,表示它不可修改;相反地,目标字符串不能被const修饰,表示它是将被修改的。

  5. 模拟实现strcpy函数

    //化简代码、链式访问、高质量C/C++编程#include#includechar* my_strcpy(char* dest, const char* src){char *ret = dest;//保存目标字符串的地址assert(dest );assert(src );while (*src){*dest = *src;dest++;src++;}//将src'\0'之前的元素赋给dest*dest = *src;//将src的'\0'赋给destreturn ret;//返回目标字符串的地址}int main(){char* str1 = "abcdef";char arr[20] ="XXXXXXXXXXXXX";printf("%s\n", my_strcpy(arr, str1));return 0;}
    1. 化简代码:在while循环中,我们可以将指针移动和赋值放在一个语句中

      while (*src){*dest++ = *src++;}*dest++ = *src++;//将src的'\0'赋给dest
    2. 括号内判断的是\0,而*dest++ = *src++这个赋值表达式的结果是被赋值的那个值,所以可以将循环外的语句放在括号里面。

      while (*dest++ = *src++)//这里最后已经将src的'\0'赋给dest了{;}//';' 表示这是一个空语句,它什么都不干,这是合法的

1.3 strcat

char * strcat ( char * destination, const char * source );

在这里插入图片描述

要点

  1. 源字符串必须以’\0’ 结束。

  2. 目标空间必须有足够的大,能容纳下源字符串的内容。

  3. 目标空间必须可修改。

    //用例如下int main(){char arr1[20] = "hello ";printf("%s\n", strcat(arr1, "world"));return 0;}//arr1的内容:"hello world\0"//实际上打印的结果为:hello world
  4. 模拟实现strcat函数

    思路:用一个指针移动到目标字符串的\0位置,然后以这个位置开始,将源字符串的内容追加,其实也就是strcpy的模拟实现。

    char* my_strcat(char* dest, const char* src){char* ret = dest;//保存目标字符串的地址assert(dest);assert(src);//1. 找目标字符串的'\0'while (*dest){dest++;}//2. 拷贝数据,同strcpywhile (*dest++ = *src++){;}return ret;}int main(){char arr1[20] = "hello ";printf("%s\n", my_strcat(arr1, "world"));return 0;}
  5. 字符串自己给自己追加,如何?

    int main(){char arr1[20] = "hello ";printf("%s\n", my_strcat(arr1, arr1));return 0;}//程序跑不起来,因为当源字符串copy到目标字符串时,\0总是被覆盖//以至于一直找不到\0,造成越界访问,程序崩溃

1.4 strcmp

int strcmp ( const char * str1, const char * str2 );

在这里插入图片描述

要点

  1. 标准规定(返回值):
    第一个字符串大于第二个字符串,则返回大于0的数字
    第一个字符串等于第二个字符串,则返回0
    第一个字符串小于第二个字符串,则返回小于0的数字

    注意:在VS编译器中,返回值分别是1、0、-1

  2. 那么如何判断两个字符串?

    //用例#includeint main(){char a[] = "abcde";char b[] = "abcdz";printf("%d\n", strcmp(a, b));return 0;}//结果为-1
  3. 模拟实现strcmp

    思路:将两个指针的值(字符的ASCII值)比较,如果相同就同时往前走,直到遇到\0为止两者都相等,返回0;如果一开始或中途就不相等,若两者ASCII差值为负数,返回-1,反之则返回1

    #include#includeint my_strcmp(const char* str1, const char* str2){//函数没有对两个字符串的内容修改,为保护两者在内存中的安全//都用const修饰assert(str1);assert(str2);while (*str1 == *str2){if (*str1 == '\0')//if语句在前在后都可以,//因为当指向最后一个元素时,后面是\0 while判断也能进来{return 0;}str1++;str2++;}//如果没有进入if语句则说明两者不相等//此时两个指针已经指向了不同的字符if (*str1 > *str2)return 1;elsereturn -1;}int main(){char a[] = "abcde";char b[] = "abcdz";printf("%d\n", my_strcmp(a, b));return 0;}//结果为-1

    这样的函数还是不够完美,因为返回值在不同编译器是不同的,将返回值改成大于零或小于零的值更有普适性,可以直接返回两者的差值。

    #include#includeint 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;}int main(){char a[] = "abcde";char b[] = "abcdz";printf("%d\n", my_strcmp(a, b));return 0;}//结果为-21

1.5 strnpy

char * strncpy ( char * destination, const char * source, size_t num );

在这里插入图片描述

它是strcpy函数的安全版本,因为strcpy不会替程序员检查目标字符串的空间是否足以提供源字符串复制,因此多了一个参数,复制字符的个数num。其实在了解它之后,会觉得其实它也不那么安全,num的主要作用我认为是提醒程序员在使用它时能注意这个问题。个人理解这个多出来的n可能是num的意思。

要点

  1. 拷贝num个字符从源字符串到目标空间。

  2. 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标字符串的后面追加0直到修改次数达到num为止。

    //示例#include#includeint main(){char a[] = "abcd";printf("%s\n", strncpy(a, "qwer",3));return 0;}//结果为qwed

    假若num的值大于要复制的源字符串的长度,剩余的空间

在这里插入图片描述

  1. 模拟实现strncpy

    思路:此函数有“复制不够0来凑”的功能,把num当作计数器,分情况决定要不要添0。除此之外,和strcpy的模拟实现相同。

    #include#includechar* my_strncpy(char* dest, const char* src, size_t num){char* ret = dest;//记录目标字符串地址assert(dest);assert(src);//先不管三七二十一,//两种情况可以先复制,然后通过num再看是否还有位置while (num-- && (*dest++ = *src++)){//1. num=len,直接将\0之前的字符复制到dest中,相当于strcpy;//2. num<len,直接将num个字符复制到dest中}//如果num>len,一定还有剩下的num个没有复制,//因为src没得复制了,所以要补0if (num){while(num--)*dest++ = '\0';}return ret;}int main(){char a[] = "abcdxxxxxxxxx";printf("%s\n", my_strncpy(a, "qwer",8));return 0;}

在这里插入图片描述

注意:第一个while循环中的num的左右位置(&&前面为假时,直接跳出循环)、以及是否在循环体内自减1、第二个while循环的`--`前置或后置都会对结果产生影响,需要根据实际情况进行匹配。

1.6 strncat

char * strncat ( char * destination, const char * source, size_t num );

在这里插入图片描述

要点

同strcat,只不过多了一个参数num。

用法

参照strncpy:

int main(){char a[20] = "abcd";printf("%s\n", strncat(a, "qwer", 3));return 0;}

在这里插入图片描述

int main(){char a[20] = "abcdxxxxxx\0xxxxxxx";printf("%s\n", strncat(a, "qwer", 6));return 0;}

在这里插入图片描述

至此我们可以了解它的原理:在目标字符串第一个\0处将源字符串的前num个元素copy并赋值,最后添加\0。

下面模拟实现strncat:

思路:用指针找到目标字符串\0的位置,然后将源字符串的前num个元素赋值,num当作计数器。

#include#includechar* my_strncat(char* dest, const char* src, size_t num){assert(dest);assert(src);char* ret = dest;while (*dest)dest++;//找到目标字符串\0的位置while (num--){*dest++ = *src++;}//最后已经将\0赋值return ret;}int main(){char a[20] = "abcdxxxxxx\0xxxxxxx";printf("%s\n", my_strncat(a, "qwer", 6));return 0;}

在这里插入图片描述

1.7 strncmp

int strncmp ( const char * str1, const char * str2, size_t num );

在这里插入图片描述

要点同strcmp,用法同上

用例

int main(){char arr1[] = "hello";printf("%d\n", strncmp(arr1, "helo", 3));return 0;}

在这里插入图片描述

int main(){char arr1[] = "hello";printf("%d\n", strncmp(arr1, "helo", 4));return 0;}

在这里插入图片描述

通过用例,我们可以知道它的原理。在前面几个模拟实现的例子的基础上,请读者自己思考是怎样实现的。

由于模拟实现strncmp更加麻烦,需要更多知识,作者目前还不具备这样的能力。但实现它的思想是不变的。这里附上VS编译器的参考代码。(路径:(VS所在的磁盘)E:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\crt\src)

int __cdecl strncmp(    const char *first,    const char *last,    size_t      count){    size_t x = 0;    if (!count)    { return 0;    }    /*     * This explicit guard needed to deal correctly with boundary     * cases: strings shorter than 4 bytes and strings longer than     * UINT_MAX-4 bytes .     */    if( count >= 4 )    { /* unroll by four */ for (; x < count-4; x+=4) {     first+=4;     last +=4;     if (*(first-4) == 0 || *(first-4) != *(last-4))     {  return(*(unsigned char *)(first-4) - *(unsigned char *)(last-4));     }     if (*(first-3) == 0 || *(first-3) != *(last-3))     {  return(*(unsigned char *)(first-3) - *(unsigned char *)(last-3));     }     if (*(first-2) == 0 || *(first-2) != *(last-2))     {  return(*(unsigned char *)(first-2) - *(unsigned char *)(last-2));     }     if (*(first-1) == 0 || *(first-1) != *(last-1))     {  return(*(unsigned char *)(first-1) - *(unsigned char *)(last-1));     } }    }    /* residual loop */    for (; x < count; x++)    { if (*first == 0 || *first != *last) {     return(*(unsigned char *)first - *(unsigned char *)last); } first+=1; last+=1;    }    return 0;}

1.8 strstr

char * strstr ( const char *str1, const char * str2);

在这里插入图片描述

用例在这里插入图片描述

#includeint main(){char arr[] = "abcdefabcdef";char* ret = strstr(arr, "cd");if (ret != NULL){printf("%s\n", ret);}return 0;}
#includeint main(){char arr[] = "abcdefabcdef";char* ret = strstr(arr, "zz");printf("%s\n", ret);return 0;}

在这里插入图片描述

由用例可知:如果找到子字符串,则返回第一个子字符串出现的起始位置,否则返回空指针。

模拟实现strstr

思路:两个指针ab分别维护两个字符串,以要找的字符串find为准,从开始往后比较,如果相等,则继续,否则指针a往后走一步,指针b则回到字符串find的起始位置,重复上述操作。直到指针b指向\0为止,在指针a指向\0时前,字符串find的所有元素在另一个字符串中都能对应,则找到子字符串。否则没找到,返回空指针。

#include#includechar* my_strstr(const char* str1, const char* str2){assert(str1 && str2);const char* s1 = str1;const char* s2 = str2;const char* cur = str1;//记录失败位置的指针while (*cur)//以主字符串的失败位置指针为准{s1 = cur;s2 = str2;//如果配对失败了,重置s1,s2指针while (*s1 && *s2 && (*s1 == *s2))//配对成功,指针同时往后走一步{s1++;s2++;}cur++;//失败了,cur往后走一步,以便重置if (*s2 == '\0'){return (char*)cur;//返回值从const char*强转回char*}}return NULL;}int main(){char arr1[] = "abbbcdef";char arr2[] = "bbc";char* ret = my_strstr(arr1, arr2);if (NULL == ret){printf("找不到子串\n");}else{printf("%s\n", ret);}return 0;}

1.9 strtok

char * strtok ( char * str, const char * sep );

在这里插入图片描述

要点

  1. sep参数是个字符串,定义了用作分隔符的字符集合
  2. 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
  3. strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
  4. strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
  5. strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
  6. 如果字符串中不存在更多的标记,则返回 NULL 指针。

用途

诸如198.162.1.1、123456@qq.com含有除了数字和字母之外的字符这类字符串,以这些字符为分隔标志,将它们分为:198 162 1 1、123456 qq com若干个子字符串。

在这里插入图片描述

用例

int main(){char arr1[] = "123456@qq.com";char tmp[30] = { 0 };strcpy(tmp, arr1);//临时拷贝一份char arr2[] = "@.";//将源字符串中出现的字符放入数组中,顺序任意char* p = NULL;//用p接收函数返回的标志的地址p = strtok(tmp, arr2);printf("%s\n", p);p = strtok(NULL, arr2);printf("%s\n", p);p = strtok(NULL, arr2);printf("%s\n", p);return 0;}

在这里插入图片描述

在这里,因为第一条语句只用执行一次,而后面的调用函数传参在形式上都是相同的,所以我们可以用for语句化简代码。

for (p = strtok(tmp, arr2); p != NULL; p = strtok(NULL, arr2)){printf("%s\n", p);}

1.9 strerror

char * strerror ( int errnum );

功能:返回错误码,所对应的错误信息。也就是根据错误的类型,返回一段含有错误信息的文字。

用例

#include #include #include //对应的头文件int main (){FILE * pFile;pFile = fopen ("unexist.ent","r");//这是个文件并不存在//知识点:文件操作if (pFile == NULL)printf ("Error opening file unexist.ent: %s\n",strerror(errno));//errno: Last error numberreturn 0;}//返回信息:不存在文件或库

在这里插入图片描述

1.11 memcpy

void * memcpy ( void * destination, const void * source, size_t num );

在这里插入图片描述

它存在的意义

strcpy或strncpy函数只能对字符串进行操作,也就是char型,而内存中的数据不止char型,所以需要用一个“万能”的拷贝函数实现各种数据之间的拷贝。这便是它的参数和返回值类型为void*型的原因。

要点

  1. 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
  2. 这个函数在遇到’\0’ 的时候并不会停下来,和\0无关,所以目标内存中无\0无所谓。
  3. 如果source和destination有任何的重叠,复制的结果都是未定义的。

用例

#include int main(){int a[] = { 1,2,3,4,5,6,7,8,9,10 };int b[15] = { 0 };memcpy(b, a, 12);//将a的前12个字节的数据拷贝到b中//3个int型元素return 0;}

在这里插入图片描述

模拟实现memcpy

思路:这里的memcpy和strcpy十分类似,只是处理的数据不同,以及没有\0作为终止的条件,但是思路是一致的。这里的思路在模拟实现qsort中的交换函数部分一致,当复制数据时,是以一个字节为一个单位复制呢还是以4个(int)字节或5个字节为一个单位复制?仔细想想,假若需要复制的数据占15个字节,最快的办法当然是以15个字节为单位复制,接着是5,然后是三。但下次是18呢?所以具有普适性的方法应该是以一个字节为单位复制。

#includevoid* my_memcpy(void* dest, const void* src, size_t num){assert(dest && src);void* ret = dest;while (num--){*(char*)dest = *((char*)src);dest = (char*)dest + 1;src = (char*)src + 1;//等价于//*((char*)dest)++ = *((char*)src)++;}return ret;}int main(){int a[] = { 1,2,3,4,5,6,7,8,9,10 };int b[15] = { 0 };my_memcpy(b, a, 12);//3个int型元素return 0;}

在这里插入图片描述

注意:

在函数中,指针变量dest和src必须强转为(char*)型才能进行+1操作

用例

假设将数组a的1234,复制到a+2开始的16个字节的位置上理想结果应该是121234
因为memcpy的缺陷,不能复制数据有重叠部分的内存块(不论大小端)结果都将会是121212

#include#includevoid* my_memcpy(void*dest, const void*src, size_t num){void* ret = dest;assert(dest);assert(src);while (num--){*(char*)dest = *(char*)src;dest = (char*)dest+1;src = (char*)src+1;}return ret;}int main(){int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };my_memcpy(a + 2, a, 16);return 0;}

在这里插入图片描述

1.12 memmove

void * memmove ( void * destination, const void * source, size_t num );

在这里插入图片描述

要点

  1. 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。所以此函数是memcpy的优化函数。
  2. 如果源空间和目标空间出现重叠,就得使用memmove函数处理。

用例

#includeint main(){int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };memmove(a + 2, a, 16);return 0;}

模拟实现memmove

思路:如图只分析了其中一种情况,还有两种情况分别是与之相反的情况和两个内存块完全重叠的情况(将蓝色框看成可移动的)。

在这里插入图片描述

#include#includevoid* my_memmove(void* dest, const void* src, size_t num){assert(dest && src);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);}}}int main(){int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };my_memmove(a + 2, a, 16);return 0;}

在这里插入图片描述

1.13 memcmp

int memcmp ( const void * ptr1, const void * ptr2, size_t num );

在这里插入图片描述

要点

它与strlen的不同点在于它不由\0决定程序是否终止,而由计数器num控制。

用例

#include #include int main(){char a[] = "abcdefg";char b[] = "bcdefg";printf("%d\n", memcmp(a, b, 24));}

在这里插入图片描述

模拟实现memcmp

思路同strcmp,只不过程序是否终止由字节计数器决定。由于涉及到其他知识,在此给出核心代码。

while ( --count && *(char *)buf1 == *(char *)buf2 ) {     buf1 = (char *)buf1 + 1;     buf2 = (char *)buf2 + 1;}return( *((unsigned char *)buf1) - *((unsigned char *)buf2) );

1.14 memset

void * memset ( void * ptr, int value, size_t num );

在这里插入图片描述

要点

要注意它是设置内存块的前num个字节,是以字节为单位的。

用例

int main(){int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };memset(a, 0, 17);return 0;}

在这里插入图片描述

在这里插入图片描述

我们将0改成1

int main(){int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };memset(a, 1, 17);return 0;}

在这里插入图片描述

所以memset的功能单一,个人觉得它的用处不是很广泛,通常用在将内存数据归零的情况下,且一般是0而不是其它。

模拟实现memset

有了前面的铺垫,要实现memset并不难,无非是强转+循环。由于涉及到其他知识,这里只给出核心代码,理解原理即可。

while (count--){    *(char*)dst = (char)val;    dst = (char*)dst + 1;}return(start);}

2. 字符函数

头文件 ctype.h
函数 如果参数符合下列条件则返回真值
iscntrl 任何控制字符
isspace 空白字符:空格‘ ’,换页‘\f’,换行’\n’,回车‘\r’,制表符’\t’或者垂直制表符’\v’
isdigit 十进制数字 0~9
isxdigit 十六进制数字,包括所有十进制数字,小写字母af,大写字母AF
islower 小写字母a~z
isupper 大写字母A~Z
isalpha 字母az或AZ
isalnum 字母或者数字,az,AZ,0~9
ispunct 标点符号,任何不属于数字或者字母的图形字符(可打印)
isgraph 任何图形字符
isprint 任何可打印字符,包括图形字符和空白字符
tolower 转换为小写字母
toupper 转换为大写字母

这些函数功能单一,但十分实用,利用得当能提高程序的效率。例如判断字母大小写、判断是否为字母、大小写转换等函数。

用例

#include #include int main (){int i=0;char str[]="Test String.\n";char c;while (str[i]){c=str[i];if (isupper(c))c=tolower(c);putchar (c);i++;}return 0;}

在这里插入图片描述

结语

至此,若读者在认真阅读时,并自己动手实现它们,会发现其实它们并不难。而要巧妙高效地使用它们,最好了解它们地工作原理,以避免不必要的错误。
欢迎读者指正,请原谅我的文章是那么的平淡无奇,且长。
如果你有收获的话,请给作者一个鼓励吧~