字符和字符串函数的这些知识你知道吗?超硬核3w字文章带你领略C语言字符与字符串的美!!!


字符函数和字符串函数


我们这篇文章重点介绍以下内容:

  • 求字符串长度

    • strlen
  • 长度不受限制的字符串函数

    • strcpy
    • strcat
    • strcmp
  • 长度受限制的字符串函数介绍

    • strncpy
    • strncat
    • strncmp
  • 字符串查找

    • strstr
    • strtok
  • 错误信息报告

    • strerror
    • perror
  • 字符操作

  • 内存操作函数

    • memcpy
    • memmove
    • memset
    • memcmp

字符串基础知识

  • 字符串就是一串或多个字符,并且以一个位模式全0的NULL字节结尾。因此我们字符串所包含的字符内部不能出现NULL字节,这个限制很少会引起问题,因为NULL字节并不存在与它相关联的可打印字符,这也是它被选为终止符的原因。NULL字节是字符串的终止符,但它本身并不是字符串的一部分,所以字符串的长度并不句括NULL字节,头文件 string.h 包含了使用字符串函数所需的原型和声明。尽管并非必需,但在程序中包含这个头文件确实是个好主意,因为有了它所包含的原型,编译器可以更好地为你的程序执行错误检查。

函数介绍


长度不受限制的字符串函数

strlen(字符串长度)
  • 库函数strlen函数的原型如下:
size_t strlen(const char*string);

image-20210606192935816

  • 字符串结束标志为’\0’,strlen函数返回的是在结束标志之前的字符个数。
#include<stdio.h>
#include<string.h>
int main()
{
    char arr[]={"hello"};
    int len = strlen(arr);
    printf("%d\n",len);
    return 0;
}

image-20210606154349546

char arr[]={"hello"};

解释:

我们在这样初始化时,后面是有\0的。strlen返回的是字符串个数,所以是5

  • 参数指向的字符串必须以’\0’结束,不然得出的结果是随机值。
#include<stdio.h>
#include<string.h>
int main()
{
    char arr[]={'h','e','l','l','o'};
    int len = strlen(arr);
    printf("%d\n",len);
    return 0;
}

image-20210606154641540

char arr[]={'h','e','l','l','o'};

解释:

我们这样初始化时,arr数组里没有\0,strlen找不到结束标志,所以他一直往后数,所以打印到的是随机值

  • 注意函数的返回值为size_t,size_t是无符号整形。
#include<stdio.h>
#include<string.h>
int main()
{
    if(strlen("abcdef")-strlen("abcdefgh")>0)
    {
        printf(">\0");
    }
    else
    {
        printf("<");
    }
    return 0;
}

image-20210606155128810

解释:

我们发现这条语句和我们预想的不一样,为什么是大于呢?原因是strlen的结果是无符号数,无符号数-无符号数得到的还是无符号数,无符号数不可能是负的,所以它的结果永远都是大于0。

strlen函数的模拟实现
  • 计数器法
#include<stdio.h>
int my_strlen(const char*str)
{
    assert(str);
    int count=0;
    while(*str++)
    {
        count++;
    }
    return count;
}
int main()
{
    char arr[]={"hello"};
    int len = strlen(arr);
    printf("%d\n",len);
    return 0;
}

解释:

函数循环体判断部分*str++,*str为’\0’时退出循环,因为’\0’的ascii码值为0,为假。str指向字符串第一个字符,我们的想法是当它指向内容不为\0时,计数器++,最后返回count,我们看这个代码:*str++,++操作符的优先级比*高,我们先使用后置++,后置++是先使用后++。然后我们判断是不是\0,不是\0进入循环,计数器加加,要是\0,则退出循环,返回count。

  • 递归实现
#include<stdio.h>
int my_strlen(const char*str)
{
    assert(str);
    if(*str!='\0')
    {
        return 1+my_strlen(str+1);
    }
    else
    {
        return 0;
    }
}
int main()
{
    char arr[]={"hello"};
    int len = strlen(arr);
    printf("%d\n",len);
    return 0;
}

递归如何实现呢?我们利用大事化小的思想,看下图:image-20210606162923630代码解释:

刚开始进入函数,str指向’h’,‘h’不等于\0,return 1+my_strlen(str+1),递归调用,str+1指向了’e’,b不等于\0,继续递归调用,str+1指向了’l’,l不等于\0,继续递归调用,str+1指向了’l’,l不等于\0,继续递归调用,str+1指向了’o’,o不等于\0,继续递归调用,str+1指向了’\0’,不会再进行调用,此时就到了归的环节了,归的是调用它的地方,所以依次return 1,return2,return 3,return 4,return 5,最后返回给main函数中调用它的地方。

  • 指针-指针
#include<stdio.h>
int my_strlen(char* str)
{
    char* start=str;
    while(*str!='\0')
    {
        str++;
    }
    return str-start;
}
int main()
{
    char arr[]="abcdef";
	int len=my_strlen(arr);
    printf("%d\n",len);//6
    return 0;
}

解释:

指针-指针,得到的是指针和指针之间的元素个数

讲到这里,我们求字符串长度函数就到这里了,下面我们看这个代码:

int main()
{
    char arr[20]={0};
    arr="hello";///error
    return 0;
}

这个代码是错误的,arr是数组名,数组名是首元素地址,把hello放入这个(编号)地址上去吗?不行的,我们是要放进arr的内存空间的,那么我们怎么放进去呢?

此时我们就要用到strcpy

