字符串函数(2)

目录

  • 前言
  • [1. strlen](#1. strlen)
    • [1.1 strlen函数的理解和使用](#1.1 strlen函数的理解和使用)
    • [1.2 strlen函数的模拟实现](#1.2 strlen函数的模拟实现)
  • [2. strcpy](#2. strcpy)
    • [2.1 strcpy函数的理解和使用](#2.1 strcpy函数的理解和使用)
    • [2.2 strcpy函数的模拟实现](#2.2 strcpy函数的模拟实现)
  • 3.strcat
    • [3.1 strcat函数的理解和使用](#3.1 strcat函数的理解和使用)
    • [3.2 strcat 函数的模拟实现](#3.2 strcat 函数的模拟实现)

前言

在上一篇文章中,我们对字符分类函数和字符转换函数进行了学习,部分小伙伴们可能会想字符串函数在哪里呢?其实是我没有写完啦!在本篇文章中,我会对该内容进行详细讲解。

1. strlen

1.1 strlen函数的理解和使用

字符串函数的使用需要包含头文件string.h。strlen的返回值的类型是size_t(无符号整型)。strlen函数是求字符串的长度的。

例如:

工作原理是:

当我们有一个字符串时,里面除了我们没看到的以外,还存放着\0。我们给strlen函数传了数组名arr。数组名是数组首元素的地址,strlen从给定的起始位置开始,向后统计\0之前字符的个数。

注意:

一定是从起始位置开始,我们现在可以测试一下别的写法。

arr是数组名,代表首元素的地址,如果传arr,则从a开始数。如果传arr+1,则跳过一个元素,从b开始数。

前面我们说过strlen的返回值的类型是size_t类型的,这一部分我们需要加深理解,现在我们举一个例子进行理解。

按照我们的想法来看,strlen(arr2)和strlen(arr1)的输出结果分别为3和6,3减6的值等于-3,应该打印<=。但是运行的结果并不是我们所想,因为strlen的返回值的类型是size_t , size_t类型的值相减也是size_t,此时计算出的-3会被作为一个无符号的整型来处理,此时的-3就没有了符号位。

在内存中存放着-3的补码,当-3为无符号整型时,最前面的1就不是符号位了,此时就不存在原反补了。计算的结果将是一个非常大的正数,所以输出的结果是>。我们现在对代码进行修改。

c 复制代码
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[] = "abcdef";
	char arr2[] = "abc";

	if (strlen(arr2) > strlen(arr1) )
		printf(">\n");
	else
		printf("<=\n");

	return 0;
}

或者:

c 复制代码
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[] = "abcdef";
	char arr2[] = "abc";

	if ((int)strlen(arr2) - (int)strlen(arr1) > 0)
		printf(">\n");
	else
		printf("<=\n");

	return 0;
}

这两个代码都能实现我们原来的目的。

1.2 strlen函数的模拟实现

我们采用递归的方式进行实现。

现在我们有一个字符数组里面存放着abcdef。现在我们需要写一个自己的函数来计算字符串的长度。仿照着strlen把arr传过去,并让其返回一个size_t的值,当然int也行,只是字符串的长度不会为负值,选择size_t比较合适。

函数的参数部分需要接受arr(即首元素的地址),我们采用char*类型的str进行接收,同时我们不希望str去改变字符串,使用可以使用const来修饰。

递归的思想是大化小,我们把字符串中的第一个字符拿出来,不是\0说明长度至少是1,1加上后面的长度就是总长度,按照这个原理我们不断的向后拿取,如图

代码:

c 复制代码
#include<stdio.h>
size_t my_strlen(const char* str)
{
	if (*str == '\0')
		return 0;
	else
		return 1 + my_strlen(str + 1);
}

int main()
{
	char arr[] = "abcdef";
	size_t len = my_strlen(arr);
	printf("%zd\n", len);
	return 0;
}

运行结果:

现在可能还有部分小伙伴没有理解,我们可以缩短字符串的长度(改为abc)来理解:

我们要计算abc的长度,此处我们调用了自己写的函数来完成。我们将a的地址传了过去,给了str,此时str向后就可以看到a b c \0。

刚开始时,* sr 为a,a并不等于\0,我们就走else这条路,然后再对my_strlen进行调用。第二次调用时,这里的str指向b,b不等于\0,走else的路线,这里我们还需要对my_strlen进行调用,第三次调用时,这里的str指向c,c不等于\0,走else的路线,最后一次调用my_strlen时,str指向了\0。此时我们的递推就结束了,将开始回归。1就会不断的相加到3,最后返回去,此时len就为3.

注意:

在这里的 1 + my_strlen(str + 1)没有返回时,是不会计算的。

代码:

c 复制代码
#include<stdio.h>
size_t my_strlen(const char* str)
{
	if (*str == '\0')
		return 0;
	else
		return 1 + my_strlen(str + 1);
}

int main()
{
	char arr[] = "abc";
	size_t len = my_strlen(arr);
	printf("%zd\n", len);
	return 0;
}

2. strcpy

2.1 strcpy函数的理解和使用

该函数是用来进行字符串拷贝的。我们现在来看函数的基本情况。

第一个参数的名字是destination(目的地)。第二个参数是source(源头),即将源头的数据拷贝到destination里面去。例如:

理解:

arr1数组里面放了hello world\0 .我们把arr1和arr2分别传给了我们刚才说的source和destination,我们认为它是一个个字符进行拷贝的,把\0拷贝完成就停止。

这里我们可以看看到底有没有将\0拷贝过去,如下:

此时我们可以看到\0也被拷贝过去了。既然我们需要将\0拷贝过去,那么我们源头中的字符串也需要有\0。如果我们去掉\0会发生什么呢?如下:

拷贝将不会有停下的动作,一直进行拷贝,甚至会越界往后进行修改。

注意:

  1. 在拷贝时,我们的目标空间必须足够大,只有足够大,才能放下从源头拷贝过来的数据
  2. 目标空间必须可修改,如图:

    这里的p是常量字符串,不能进行修改。

2.2 strcpy函数的模拟实现

我们首先创建arr1,里面放上abcdef,再创建一个arr2,里面可以放上20个元素。根据刚才学到的strcpy的参数部分,我们也可以将my_strlen中的参数设置为char * dest 和char * src。暂时我们把返回这里写为void。

我们将arr2和arr1传过去之后,dest和src分别指向如下位置:

这里我们需要将字符一个个拷贝过去,即我们需要进行多次拷贝,我们可以写一个while循环,在里面使dest和src++来不停的往后。当\0也拷贝下来时循环结束,所以我们把条件写为 * src! = '\0',此时\0并没有拷贝下来,我们添加 *dest = * src即可(停下来时 * src为\0,我们此时相当于把'\0'拷贝过去)。此时代码就完成了。

代码:

c 复制代码
#include<stdio.h>
void my_strcpy(char* dest, char* src)
{
	while (*src != '\0')
	{
		*dest = *src;
		src++;
		dest++;
	}
	*dest = *src;
}

int main()
{
	char arr1[] = "abcdef";
	char arr2[20] = { 0 };
	my_strcpy(arr2, arr1);

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

	return 0;
}

运行结果:

现在我们进行优化,把代码的改为后置++,用完后再++,然后我们的 * dest++ = * src++和 * dest = * src是有一点重复的,我们需要改为如下写法:

c 复制代码
#include<stdio.h>
void my_strcpy(char* dest, char* src)
{
	while (*dest++ = *src++)
	{
		;
	}
}

int main()
{
	char arr1[] = "abcdef";
	char arr2[20] = { 0 };
	my_strcpy(arr2, arr1);

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

	return 0;
}

理解:

第一次 * src是a,我们将a赋值过去,此时整个表达式的值就为a,a的ASCII码值不为0,为真,进入循环,因为赋值完成以后进行++,则下一次 * src拿到的值就是b,然后再进行该过程,直到 * src指向\0,将\0赋值过去,此时表达式的结果为\0,\0的ASCII码为0,0为假,跳出循环。我们会发现这里我们既可以赋值,赋值产生的值有可以进行判断。

我们还可以对代码进行调整,我们的目的是将src指向的内容拷贝到dest指向的空间里面去,我们不希望src指向的空间被修改。这里可以加const进行修饰,但是解引用之前我们害怕为空指针,我们可以利用assert断言进行处理。

代码:

c 复制代码
#include<stdio.h>
#include<assert.h>
void my_strcpy(char* dest, const char* src)
{
	assert(dest && src);
	while (*dest++ = *src++)
	{
		;
	}
}

int main()
{
	char arr1[] = "abcdef";
	char arr2[20] = { 0 };
	my_strcpy(arr2, arr1);

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

	return 0;
}

但是strcpy的返回值的类型是char*,与我们自己写的函数是不同的,那使用char * 有什么好处吗?

函数是将原字符串拷贝放到目标空间里面去,我们是期望目标空间发生变化来进行观察。所以函数最终返回目标空间的起始位置。但是我们这里不能直接return dest。因为这里dest不断地++以后,dest指向的不再是起始地址,解决方式是我们创建一个 char * 的ret把未进行改动的dest存一份,最后return ret。

代码:

c 复制代码
#include<stdio.h>
#include<assert.h>
char* my_strcpy(char* dest, const char* src)
{
	char* ret = dest;
	assert(dest && src);
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}

int main()
{
	char arr1[] = "abcdef";
	char arr2[20] = { 0 };
	char* ret = my_strcpy(arr2, arr1);

	printf("%s\n",ret);

	return 0;
}

运行结果:

此时我们的函数更加灵活,my_strcpy的返回值也可以作为其他函数的参数,实现链式访问。

代码:

c 复制代码
#include<stdio.h>
#include<assert.h>
#include<string.h>
char* my_strcpy(char* dest, const char* src)
{
	char* ret = dest;
	assert(dest && src);
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}

int main()
{
	char arr1[] = "abcdef";
	char arr2[20] = { 0 };
	/*char* ret = my_strcpy(arr2, arr1);*/
	size_t len = strlen(my_strcpy(arr2, arr1));
	printf("%zd\n",len);

	return 0;
}

运行结果:

3.strcat

3.1 strcat函数的理解和使用

该函数的作用是字符串的追加,我们直接上实例进行理解:

我们现在有一个数组arr1,里面存放着hello。我们想在后面追加world。可能有同学会想用strcpy,但是它会将hello给覆盖。所以以我们会使用strcat这个函数。

现在我们研究一下这个函数:

我们不难发现这与我们刚才看到的strcpy的参数是一样的,第一个参数是char * destination,是我们需要追加的对象,第二个参数是const char * source,是需要追加的内容。这个函数的返回值的路线是char *,返回的是目标空间的起始地址(与我们之前讲的strcpy一样)。

我们现在看看是如何追加的:

这里是在第一个\0的位置开始追加world,我们不免会想这里我们是将world的\0追加过来了,还是原来在arr1中的?我们可以测试看看,我们可以在原arr1中添加一个\0并观察试试。

两张图对比我们可以发现,world从hello后面的\0开始追加,world还把它原来的\0也带了过来。

strcat的原理:

  1. 找到目标空间中的第一个\0
  2. 然后从这个\0的位置开始追加源头字符串
  3. 源头字符串的内容,包括\0都会追加到目标空间

注意:

在追加时目标空间的大小要足够长,能够放下我们要追加的数据。其次目标空间要可修改,以便追加数据。因为会将源头字符串字符串的\0追加过去,所以我们的源头字符串要有\0。目标字符串也要有\0,不然不知道从哪里开始追加。

3.2 strcat 函数的模拟实现

我们之前观察过它的参数和返回类型,这些部分是和strcpy一样的。我们就可以仿照来写代码。

我们首先需要在目标空间中找到\0再进行追加,这里我们可以利用while循环往后找,如果 * dest不等于\0,我们就让dest++,直到dest指向\0跳出循环。

之后我们需要将world这个数据拷贝过去 ,这个拷贝数据的代码我们观察已经学习过了,我们可以照搬过来。刚才dest已经指向了\0,现在我们进行拷贝就会将\0覆盖。

接下来我们要返回char * 的数据,即返回目标空间的起始地址,但是我们的dest通过++已经走很远了,我们可以仿照刚才的ret来存放目标空间的起始地址,最后返回ret。当然如果担心我们的指针有效性也可以采用assert断言一下。

代码:

c 复制代码
#include<stdio.h>
#include<assert.h>
char* my_strcat(char* dest, const char* src)
{
	char* ret = dest;
	assert(dest && src);
	while (*dest != '\0')
		dest++;
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}

int main()
{
	char arr1[20] = "hello ";
	my_strcat(arr1, "world");
	printf("%s\n", arr1);
	return 0;
}

运行结果:

思考:

我们能不能自己给自己追加?

进行传参以后我们的dest和src指向的位置为:

程序开始运行以后我们的dest会一直往后找,直到找到\0,之后开始拷贝,此时h就会将原来的\0覆盖,没有\0以后拷贝的这个循环是停不下来的,就会造成死循环的问题。

所以我们通常是不会用这个函数进行对自己追加的操作的。strcat在进行自己给自己追加时会有一定的概率会成功,我们如果想要自己给自己追加可以使用strncat。

好了今天知识就学习到这里,我们下期blog再见!如果文章内容有误,请大佬在评论区斧正!谢谢大家!

相关推荐
爱吃生蚝的于勒1 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
小白学大数据3 小时前
Python爬虫开发中的分析与方案制定
开发语言·c++·爬虫·python
冰芒猓4 小时前
SpringMVC数据校验、数据格式化处理、国际化设置
开发语言·maven
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
杜杜的man4 小时前
【go从零单排】Closing Channels通道关闭、Range over Channels
开发语言·后端·golang
java小吕布5 小时前
Java中Properties的使用详解
java·开发语言·后端
versatile_zpc5 小时前
C++初阶:类和对象(上)
开发语言·c++
尘浮生5 小时前
Java项目实战II基于微信小程序的移动学习平台的设计与实现(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·学习·微信小程序·小程序
ChoSeitaku6 小时前
链表循环及差集相关算法题|判断循环双链表是否对称|两循环单链表合并成循环链表|使双向循环链表有序|单循环链表改双向循环链表|两链表的差集(C)
c语言·算法·链表