不会这些字符串操作,你怎么精通C语言?如何玩转C++?

简介: 对于学习C语言或者C++的同学,如果对字符串的操作不熟悉,那就无法在学习的过程中更进一步,因为这是非常基础,也是非常重要的一个环节,希望博主这篇文章中的例子,可以教你去搞定字符串操作!对于初学者来说,一下要记住这么多东西是很难的,所以【建议收藏】,方便以后用到的时候查找。

废话不多说,进入正题。字符串操作几乎是我们在C或C++学习的过程中都会遇到困惑的地方,我们得不到自己想要的输出结果, 或者得到了,但没完全得到。就比如我们得到了我们想要的字符串,但是紧接着也会输出一些乱码。类似如图所示的情况:

image.png

那么我们应该如何去搞定这些字符串操作的bug呢?接下来咱们来细细讨论:


对于一个字符串数组或者char型指针,我们一定应该给它初始化,因为不初始化的话,里面都是一些默认初始化的值,可以认为里面是一些“垃圾”数据,那么我们首先来介绍我们的第一个函数:


1. 内存初始化memset


我一般用memset这个函数来给动态申请的内存进行初始化,因为如果是栈区内存,我们一般就在变量声明的同时就进行初始化,所以在堆区申请的内存,我们需要手动来给它初始化,我们来看一下函数的声明:

void *
memset (
      void            *Dest,
      int              Value,
      ACPI_SIZE        Count)

从函数的声明来看,总共有三个参数,我们来解释一下这三个参数:

void   *Dest:这是我们要进行初始化的指针,在这里被默认为void*指针
int    Value:一个int型变量,也就是我们需要给这片内存初始化的值,一般传0
ACPI_SIZE  Count:这是我们需要初始化的内存长度,一般是和申请内存的大小相同

我们一般把一块申请到的内存,使用之前,初始化为0,以便后续使用,特别说明:memset 不止针对字符串指针,其它的指针也可以使用。我们来看一个简单的使用举例:

//对于栈区内存,我们在申请到的时候,就对其初始化
  char Des[15] = { 0 };
  //堆区内存,我们申请到之后,使用memset初始化为0
  char* ptr1 = (char*)malloc(15);
  memset(ptr1, 0, 15);//将我们申请到的15个字节初始化为0

对于栈区内存初始化,我们看一下这个图:

image.png

对于堆区内存初始化,我们来看一下初始化执行的效果:

image.png


2. 字符串长度计算strlen


一个字符串长度的计算会被用来干什么呢?也许拷贝字符串的时候会用到的吧,或者说来计算一下输入了多少字符?反正不管怎么说,这都是非常通用的字符串操作函数,来看一下它的声明:

ACPI_SIZE
strlen (
    const char     *String)

我们可以看到返回值是一个数值类型,而这个函数的参数只有一个,多少有点敷衍了。。。但是流程还是要走的哈,所以我们再来看一下参数代表的意义:

const char   *String:特别注意是一个const char*。有关这个的概念,
在我之前讲C语言指针的时候讲过,意思就是字符串内容不允许修改,这是肯定的,我们传入字符串指针是为了让它计算长度,肯定不能被修改。

其实strlen的实现非常简单,在内部申请了一个局部变量用来计数,然后用一个while循环来判断,如果这个字符串不为 ‘\0’ ,那就指针后移,计数器++,否则,字符串长度计算结束,返回这个计数器变量的值。

现在我们来用它计算一个字符串的长度来看一下:

//初始化为HelloWorld
  char Des[] = "HelloWorld";//注意这是11个字节,因为还有 \0 结尾
  //计算字符串的长度
  size_t StringLength = strlen(Des);
  return 0;

计算这个字符串的结果:

image.png

从上图中我们应该能看到两个问题,首先我们默认初始化的Des字符串,里面有11个字符,但是最后我们发现计算出来的StringLength只算出了10,这个原因就是因为在strlen实现的时候,没有把 \0 的长度计算进去,所以需要注意:strlen计算出来的字符串长度不包含 \0 ,这个要切记,以后在写代码过程中会经常用到的。


3. 字符串复制strcpy


当我们想要将一个源字符串拷贝一个备份出来的时候,我们依然有可以使用的字符串操作函数,我们也可以预想的到,如果想要实现一个复制操作,那最起码得有源字符串和目标字符串,这样才能说是拷贝吧,我们在这里先说strcpy函数,我们还是惯例先来看看它的声明:

char *
strcpy (
      char           *DstString,
      const char     *SrcString)

我们看到这个函数的返回值是一个char型指针,但是我们一般不取它的返回值,因为在传参时候的DstString指向的就是我们要传回的字符串值,来看一下这两个参数分别代表什么:

char  *DstString:目标字符串指针,需要拷贝到的地方
const char  *SrcString:源字符串指针,被拷贝的对象