strcpy(字符串拷贝)
  • 库函数strcpy函数的原型如下:
char *strcpy(char *destination,const char *source);

image-20210606193209887

我们首先来看一下它的使用:

#include<stdio.h>
#include<string.h>
int main()
{
    char arr[20]={0};
    strcpy(arr,"hello");//传字符串实际上是将hello字符串的首字符地址h传过去了
    printf("%s\n",arr);
    return 0;
}

image-20210606170320427

  • 源字符串必须以 ‘\0’ 结束。
int main()
{
    char arr1[20]={0};
    char arr2[]={'a','b','c'};
    strcpy(arr,arr2);
    return 0;
}

arr2能拷贝进arr1吗?

答案是不行的,我们拷贝是要将’\0’也拷贝进去的,源字符串没有’\0’,我们不知道啥时候拷贝停止

  • 会将源字符串中的 ‘\0’ 拷贝到目标空间。
#include<stdio.h>
#include<string.h>
int main()
{
    char arr[20]={0};
    strcpy(arr,"hello");//传字符串实际上是将hello字符串的首字符地址h传过去了
    printf("%s\n",arr);
    return 0;
}

如果你不相信我们可以调试:

image-20210606171305712

调试后发现确实要将\0也拷贝进去的

  • 目标空间足够大,能够存入源字符串长度的空间
#include<stdio.h>
#include<string.h>
int main()
{
    char arr1[5]={0};
    strcpy(arr1,"hello world");
    return 0;
}

image-20210606173112716

目标空间不够时,他会帮你拷贝进去,但是他会出问题

而程序运行是会报错的
image-20210606173219454

我们进行打印arr1是他会打印出来,但是会报错:

image-20210606232501412

  • 目标空间必须可变。
  #include<stdio.h>
  #include<string.h>
  int main()
  {
      char* p = "************";
      char arr[]="hello";
      strcpy(p,arr);
      return 0;
  }

字符指针p指向的字符串是常量字符串,是不能修改的,所以我们的目标空间必须可变

我们调试发现,程序会崩掉:

image-20210606173741998

strcpy函数的模拟实现
 #include<stdio.h>
 #include<assert.h>
 char* my_strcpy(char* dest, const char* scr)
 {
     char* temp = dest;//创建一个临时变量存储dest,以便后面返回目标字符串的地址
     assert(dest && scr);
     while (*dest++ = *scr++)
     {
         ;
     }
     return temp;
 }
 int main()
 {
     char arr1[] = "**************";
     char arr2[] = "hello";
     printf("%s\n", my_strcpy(arr1, arr2));
     return 0;
 }
 

解释:

循环判断条件是*dest++ = *scr++,首先我们要知道,a=b这个表达式的值是a的值,我们将arr1中的每个字符赋值给arr2中时,其实整个表达式的值在每次循环时的值分别为’h’e’l’l’o’\0’字符的ASSIC码值,当我们把\0赋值到arr2中时,同时整个表达式的值也为0了,所以退出循环。

  • 注意:

我们必须保证目标字符数组的空间足以容纳需要复制的字符串。若字符串比数组长,多余的字符仍被复制,它们将覆盖原先存储于数组后面的内存空间的值。strcpy无法解决这个问题,它无法判断目标字符数组的长度。

接下来我们看strcat连接字符串

strcat(连接字符串)
  • 库函数strcpy函数的原型如下:
char * strcat ( char * destination, const char * source );

image-20210606192705457

  • 我们首先来看看strcat函数的使用:
#include<stdio.h>
#include<string.h>
int main()
{
    char arr1[20]={"hello "};//假设我们要追加world
    char arr2[]="world";
    //strcat(arr1,"world");//字符串追加或连接
    strcat(arr1,arr2);
    printf("%s\n",arr1);
    return 0;
}

image-20210606174630815

strcat(arr1,“world”); strcat(arr1,arr2);这两种方式都可以字符串连接。

  • 源字符串必须以 ‘\0’ 结束。
  #include<stdio.h>
  #include<string.h>
  int main()
  {
      char arr[]={'h','e','l','l','o'};
      char arr2[]="world";
      strcat(arr1,arr2);//字符串追加或连接
      printf("%s\n",arr1);
      return 0;
  }  

如果不以\0结束,是不能进行连接的,为什么不能连接呢?慢慢往下看,看到strcat函数是如何实现的你就知道啦!

image-20210606180926715

  • 目标空间必须有足够的大,能容纳下源字符串的内容。
  #include<stdio.h>
  #include<string.h>
  int main()
  {
      char arr1[8] = { "hello "};
      char arr2[] = "world";
      strcat(arr1, arr2);//字符串追加或连接
      printf("%s\n", arr1);
      return 0;
  }

如果目标空间不足够大,这里是会发生访问越界的,这里只能拷贝进wo。

image-20210606181444256

  • 目标空间必须可修改。
  #include<stdio.h>
  #include<string.h>
  int main()
  {
      char *p = "hello ";
      char arr2[] = "world";
      strcat(p, arr2);//字符串追加或连接
      return 0;
  }

image-20210606181708006

字符指针p指向的字符串是常量字符串,是不能修改的,所以我们的目标空间必须可变

  • 字符串自己给自己追加,如何?
 #include<stdio.h>
 #include<string.h>
 int main()
 {
     char arr[10]={"abcd"};
     strcat(arr,arr);
     printf("%s\n",arr);
     return 0;
 }

看下图解释:

image-20210607005200037

