目录
- 前言
- [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会发生什么呢?如下:
拷贝将不会有停下的动作,一直进行拷贝,甚至会越界往后进行修改。
注意:
- 在拷贝时,我们的目标空间必须足够大,只有足够大,才能放下从源头拷贝过来的数据
- 目标空间必须可修改,如图:
这里的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的原理:
- 找到目标空间中的第一个\0
- 然后从这个\0的位置开始追加源头字符串
- 源头字符串的内容,包括\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再见!如果文章内容有误,请大佬在评论区斧正!谢谢大家!