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

大家好,我是KookNut39,在CSDN写文,分享一些自己认为在学习过程中比较重要的东西,致力于帮助初学者入门,希望可以帮助你进步。最近在更新C/C++方面的知识,感兴趣的欢迎关注博主,可以去专栏查看之前的文章,希望未来能和大家共同探讨技术。

对于学习C语言或者C++的同学,如果对字符串的操作不熟悉,那就无法在学习的过程中更进一步,因为这是非常基础,也是非常重要的一个环节,希望博主这篇文章中的例子,可以教你去搞定字符串操作!对于初学者来说,一下要记住这么多东西是很难的,所以【建议收藏】,方便以后用到的时候查找。
新学习了制作动图,以前结果演示都是干巴巴的一张截图,今天新插了动图,感觉很爽,hhhhhhhhhhhh。喜欢文章的话,一键三连支持博主欧🤞🤞🤞!!!

文章目录

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

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

那么我们应该如何去搞定这些字符串操作的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

对于栈区内存初始化,我们看一下这个图:
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》
对于堆区内存初始化,我们来看一下初始化执行的效果:
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》

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;

计算这个字符串的结果:
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》
从上图中我们应该能看到两个问题,首先我们默认初始化的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);

再来看一下拷贝字符串的效果:
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》
在使用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来达到这个效果:
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》
但是这个函数就不会像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来解决这个问题,没想到。。。它居然行了??也就是说正确输出了结果:
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》
我都无语了,没想到微软居然把这个问题解决了,而我浑然不知!!!但是,但是,我肯定不甘心,我得告诉大家它之前是怎么实现的:

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);
}

这就是它原本的实现逻辑,而按照这个函数的实现逻辑,我们再来测试一番:
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》
OK!!nice!达到我要的错误示范的效果了,而且,它确实之前就是这么实现的!
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》

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

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

对于这个函数的三个参数我们就不解释了,因为这三个参数和memcpy的三个参数是一样的,所以我们直接来看使用举例以及效果图吧:
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》
这里也给大家看一下这个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:
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》

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);

《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》
对于以上两种字符串比较函数,这个需要我们去衡量,在什么时候用哪个,如果说我们需要比较两个完整的字符串,可以选择使用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);

《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》
但是!!!必须要说的是,这样做是不对的,这是一个栈区内存,我们本身只有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);

稍作修改,动态申请内存,让我们拥有足够的内存空间来存放加上来的字符串,结果如下:
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》

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');

看一下运行结果:
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》

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;

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

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

今日份与君共勉:“待到秋来九月八,我花开后百花杀”
《不会这些字符串操作,你怎么精通C语言?如何玩转C++?》

    原文作者:KookNut39
    原文地址: https://blog.csdn.net/qq_42253797/article/details/117569120
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