我们调试发现:确实进行了死循环的连接,因为没有了连接的结束标志\0,不知道啥时候停止,直到空间满时会报错。

image-20210607005541404

这里将追加的结束标志修改了,程序陷入了死循环,所以strcat函数是不能自己连接自己的

  • 目的字符串和源字符串必须要有\0

我们这里来看一下strcat是如何实现的,我们首先看下面代码测试:

#include<stdio.h>
#include<string.h>
int main()
{
    char arr1[20]={"hello \0*************"};//假设我们要追加world
    char arr2[]="world";
    strcat(arr1,arr2);//字符串追加或连接
    printf("%s\n",arr1);
    return 0;
}  

image-20210606182517104

strcat函数的实现是目标字符串中的结束标志\0被源字符串的首字符覆盖,然后依次进行追加,直到追加到\0,即追加结束。

arr1中的\0是开始追加的标志,arr2中的\0是追加结束的标志,所以目的字符串和源字符串必须要有\0

strcat函数的模拟实现
  • 1.找到目标字符串中的\0。
  • 2.源数据追加过去,包含\0。
#include<stdio.h>
void my_strcat(char *dest,const char*src)
{
    assert(dest&&src);
    //1.找到目标字符串中的\0
    while(*dest)
    {
        dest++;
    }
    //2.源字符串的追加,包含\0
    while(*dest++=*src++)
    {
        ;
    }
}
int main()
{
    char arr1[20]={"hello "};//假设我们要追加world
    char arr2[]="world";
    my_strcat(arr1,arr2);//字符串追加或连接
    printf("%s\n",arr1);
    return 0;
} 

image-20210607085826442

关于返回值:

image-20210606095812112

库函数里strcat返回的是目标空间的起始地址,所以我们也将目标空间的起始地址返回,my_strcat函数返回的是指针,那么我们在打印时就可以这样打印:

printf("%s\n",my_strcat(arr1,arr2));

看如下代码:

#include<stdio.h>
char* my_strcat(char *dest,const char*src)
{
    char *ret=dest;//利用ret变量保存目标空间的起始地址
    assert(dest&&src);
    //1.找到目标字符串中的\0
    while(*dest)
    {
        dest++;
    }
    //2.源字符串的追加,包含\0
    while(*dest++=*src++)
    {
        ;
    }
    return ret;//返回起始地址
}
int main()
{
    char arr1[20]={"hello "};//假设我们要追加world
    char arr2[]="world";
    printf("%s\n",my_strcat(arr1,arr2));
    return 0;
} 

注意:

和前面的strcpy一样,我们必须保证目标字符数组剩余的空间足以保存整个源字符串。但这次并不是简单地把源字符串的长度和目标字符数组的长度进行比较,我们必须考虑目标数组中原先存在的字符串。

strcmp(字符串比较)

在讲strcmp之前我们先来看一下下面的这段代码:

#include<stdio.h>
int main()
{
    char *p="qwer";
    char *q="awerty";
    if(p>q)
    {
        printf(">\n");
    }
    else
    {
        printf("<=\n");
    }
    return 0;
}

经过调试发现:

image-20210607091802834

p,q是字符指针,存放的是字符串首字符的地址,我们想需要的是比较两个字符串,而这里实际上比较的是p,q指针变量的地址,所以肯定不正确。

那么下面的这种写法可行吗?答案是也不行

if("obc">"abcdef")//这里比较的是字符o和字符a的地址
{
    
}

请看如下动图演示调试:

动画

如果比较的是字符串的话,首字符o是大于首字符a的,应该进入if语句里面,但是我们调试发现这里进入了else语句里面,所以这里比较比较的是首字符o和首字符a的地址,而不是字符串,所以这种比较方式也不可行

这就出现了我们的主角strcmp(字符串比较函数)

  • 库函数strlen函数的原型如下:
int strcmp(const char *string1,const char *string2);

image-20210607093502701

下面代码才能正确的比较两个字符串:

#include<stdio.h>
#include<string.h>
int main()
{
    char *p="qwer";
    char *q="awerty";
	strcmp(p,q);
    return 0;
}

知道了这个库函数,那么这个库函数的返回值是什么呢?

image-20210607093841550

我们发现字符串1小于字符串2时,返回的值是小于0,字符串1等于字符串2时,返回的值为等于0,字符串1大于字符串2时,返回的值为大于0。

#include<stdio.h>
#include<string.h>
int main()
{
    char *p="qwer";
    char *q="awerty";
    int ret = strcmp(p,q);
    printf("%d\n",ret);
    return 0;
}

image-20210607094343531

!!!这里说明一下,博主的编译器是vs2019,vs编译器他将大于返回1,等于返回0,小于返回-1,像上面的代码,只要返回的值大于0就可以了,不需要纠结和我打印的值不一样

strcmp函数的模拟实现

实现思路如下图解释:

image-20210607100231504

代码实现如下:

#include<stdio.h>
int my_strcmp(const char *s1,const char *s2)
{
    assert(s1&&s2);
    while(*s1==*s2)
    {
        if(*s1=='\0')
        {
            return 0;
        }
        s1++;
        s2++;
        
    }
    if(*s1>*s2)
    {
        return 1;
    }
    else
    {
        return -1;
    }
    //return *s1-*s2;//刚好符合大于返回大于0的数,小于返回小于0的数
}
int main()
{
    char *p="abcdef";
    char *q="abcc";
	int ret = my_strcmp(p,q);
    if(ret>0)
    {
        printf("p > q\n");
    }
    else if(ret<0)
    {
        printf("p < q\n");
        
    }
    else
    {
        printf("p == q\n");
    }
    return 0;
}