看一下使用举例:

//对于栈区内存,我们在申请到的时候,就对其初始化
  char SrcString[] = "HelloWorld";
  //堆区内存,15个字节的长度
  char* DstString = (char*)malloc(15);
  memset(DstString, 0, 15);//将我们申请到的15个字节初始化为0
  strcpy(DstString, SrcString);

再来看一下拷贝字符串的效果:

image.png

在使用strcpy的过程中需要注意:那就是对于目标内存长度的申请,因为在之前strlen计算长度的时候,并没有把字符串的结尾null给计算进去,所以申请内存时候要多申请一个字节,用来存放字符串结尾字符。这是因为strcpy在内部实现的时候,就会在字符串复制结束之后,在DstString的最后不上\0,也就是null字符来结尾,这个需要占用一个字节。


4. 字符串复制memcpy


在使用strcpy的过程中,我们发现我们只能按照字符串的null字符结尾来控制复制操作的停止,这显得有点被动了,那我们能不能变得主动出击呢?当然是可以的,我们接下来介绍的memcpy就是可以控制我们对于复制长度的控制:

void *
memcpy (
      void               *Dest,
      const void         *Src,
      ACPI_SIZE          Count)

我们还是来看一下各个参数的详细意思吧:

void         *Dest:需要拷贝到的目标指针首地址
const void   *Src:被拷贝的指针首地址
ACPI_SIZE    Count:当前指针类型被拷贝的个数


比如说我们指向从源字符串里面拷贝5个字符过来,那我们只需要将第三参数传个5就OK了,看一下下面这个例子:

//对于栈区内存,我们在申请到的时候,就对其初始化
  char SrcString[] = "HelloWorld";
  //堆区内存,15个字节的长度
  char* DstString = (char*)malloc(15);
  memset(DstString, 0, 15);//将我们申请到的15个字节初始化为0
  //我们只想复制前五个字节 Hello,所以这样写
  memcpy(DstString, SrcString,5);

我们可以使用memcpy只复制Hello,但是不能使用strcpy来达到这个效果:

image.png

但是这个函数就不会像strcpy一样,在复制完成后给补上 \0 来作为结尾,因为它不止针对字符串操作,所以不会默认给添加\0结尾。


那对于这个函数一定要注意,它不仅仅针对的是字符串系列的操作,我们可以从参数类型看出来,它并不是和str系列的函数一样,str系列都是char型参数,这个函数的参数都是void型参数,那就意味着什么类型,它都可以接受。其实不止当前函数,所有的mem系列函数都不是仅仅针对字符串操作。


5. 字符串复制memmove


好的,在这里我们来提出新的问题,因为有的同学喜欢玩,现在各个领域内卷态势严重,那字符串拷贝当然也要来帮帮场子,如果我们想实现一个自己拷贝自己,那我们使用什么函数呢?如果使用memcpy会有什么样的效果呢?我记得在很久之前一直被大家津津乐道的内存重叠问题,memcpy就是典型的例子,但是因为我很久没测试了,今天测试时候,居然发现memcpy内存重叠问题被解决掉了??

//对于栈区内存,我们在申请到的时候,就对其初始化
  char SrcString[] = "HelloWorld";
  //我希望拷贝HelloWorld 中的前8个字节给字符串本身
  //也就是我希望拷贝之后字符串变为HeHelloWor 
  //但是我预计它会出现HeHeHeHeHe的情况
  memcpy(SrcString + 2, SrcString, 8);

本来这里我是预计自己搞自己,然后会发生内存重叠,输出HeHeHeHeHe,然后再引出memmove来解决这个问题,没想到。。。它居然行了??也就是说正确输出了结果:

image.png

我都无语了,没想到微软居然把这个问题解决了,而我浑然不知!!!但是,但是,我肯定不甘心,我得告诉大家它之前是怎么实现的:

void *
my_memcpy(
  void                    *Dest,
  const void              *Src,
  size_t               Count)
{
  char                    *New = (char *)Dest;
  char                    *Old = (char *)Src;
  while (Count)
  {
  *New = *Old;
  New++;
  Old++;
  Count--;
  }
  return (Dest);
}

这就是它原本的实现逻辑,而按照这个函数的实现逻辑,我们再来测试一番:

image.png

OK!!nice!达到我要的错误示范的效果了,而且,它确实之前就是这么实现的!


接下来我们来“顺理成章”的引出我们的主角memmove吧!先来看看声明:

void *
memmove (
      void           *Dest,
      const void     *Src,
      ACPI_SIZE      Count)

对于这个函数的三个参数我们就不解释了,因为这三个参数和memcpy的三个参数是一样的,所以我们直接来看使用举例以及效果图吧:

image.png

这里也给大家看一下这个memmove的实现逻辑,来帮助大家更好的理解为什么能够避免内存重叠:

void *
memmove (
    void                    *Dest,
    const void              *Src,
    ACPI_SIZE               Count)
{
    char                    *New = (char *) Dest;
    char                    *Old = (char *) Src;
  //在这里对于两个地址的大小进行了比较,用来确定该如何去复制
    if (Old > New)
    {
        /*从头部复制*/
        while (Count)
        {
            *New = *Old;
            New++;
            Old++;
            Count--;
        }
    }
    else if (Old < New)
    {
        /*从尾部开始复制*/
        New = New + Count - 1;
        Old = Old + Count - 1;
        while (Count)
        {
            *New = *Old;
            New--;
            Old--;
            Count--;
        }
    }
    return (Dest);
}


6. 字符串比较strcmp


在两个字符串出现的时候,有时候我们需要比较两个字符串是否相同,或者看一下从哪儿开始不同的,那我们就用到了strcmp函数:

int
strcmp (
      const char     *String1,
      const char     *String2)

这个函数的返回值是我们需要注意的地方,它的两个参数倒也容易理解:

返回值:如果两个字符串相等,返回0,若String1>String2则返回 1,若String1<String2,则返回 -1
const char     *String1:第一个需要比较的字符串
const char     *String2:第二个需要比较的字符串

这个函数相对来说比较好理解,让我们来看一个小例子:

//初始化为HelloWorld
  char String1[] = "HelloWorld";
  //初始化为HelloChina
  char String2[] = "HelloChina";
  //预期返回1
  int index = strcmp(String1, String2);

比较的办法,就是从两个传入的指针第一个字符开始比较,然后比较标准就是按照ASCII值的大小,比如’W’就大于’C’,这个就根据ASCII表比较了。如上的代码我们预期index值是1:

image.png


7. 字符串比较memcmp


我们使用strcmp比较的时候也是有个不好的地方,那就是无法控制比较字符串的长短,字符串长短还是自己能够控制比较舒服,毕竟有时候不需要比较太长。那现在提供给了大家机会,那还愣着干什么,赶紧来看看memcmp:

int __cdecl 
memcmp(
  const void *s1, 
  const void *s2, 
  size_t n)

mem系列的函数我们之前就说过了不止针对字符串,从它的参数就可以看出,那这三个参数又是什么意思呢?其实也和memcpy差不多:

返回值:如果两个字符串相等,返回0,若String1>String2则返回 1,若String1<String2,则返回 -1
void         *s1:第一个需要比较的指针地址
const void   *s2:第二个需要比较的指针地址
size_t    n:比较的个数

它的返回值和strcmp是一样的,0,-1或者1,对于我们来说,我们就想比较两个字符串的前5个字节,因为前5个字符是一样的,我们来看看效果:

//初始化为HelloWorld
  char String1[] = "HelloWorld";
  //初始化为HelloChina
  char String2[] = "HelloChina";
  //预期输出0
  int index = memcmp(String1, String2, 5);

image.png

对于以上两种字符串比较函数,这个需要我们去衡量,在什么时候用哪个,如果说我们需要比较两个完整的字符串,可以选择使用strcmp,如果我们比较两个字符串的一部分,那我们可以使用memcmp。


8. 字符串拼接strcat


我们总希望能有改变自己的机会,字符串又何必不是这样呢??它也许也想变得强大,变的无坚不摧,但是苦于没有办法,一直在沉淀自己,终于,等到了strcat函数的到来:

char *
strcat (
      char           *DstString,
      const char     *SrcString)

我们可以看到这个函数的两个参数和strcpy的参数的一样的,第一参数是个char型指针,第二参数是个const char*指针,也就是说第二个参数指向的字符串不允许被修改:

char  *DstString:目标字符串指针,需要拼接到的目标字符串地址
const char  *SrcString:源字符串指针,拼接在DstString之后

返回值是一个指针,指向目标字符串的首地址,我们来看看经过拼接之后,能不能顺利的让字符串完成变身,强大自我呢?

//初始化为Hello
  char String1[] = "Hello";
  //初始化为KookNut39
  char String2[] = "KookNut39";
  //预期最终String1变为HelloKookNut39
  strcat(String1, String2);

image.png

但是!!!必须要说的是,这样做是不对的,这是一个栈区内存,我们本身只有6个字节的空间,现在加上这个字符串之后,很明显栈区内存越界了!!!这是肯定不被允许的。

想要强大自己,这没错,但是得看有没有这个能力,所以我们现在需要让它首先具备强大自己的能力,那就是具有足够的内存空间!!

//动态申请20个字节,并且初始化为0
  char* String1 = (char*)malloc(20);
  memset(String1, 0, 20);
  //修改String1为Hello
  strcpy(String1, "Hello");
  //初始化为KookNut39
  char String2[] = "KookNut39";
  //预期最终String1变为HelloKookNut39
  strcat(String1, String2);