image-20210607100923166

注意1:

如果有些人没有关注到strcmp函数的返回值,它们常常会写出这样的表达式:if(strcmp(a,b)),他以为如果两个字符串相等,它的结果将会是真。但是,事实上这个结果却刚刚好相反,因为两个字符串相等时它返回的是0。

注意2:

标准并没有规定当两个字符串不相等时,strcmp函数返回的具体值是多少,它只是说字符串1大于字符串2会返回大于0的数,字符串1小于字符串2会返回小于0的数。


长度受限制的字符串函数介绍

strncpy
  • 库函数strcpy函数的原型如下:
char *strncpy(char *dest,const char *source,size_t count);

image-20210607101921079

我们发现strncpy相比于strcpy增加了一个参数,这个参数类型为size_t,无符号的数,strncpy函数用于将指定长度的字符串复制到字符数组的前(指定长度)个字符中

我们先来看一看它的使用:

#include<stdio.h>
#include<string.h>
int main()
{
    char arr1[20]="abcdef";
    char arr2[]="qwer";
    strncpy(arr1,arr2,2);//相对安全
    //strcpy(arr1,arr2);
    printf("%s\n",arr1);
    return 0;
}

image-20210607102017163

strncpy函数用于将指定长度的字符串复制到字符数组中,他将arr2的前两个字符复制到arr1中。

当传入的长度要比源字符串长能拷贝进去吗?

#include<stdio.h>
#include<string.h>
int main()
{
    char arr1[20]="abcdef";
    char arr2[]="qwer";
    strncpy(arr1,arr2,6);//相对安全
    //strcpy(arr1,arr2);
    printf("%s\n",arr1);
    return 0;
}

image-20210607102713955

当传入的长度要比源字符串长,strncpy函数也是能拷贝进去,而且打印的arr是qwer,为什么不是qweref呢?为什么呢?

我们首先调试一下:

开始时的arr1:

image-20210607113433277

最后的arr1:

image-20210607113532168

我们发现他不仅将qwer拷贝过去了,他还会给你拷贝够6个,只不过后面拷贝过去的是\0。实际上如果strlen(source)的值小于指定的长度,目标数组就用额外的NULL字节填充到指定长度,strlen(source)的值大于或等于指定的长度,那么只有指定长度个字符被复制到目标字符串中。注意!此时并不会将以\0拷贝过去。

当然我们必须需要知道库函数的代码是怎么样的,才能知道结果为什么是这样的,我们首先先来看一下库函数是怎么实现的,下面代码为strncpy在库函数中的实现:

char * __cdecl strncpy (
        char * dest,
        const char * source,
        size_t count
        )
{
        char *start = dest;

        while (count && (*dest++ = *source++) != '\0')    /* copy string */
                count--;

        if (count)                              /* pad out with zeroes */
                while (--count)
                        *dest++ = '\0';

        return(start);
}

当传入的长度要比源字符串长时的解释:

count刚开始是6,每拷贝一次conut–,当字符串qwer拷贝完之后,遇到了\0,while循环终止,此时count是2,然后,count不为0,为真,进入if语句,将目标字符串拷贝进去的串后面的2个字符填充为\0,所以前面的代码打印的是qwer,这就解释了当传入的长度要比源字符串长时的情况。

strncat
  • 库函数strlen函数的原型如下:
char *strncat(char *dest,const char *source,size_t count);

image-20210607111554433

strncat与strcat相比于strncpy与strcpy是一样的道理,strncat相比于strcat增加了一个参数,这个参数类型为size_t,无符号的数,strncat函数用于将指定长度的字符串连接到字符数组中

  • 我们先来看一看它的使用:
#include<stdio.h>
#include<string.h>
int main()
{
    char arr1[20]="hello ";
    char arr2[]={"world"};
    strncat(arr1,arr2,3);
    printf("%s\n",arr1);
    return 0;
}

image-20210607112221779

strncat函数用于将指定长度的字符串连接到字符数组中,这里将arr2中的前三个字符连接到arr1中去

那么当指定字符长度要比源字符串长时能连接过去吗?

#include<stdio.h>
#include<string.h>
int main()
{
    char arr1[20]="hello ";
    char arr2[]={"world"};
    strncat(arr1,arr2,10);
    printf("%s\n",arr1);
    return 0;
}

image-20210607114617702

答案是可以的。

我们来看一看库函数中strncat是怎么实现的:

char * __cdecl strncat (
        char * front,
        const char * back,
        size_t count
        )
{
        char *start = front;

        while (*front++)
                ;
        front--;

        while (count--)
                if ((*front++ = *back++) == 0)
                        return(start);

        *front = '\0';
        return(start);
}

当我们指定字符传10进去时,我们看第13行代码,while(count–)循环里面进行连接,当连接到\0时,count不为0,循环继续,此时也将源字符串中的\0连接上去了,此时if语句里面的表达式成立(这里注意例如:a=b,则整个表达式的结果为a的值),然后return回目的字符串的地址了;当我们指定字符传3时,while循环当count变为0时,不会再进入循环,此时我们还没有将\0连接上去,所以后面*front = ‘\0’;这句代码就将\0连接上去了,最后返回目的字符串的地址。

注意:

和strncpy不同的是,它最多从源字符串中复制源字符串的长度个字符到目标数组的后面,strcat总是在结果字符串后面添加一个\0,它不会像strncpy那样在指定长度大于源字符串长度时对目标数组用\0进行填充。strncat最多向目标数组复制源字符串的长度个字符(加\0),它不会去管目标参数除去原先的字符串还够不够存储连接过来的字符串。

strncmp
  • 库函数strlen函数的原型如下:
int strncmp( const char *string1, const char *string2, size_t count );

image-20210607121418471

长度受限制的字符串函数与各自对应的长度受限制的字符串函数是十分相似的,strncmp与strcmp相比于前面两个是一样的道理,strncmp相比于strcmp增加了一个参数,这个参数类型为size_t,无符号的数,strncmp函数的作用是:字符串1的前指定长度的字符与字符串2中的前(指定长度)的字符进行比较

返回值:image-20210607121758827

字符串1的前指定长度的字符与字符串2中的前(指定长度)的字符相等时,返回0

字符串1的前指定长度的字符小于字符串2中的前(指定长度)的字符时,返回<0的值

字符串1的前指定长度的字符大于字符串2中的前(指定长度)的字符时,返回>0的值

  • 我们先来看strcmp比较以下这两个字符串的结果:
#include<stdio.h>
#include<string.h>
int main()
{
    char *p="abcdef";
    char *q="abcdqwert";
    int ret = strcmp(p,q);
    printf("%d\n",ret);//-1
    return 0;
}

image-20210607121954068

p所指向的字符串是比q指向的字符串小的,所以返回的是小于0的数,这里我的vs编译器返回的是-1

然后我们再来看看strncmp指定长度比较以下两个字符串的结果:

#include<stdio.h>
#include<string.h>
int main()
{
    char *p="abcdef";
    char *q="abcdqwert";
    int ret = strncmp(p,q,4);
    printf("%d\n",ret);
    return 0;
}

image-20210607122434704

p所指向字符串的前4个字符和q指向的字符串的前4个字符是相等的,所以返回的是0


字符串查找函数

strstr(查找子串)
  • 作用是查找子串并返回字符串中首次出现子串的地址。
  • 库函数strlen函数的原型如下:
char *strstr( const char *string, const char *strCharSet );

image-20210607123726388

  • 函数参数以及返回值解释:

image-20210607125948673

  • 接下来我们来看一看它的使用:
#include<stdio.h>
#include<string.h>
int main()
{
    char arr1[]="abcdefabcdef";
    char arr2[]="bcd";
    //在arr1中查找是否包含arr2
    char *ret = strstr(arr1,arr2);//找到了就返回第一次出现的地址
    if(ret==NULL)
    {
        printf("没找到\n");
    }
    else
    {
        printf("找到了:%s\n",ret);
    }
    return 0;
}

image-20210607130534256

我们可以看到它返回值存入ret变量中,我们将ret打印,它确实返回的是子串第一次出现的地址

同样的我们可以测试找不到的情况:

image-20210607130935505

可以看到此时ret为NULL,说明返回的是NULL,打印了没找到

strstr函数的模拟实现
#include<stdio.h>
char *my_strstr(const char *str1,const char *str2)
{
    assert(str1&&str2);
    const char *s1=NULL;
    const char *s2=NULL;//我们比较时不使用str1和str2,我们创建两个临时变量来移动比较,因为如果我们用str1和str2一直进行移动比较,到最后我们不知道起始的串在哪里,无法找到了。
    const char *cp=str1;//指向每次开始比较时起始的第一个字符
    if(*str2=='\0')//需要查找的字符串为空字符串的话,返回要浏览的字符串
    {
        return (char*)str1;//因为函数返回值为char*,所以需要强制类型转换
    }
    while(*cp)//cp指向不为\0时进入
    {
        s1=cp;//每次将新的cp赋给s1
        s2=str2;//将需要查找的串赋给s2
        while(*s1 && *s2 && (*s1==*s2))//s1和s2指向内容相等时s1、s2分别加加指向下一个字符进行比较,直到s1指向内容不等于s2所指向内容时跳出循环,并且s1和s2指向\0时,循环也停止
        {
            s1++;
            s2++;
        }
        if(*s2=='\0')//当s2指向为\0时,说明比较结束,是子串,返回子串的起始位置
        {
            return (char*)cp;
        }
        cp++;//s2不是\0时,说明不是子串,cp指向下一个字符开始比较
    }
    return NULL;
}
int main()
{
    char arr1[]="abcdefabcdef";
    char arr2[]="bcd";
    //在arr1中查找是否包含arr2
    char *ret = my_strstr();//找到了就返回第一次出现的地址
    if(ret==NULL)
    {
        printf("没找到\n");
    }
    else
    {
        printf("找到了:%s\n",ret);
    }
    return 0;
}
strtok(分割字符串)
  • 库函数strtok函数的原型如下:
char *strtok( char *strToken, const char *strDelimit );
  • strtok()用来将字符串分割成一个个片段。参数strToken指向欲分割的字符串,参数strDelimit 则为分割字符的字符串,当strtok()在参数strToken 的字符串中发现到参数strDelimit 的分割字符时则会将该字符改为\0 字符。在第一次调用时,strtok()必需给予参数strToken字符串,往后的调用则将参数strToken设置成NULL。每次调用成功则返回下一个分割后的字符串指针。

image-20210607153520189

  • 函数参数以及返回值解释:

image-20210607160512949

注意:

如果strtok函数的第1个参数不是NULL,函数将找到字符串的第1个标记。strtok同时将保存它在字符串中的位置。如果strtok 函数的第1个参数是NULL,函数就在同一个字符串中从这个被保存的位置开始像前面一样查找下一个标记。如果字符串内不存在更多的标记,strtok函数就返回一个NULL 指针。在典型情况下,在第1次调用strtok时,向它传递一个指向字符串的指针。然后,这个函数被重复调用(第1个参数为NULL),直到它返回NULL为止。

  • 接下来我们来看一看strtok函数的使用:
#include<stdio.h>
#include<string.h>
int main()
{
	char arr[] = "www.baidu.com";
	//printf("%s\n",strtok(arr, "."));
	//printf("%s\n", strtok(NULL, "."));
	//printf("%s\n", strtok(NULL, "."));*/
	char* temp;
	for (temp = strtok(arr, "."); temp != NULL; temp = strtok(NULL, "."))
	{
		printf("%s\n", temp);
	}
	return 0;
}

image-20210607172907985

我们调试可以看到每一步strtok的调用的返回值temp的变化:

strtok


错误信息报告

strerror
  • 库函数strerror函数的原型如下:

    char *strerror( int errnum );
    

image-20210607173152608

我们在使用库函数的时候,调用库函数失败时,都会设置错误码,或者说当你调用一些函数,请求操作系统执行一些功能如打开文件时,如果出现错误,操作系统是通过设置一个外部的整型变量errno进行错误代码报告的。strerror函数把其中一个错误代码作为参数并返回一个指向用于描述错误的字符串的指针。

我们的strerror函数翻译错误码并返回一个指向用于描述对应错误信息的字符串的指针。

  • 函数参数以及返回值解释:

image-20210607174810756

  • 接下来我们来看一看strerror函数的使用:
#include<stdio.h>
#include<string.h>
int main()
{
    printf("%s\n",sterror(0));
    printf("%s\n",sterror(1));
    printf("%s\n",sterror(2));
    printf("%s\n",sterror(3));
    printf("%s\n",sterror(4));
    printf("%s\n",sterror(5));
    FILE* pf=fopen("test.txt","r");
    if(pf==NULL)//打开文件失败时,会返回NULL
    {
        printf("%s\n",strerror(errno));
        return 1;
    }
    fclose(pf);
    pf=NULL;
    return 0;
}

image-20210607174032976

错误码0,1,2,3,4,5所对应的错误信息如上图打印,当我们打开一个文件失败时,操作系统是通过设置一个外部的整型变量errno进行错误代码报告的。

perror(打印错误信息)

perror函数的功能是打印错误信息,它和strerror函数不同的是,strerror返回的是字符指针,perror返回的是void,使用strerror函数时,如果需要打印错误信息,需要我们自己写printf进行打印,而perror函数在调用它时,就将错误信息打印了。参数 string 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。

  • 库函数strerror函数的原型如下:
 void perror( const char *string );

image-20210609222706679

perror函数实现主要是以下两步:

  • 1、首先把错误码转换为错误信息

  • 2、打印错误信息(包含了自定义信息)

#include<stdio.h>
#include<string.h>
int main()
{
	printf("%s\n", strerror(0));
	printf("%s\n", strerror(1));
	printf("%s\n", strerror(2));
	printf("%s\n", strerror(3));
	printf("%s\n", strerror(4));
	printf("%s\n", strerror(5));
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)//打开文件失败时,会返回NULL
	{
     	//printf("%s\n",strerror(errno));
		perror("fopen");
		return 1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

它会直接打印出错误信息:

image-20210609091909500

我们前面讲到的都是操作字符串的,下面我们来讲一些操作字符的函数。


字符操作

  • 标准库包含了两组函数,用于操作单独的字符,它们的原型位于头文件ctype.h。第一组函数为字符分类,第二组函数用于转换字符。下面我们来看这两组函数。
字符分类

每一个分类函数接收一个包含字符值得整形参数。函数测试这个字符并返回一个整形值,表示真或假。

分类函数如下表:

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

我们来看两个字符分类函数的测试:

#include<ctype.h>
#include<stdio.h>
int main()
{
    char ch='#';
    int ret = isdigit(ch);//是不是数字字符
    printf("%d\n",ret);
    return 0;
}

image-20210609182023305

image-20210609182652047

如果是数字字符返回非零,如果不是数字字符返回零

#include<ctype.h>
#include<stdio.h>
int main()
{
    char ch='a';
    int ret = islower(ch);//是不是小写字母
    printf("%d\n",ret);
    return 0;
}

image-20210609182530309

image-20210609182559009

如果是小写字母返回非零,如果不是小写字母返回零

字符转换
  • 转换函数把大写字母转换成小写字母或者把小写字母转换为大写字母。
int tolower(int ch);
int toupper(int ch);
  • toupper函数返回其参数的对应大写形式,tolower函数返回其参数的对应小写形式。如果函数的参数并不是一个处于适当大小写状态的字符(即toupper的参数不是小写字母或tolower的参数不是大写字母),函数将不修改参数直接返回。
#include<stdio.h>
#include<ctype.h>
int main()
{
    char arr[20]={0};
    scanf("%s",arr);
    int i=0;
    while(arr[i]!='\0')
    {
        if(isupper(arr[i]))
        {
            arr[i]=tolower(arr[i]);
        }
        printf("%c",arr[i]);
        i++;
    }
    return 0;
}

image-20210609183039734

如果函数的参数并不是一个处于适当大小写状态的字符(即toupper的参数不是小写字母或tolower的参数不是大写字母),函数将不修改参数直接返回。

image-20210609184323972

我们在判断一个ch字符是不是一个大写字符时可以这样判断:

if(ch>='A'&&ch<='Z')
  • 这样判断是可以判断,但是程序的可移植性不好,可能在其他使用不同的字符集机器上可能是不会成功的,而下面这个语句,无论在哪个机器上,都能够顺利运行的。
if(isupper(ch));

内存操作函数

  • 我们上面讲的函数都是操作字符串或者字符的,字符串是以NULL字节结尾的,所以字符串内部是不能包含任何NULL字节的,那么对于那些内部包含零值的数据类型该怎么处理呢?我们是无法通过字符串函数来操作这类数据类型的,这时就出现了另外一组相关的函数—内存操作函数

内存操作函数的操作和字符串函数类似,只是它可以处理任意的字节序列,接下来我们首先来看memcpy函数。

memcpy
  • 库函数memcpy函数的原型如下:
void *memcpy( void *dest, const void *src, size_t count );

image-20210609111731601

  • 函数的参数和返回类型解释:

image-20210609192400038

  • 下面我们来看一下它的使用:
#include<string.h>
int main()
{
    int arr1[10]={1,2,3,4,5,6,7,8,9,10};
	int arr2[20]={0};
    memcpy(arr2,arr1,20);
    return 0;
}

image-20210609192956909

  • 我们调试能够看到memcpy函数将arr1的前20个字节拷贝到了arr2中
memcpy函数的模拟实现

memcpy函数是怎么将源数据拷贝到目的地的呢?我们这样进行拷贝可以吗?

void *my_memcpy(void *dest,const void *src,size_t num)
{
    assert(dest&&src);
    void *ret=dest;
    while(num--)
    {
        *dest++ = *src++;
    }
    return ret;
}

这样显然是不可以的,因为我们的函数参数是void*的类型,我们是不能对void*类型的参数进行解引用操作的,而且我们解引用也不能明确的知道他会访问几个字节,所以我们要先将dest和src强制类型转换为char*,我们一个字节一个字节的进行拷贝。那么拷贝成功一个字节我们怎么让dest和src指向下一个字节呢?同样的我们需要将dest和src分别强制类型转换为char*然后+1,这样就向后移动一个字节了

所以我们的代码应该这样写:

#include<assert.h>

void *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;
    }
    return ret;//返回拷贝目的地的起始地址
}
int main()
{
    int arr1[10]={1,2,3,4,5,6,7,8,9,10};
	int arr2[20]={0};
    my_memcpy(arr2,arr1,20);
    return 0;
}

完成了memcpy函数的模拟,我们思考这么一个问题:我们将arr1数组的1,2,3,4,5拷贝到3,4,5,6,7所在的内存空间上,可不可以呢?

image-20210609200050886

my_memcpy(arr1+2,arr1,20);

image-20210609200730081

我们结果和我们想的并不一样,我们想的是1,2,1,2,3,4,5,8,9,10,但是结果却是1,2,1,2,1,2,1,8,9,10,为什么呢?看下图解释:

image-20210609203746988

所以memcpy应该拷贝不重叠的内存

这时就有了memmove函数,memmove可以处理内存重叠的情况。

memmove
  • 库函数memmove函数的原型如下:
void *memmove( void *dest, const void *src, size_t count );

image-20210609151259594

memmove的参数和返回类型的解释与memcpy函数是一样的,这里就不再进行解释了。

  • 下面我们来看看memmove函数的使用:
#include<string.h>
int main()
{
    int arr1[10]={1,2,3,4,5,6,7,8,9,10};
    memmove(arr1+2,arr1,20);
    return 0;
}

image-20210609204605811

我们调试发现,已经进行了拷贝,memmove可以处理内存重叠的情况。

memmove函数的模拟实现

memmove函数该怎么实现呢?请看下图说明:

image-20210609212838469

我们进行拷贝时,可以从前往后拷贝,也可以从后往前拷贝。当我们想要把如图中的1 2 3 4 5拷贝到3 4 5 6 7上去,从前往后显然是不行的,但是我们仔细思考发现从后往前是可以拷贝成功的,数组里面元素地址从前往后是从低到高的。我们得出结论:当src的地址大于dest的地址时,我们可以用从前往后进行拷贝,当src的地址小于dest的地址时,我们可以用从后往前进行拷贝,当dest大于src加指定字节数时,无论是从前往后还是从后往前都可以完成拷贝。

我们也可以将上面三种情况分为两种情况:dest<src的情况和dest>=的情况,因为第三种情况既可以从前往后拷贝,又可以从后往前拷贝。

最终的memmove的模拟实现代码如下:

void *my_memmove(void *my_memmove(void *dest,const void *src,size_t num))
{
    assert(dest&&src);
   	void* ret=dest;
    if(dest<src)
    {
        //前->后
        while(num--)
        {
            *(char*)dest=*(char*)src
        	dest=(char*)dest+1;
            src=(char*)src+1;
        }
    }
    else
    {
        //后->前
        while(num--)
        {
            *((char*)dest+num)=*((char*)src+num);
        }
    }
    return ret;
}

memmove函数我们的讲解就到这里啦!

下面我们来看一个小问题:

我们在前面中使用自己模拟实现的memcpy函数时,发现是不能拷贝重叠内存的,可能有人会疑惑,为什么我的memcpy库函数是可以实现拷贝重叠内存的呢?是不是我们的模拟实现的memcpy函数错了呢?