稍作修改,动态申请内存,让我们拥有足够的内存空间来存放加上来的字符串,结果如下:

image.png


9. 字符串中搜索字符strchr


字符串中搜索特定字符也是我们也许会用到的小功能,其实这个函数相比前面的那些函数来说,也简单很多,我们先来看一下这个函数的声明:

char *
strchr (
      const char    *String,
      int           ch)

对于其中的两个参数和返回值,我们这么理解它:

返回值,当字符ch与源串String第一次匹配的时候的地址,如果串中所有字符都不匹配,那么返回NULL地址
const char    *String:源串,匹配的来源
int           ch:目标字符,用在再源串中匹配的

我们写一个简单的小例子来结束这个函数的讲解,我们涉及到了匹配成功和不成功两种情况:

char String1[] = "KookNut39";
  //字符串中没有 Y 预期返回NULL
  char* ptr1 = strchr(String1, 'Y');
  //预期返回 N 第一次出现的地址
  char* ptr2 = strchr(String1, 'N');

看一下运行结果:

image.png


10. 字符串中搜索子串strstr


字符串中可以搜索匹配指定字符?那如果我要搜索一个字符串,有没有什么办法来实现呢?当然有啦,直接来一个strstr就可以找到字符串中子串出现的位置啦:

char *
strstr (
      const char     *String1,
      const char     *String2)

我们来看一下这个字符串中这些参数的具体意思:

返回值:如果String2是String1的子串,那么返回第一次匹配的地址,否则返回NULL
const char     *String1:目标字符串
const char     *String2:需要在目标串中寻找的子串

我们还是一样,在这里进行能匹配的子串和不能匹配的子串进行两次测试,看一下返回值是不是符合我们的预期:

//初始化为HelloKookNut39
  char String1[] = "HelloKookNut39";
  //寻找我的名字KookNut39
  char String2[] = "KookNut39";
  //预期返回第一次匹配到的地址
  char* ptr1 = strstr(String1, String2);
  //预期返回NULL
  ptr1 = strstr(String1, "Kt39");
  return 0;

image.png

行文至此,终于结束了有关常用字符串操作函数的介绍,授人以鱼不如授人以渔,这句话我觉得始终是没问题的,在这里我希望通过给大家介绍简单的使用例子,能够让大家在以后的学习日子里,对于字符串操作可以游刃有余!!如果这篇博文能够给您的学习带来帮助,麻烦您赏博主一个点赞+评论+收藏,给博主继续创作提供动力,谢谢!


今日份与君共勉:“待到秋来九月八,我花开后百花杀”


目录
相关文章
|
18天前
|
编译器 C语言 C++
C语言,C++编程软件比较(推荐的编程软件)
C语言,C++编程软件比较(推荐的编程软件)
|
1月前
|
C语言 C++ 数据格式
【C++对于C语言的扩充】C++与C语言的联系,命名空间、C++中的输入输出以及缺省参数
【C++对于C语言的扩充】C++与C语言的联系,命名空间、C++中的输入输出以及缺省参数
|
7天前
|
C语言
C语言中 字符串和数字的相互转换
C语言中 字符串和数字的相互转换
12 1
|
14天前
|
编解码 JavaScript 前端开发
【专栏】介绍了字符串Base64编解码的基本原理和在Java、Python、C++、JavaScript及Go等编程语言中的实现示例
【4月更文挑战第29天】本文介绍了字符串Base64编解码的基本原理和在Java、Python、C++、JavaScript及Go等编程语言中的实现示例。Base64编码将24位二进制数据转换为32位可打印字符,用“=”作填充。文中展示了各语言的编码解码代码,帮助开发者理解并应用于实际项目。
|
18天前
|
存储 编译器 C语言
C++字符串大小写之for语句
C++字符串大小写之for语句
18 0
|
18天前
|
C语言 C++
C语言利用ASCII码表统计字符串每个字符出现的次数
C语言利用ASCII码表统计字符串每个字符出现的次数
16 0
|
20天前
|
存储 C语言
C语言中字符串的引用与数组元素操作
C语言中字符串的引用与数组元素操作
21 0
|
20天前
|
C++
【代码片段】【C++】获取当前时间戳并生成固定格式字符串
【代码片段】【C++】获取当前时间戳并生成固定格式字符串
15 0
|
20天前
|
安全 C语言
指针与字符串:C语言中的深入探索
指针与字符串:C语言中的深入探索
15 0
|
存储 编译器 Linux
标准库中的string类(中)+仅仅反转字母+字符串中的第一个唯一字符+字符串相加——“C++”“Leetcode每日一题”
标准库中的string类(中)+仅仅反转字母+字符串中的第一个唯一字符+字符串相加——“C++”“Leetcode每日一题”