image-20210609215141086

如上图,我们的vs编译器中的memcpy函数就可以重叠拷贝。

解释:

我们实现的memcpy当然不是错误的,对于memcpy函数,标准规定只要实现了不重叠拷贝时的情况就可以了,而vs编译器中的实现既可以拷贝不重叠内存,也可以拷贝重叠内存,在vs编译器中,memcpy是可以实现重叠拷贝的,但是在其他编译器环境中,memcpy不一定也能实现重叠拷贝。

memcmp(内存比较)

函数的作用是比较内存区域buf1和buf2的前count个字节

库函数memcmp函数的原型如下:

int memcmp( const void *buf1, const void *buf2, size_t count );

image-20210609155837508

  • 下面我们来看一下它的使用:
#include<stdio.h>
#include<string.h>
int main()
{
    float arr1[]={1.0,2.0,3.0,4.0};
    float arr2[]={1.0,3.0};
    int ret=memcmp(arr1,arr2,4);//0
    //memcmp - strcmp返回值的设置是一样的
    return 0;
}

image-20210609220345738

关于函数返回值:

image-20210609160153875

memcmp函数的返回值和strcmp函数返回值的设置是一样的,这里就不进行描述了,前面我们已经详细说过了。

memset

库函数memset函数的原型如下:

void *memset( void *dest, int c, size_t count );

image-20210609220953895

将指针变量 dest所指向的前 count 字节的内存单元用一个“整数” c 替换,注意 c 是 int 型。dest 是 void 型的指针变量,所以它可以为任何类型的数据进行初始化。*

  • 下面我们来看一下它的使用:
#include<stdio.h>
#include<string.h>
int main()
{
    int arr[10]={0};
    memset(arr,1,20);//以字节为单位设置内存的
    return 0;
}

image-20210609221805429

将arr中的前20个字节初始化为1


总结

  • 字符串是零个或多个字符的序列,该序列以\0为结尾。标准库提供了一些函数处理这些字符串,它们的原型位于头文件string.h中。

长度不受限制的字符串函数

  • strlen函数用于计算一个字符串的长度,要注意它的返回值是一个无符号的整数,我们将它用于表达式中时要小心。strcpy函数把一个字符串从一个位置复制到另一个位置,而strcat函数把一个字符串的一份拷贝连接到另一个字符串的后面,我们要注意,这两个函数都是假定它们的参数是有效的字符串,如果源字符串和目标字符串出现重叠时,函数的结果是未定义的。strcmp函数对两个字符串进行比较,它的返回值提示第一个字符串是大于、等于、或者小于第二个字符串。

长度受限制的字符串函数

  • strncpy、strncat、strncmp都类于它们所对应的长度不受限制的函数,区别在于它们多了一个长度参数,在strncpy函数中,长度指定了多少个字符将被拷贝进目标字符数组中,如果源字符串比指定字符长,将不会拷贝\0;如果源字符串比指定长度短,它会将源字符串全部拷贝进目标字符数组中,然后剩余的长度将以\0填充进目标字符数组。在strncat函数中,长度指定了多少个字符将被连接到目标字符数组后面,当指定长度大于源字符串长度时,它会拷贝整个源字符串过去(加\0拷贝过去),当指定长度小于源字符串长度时,它会将指定长度字符拷贝到目标字符数组中(加\0拷贝过去),所以不管指定长度和源字符串长度的关系大小,它都会将\0拷贝过去。在strncmp函数中,长度指定了字符比较的数目,它的返回值提示第一个字符串的前指定字符是大于、等于、或者小于第二个字符串的前指定字符。

字符串查找函数

  • 字符串查找函数我们这里讲解了两个函数,strstr函数和strtok函数,strstr函数在一个字符串中查找另一个字符串第一次出现的位置,strtok函数把一个字符串分割成几个标记,每次当它被调用时,都返回一个指向字符串中下一个标记位置的指针,当它找不到标记时,会返回空指针;这些标记由一个指定字符集的一个或多个字符分隔。

错误信息报告函数

  • strerror把一个错误代码作为它的参数,它返回一个指向描述错误信息字符串的指针。perror函数和strerror函数不同的是,我们在调用perror时,它会打印我们传入的信息,然后后面跟着错误信息,而strerror函数则需要我们自己打印错误信息。

字符操作函数

  • 标准库还提供了各种用于测试字符和转换字符的函数,我们使用这些函数的程序比那些自己执行字符测试和字符转换的程序更具移植性。toupper函数把一个小写字母字符转换成大写字母字符,而tolower函数则把一个大写字母转换成小写字符。测试字符大家可以认真看字符分类那一块讲解的那一个表格。它们的原型位于头文件ctype.h中

内存操作函数

  • memxxx类的函数提供了类型字符串函数的功能,但它们可以处理包括空字节在内的任意字节。这些函数都接受一个长度参数,memcpy将源参数向目标参数复制有长度参数指定的字节数,memmove与memcpy功能相同,只是它可以处理源参数和目标参数内存重叠的情况,memcmp函数比较两个参数的指定字节,memset函数把一个序列的指定字节初始化为特定的值。

有关字符串和字符函数的讲解就到这里,由于博主水平有限,如有错误之处,还望指正,欢迎大家学习交流!

大家如果觉得文章不错的话别忘了点赞评论加收藏哦

  • 12
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 17
    评论
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小赵小赵福星高照~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值