部分字符串函数的简单模拟实现
本文介绍了C语言中7个常用字符串处理函数的模拟实现方法:strlen(三种实现方式)、strcpy、strcat、strcmp、strstr、strncpy和strncat。每个函数都先通过Cplusplus网站查询其标准语义,再用示例代码演示功能,最后给出模拟实现代码及详细注释。特别对较复杂的strstr函数进行了重点分析,展示了字符串查找的实现思路。文章强调在实际编程中应注重边界条件处理,建议
一、简介
我们要自己模拟实现函数首先要了解它的基本语义。Cplusplus这个网站可以帮助我们找到想实现的函数的基本语义,让我们更容易去模拟实现函数功能。以下模拟实现不出现超出缓冲区,源字符串和目标字符串不重叠。
今天主要分享strlen、strcpy、strcat、strcmp、strstr、strncpy、strncat。
二、模拟实现
1、strlen
在Cplusplus上可查到strlen的相关信息(后面截图均来自该网站),如下:

可以知道它的参数是一个指针并以其指向的地址作为起始地址,以空字符'\0'为结束地址。它可以得到字符串的长度,长度等于字符串开头和终止空字符之间的字符数量(不包括终止空字符本身)。我们用最简单的一段代码来演示它如何使用的:
#include<stdio.h>
#include<string.h>//使用字符串函数所需要包含的头文件
int main()
{
char a[10] = "Hello C";//定义一个长7的字符串
size_t ret = strlen(a);//定义一个与返回值类型相同的变量
printf("%zd", ret);
return 0;
}
代码走起来,结果是在屏幕上打印7(包含一个空格)。当strlen读完C后向后找一位发现是'\0',于是停止查找返回已读取的字节数(char类型字符长一个字节)。基本信息了解后可以开始自己模拟实现了!我将给出三种实现方案:计数器实现、递归实现、指针实现(只给出函数部分的代码);
1.计数器实现:额外设置一个计数器变量,代码清晰易理解。
//计数器模拟
size_t my_strlen(const char* s)
{
int count = 0;//定义计数器变量
while (*s != '\0')//判断是否到字符串结尾
{
count++;//若不是则长度加1
s++;//且指针向后移
}
//当指针指向空字符,跳出循环
return count;//最后返回计数器的值,即为字符串的长度
}
2.递归实现:使用递归算法,代码更加简洁
//递归
size_t my_strlen(const char* s)
{
if (*s == '\0')//判断是否为空字符,为空时则跳出递归
{
return 0;
}
else//如果不是则递归调用
{
return 1 + my_strlen(s + 1);//提取1后,指针向后移一位再传参给my_strlen
}
}
3.指针实现:定义新指针,移动新指针遍历字符串,到结尾再与参数指针(首字符地址)作差
//指针运算
size_t my_strlen(const char* s)
{
const char* p =s;//定义新指针
while (*p != '\0')//判断是否遍历到空字符
{
p++;//没有则指针向后移
}
//找到则跳出循环
return p - s;//两指针作差后的值就是字符串长度
}
这是三种实现strlen的方法,第一种相对简单,后两种可能需要画图才能更好地理解!一定要尝试自己画图和敲代码!!!
2、strcpy
相关信息如下:

可以简单了解strcpy函数是以两个字符型指针为参数,字符型指针为返回类型的。它的作用是将一个字符串拷贝到另一个字符串。什么时候停止拷贝呢?当拷贝到源字符串的'\0'的时候就会停下来。用简单代码演示一下它的功能。
#include<stdio.h>
#include<string.h>
int main()
{
char a[10] = {0};
char b[10] = "Hello";
strcpy(a, b);//将字符串b拷贝入a
printf("%s", ret);//%s占位符用于打印字符串,遇到'\0'停止打印
return 0;
}
程序走起来,会在屏幕上打印Hello(即拷贝后的a)。那我们怎么知道它有没有把空字符'\0'拷贝过来呢?调试过程的监视功能可以很好的帮助我们。我们现将a用字符'x'初始化,然后再在strcpy函数那行打上断点(调试时程序走到断点会停下),再开始调试,调试时启用监视功能观察a内的变化:

可以看到拷贝前a的前9位为'\0',f11往下走一行,可以看到拷贝后的a的变化如下:

可以看到'\0'也被拷贝过来了,当然我通过%s的打印结果也可以推断出'\0'被拷贝过来了。那我们模拟实现时除了要实现各字符的拷贝,也得保证'\0'的拷贝。
char* my_strcpy(char* dest,const char* src)
{
assert(dest && src);//断言判断是否为野指针,需包含头文件<assert.h>
char* ret = dest;//目标字符串首地址
while (*src != '\0')
{
*dest++ = *src++;//赋值'\0'前的字符
}
//遍历到源字符串'\0'时跳出循环,再单独把'\0'赋值给目标字符串
*dest = *src;
return ret;//返回目标字符串地址
}
我的模拟函数里面采用while循环遍历源字符串,以是否为空字符为判断条件。若未遍历到'\0',则将当前地址解引用并赋值给目标字符串的相应位置,赋值后两个指针都向后移一位(遍历下一个字符);当遍历到空字符则跳出循环,再单独赋值一次。while循环可以写的更简洁,不过比这个理解起来麻烦一点点,这里就不展示了。
3、strcat

strcat也是以两个字符型指针为参数,字符型指针为返回类型的函数,不过功能不同于strcpy。strcat将源字符串的副本追加到目标字符串。目标字符串中的终止空字符将被源字符串的第一个字符覆盖,并且在目标字符串中由两者连接形成的新字符串的末尾包含一个空字符。我们的目标空间必须足够大,且末尾要有'\0'。下面简单演示它
#include<stdio.h>
#include<string.h>
#include<assert.h>
int main()
{
char a[20] = "Hello ";
char b[20] = "world";
strcat(a, b);
printf("%s", a);
return 0;
}
代码走起来,会在屏幕上打印Hello world。根据其语义我们要实现两个方面的功能,先是找到目标字符串的'\0',再拷贝。
char* my_strcat(char* dest, const char* src)
{
assert(dest != NULL);
assert(src != NULL);
char* p = dest;//存储目标字符串首地址
//找到'\0'
while (*dest != '\0')
{
dest++;
}
//拷贝
while (*src != '\0')
{
*dest++ = *src++;
}
*dest = *src;
return p;//返回连接后的目标字符串首地址
}
我们首先断言判断两个指针是否为野指针,再用一个循环找到目标字符串的空字符,然后再用一个循环进行拷贝操作(和strcpy相似),最后返回连接后的目标字符串首地址。
4、strcmp

strcat是以两个字符型指针为参数,整数类型为返回类型的函数。用于比较两个字符串长度,是逐字符比较他们的ASCII码值。如果第一个字符串小于第二个字符串,则返回值<0;如果相同,则返回值为0;如果大于,则返回值>0。用一段代码简单演示一下它的功能:
#include<stdio.h>
#include<string.h>
int main()
{
char a[20] = "abd";
char b[20] = "abcd";
int ret = strcmp(a, b);
printf("%d", ret);//通过打印的值判断字符串大小
return 0;
}
程序运行后会在屏幕上打印1,这是由于字符'd'比字符'c'的ASCII码值更大,所以第一个字符串更大,所以返回一个大于0的值。那我们模拟实现就要逐字符进行比较,最后返回出一个整数值。
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 && str2);//断言判断是否为野指针
while (*str1 == *str2)//比较
{
if (*str1 == '\0')
return 0;
//若相同则两指针指针向后移一位
str1++;
str2++;
}
//不相同则跳出循环进行比较
if (*str1 > *str2)
return 1;
else
return -1;
}
// "abc" vs "abc" -> 两边都是 '\0',差值为 0
// "abc" vs "abcd" -> '\0' - 'd' < 0
// "abcd" vs "abc" -> 'd' - '\0' > 0
最下面的注释给了三种情况。当然跳出循环后也可以直接返回两个值的差值,只是不会是固定的正负一了,如果大于就是正数,小于则为负数。
5、strstr

strstr应该是这里最复杂的一个函数了,它的功能是在一个字符串里找另一个字符串并返回str2 在 str1 中第一次出现的指针,如果 str2 不是 str1 的一部分,则返回空指针。它也是以两个字符型指针为参数,字符型指针为返回类型的函数。简单演示其作用,再直接配合模拟函数的代码分析。

可以看到找到的话则从返回首次匹配的的字符的地址,否则则返回空指针。下面来看模拟实现该函数的代码及详细注释
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2); // 断言:两个入参指针都不能为空
if (*str2 == '\0') // 如果要查找的子串是空串 ""
return (char*)str1; // 按 strstr 约定:返回原串首地址(强转为 char* 以匹配返回类型)
const char* s1 = NULL; // 用于从 str1 的某个起点开始向后比较的临时指针
const char* s2 = NULL; // 用于遍历 str2 进行匹配的临时指针
const char* cur = str1; // cur 表示当前在 str1 中尝试匹配的起始位置(从 str1 开头开始)
while (*cur) // 只要 cur 没到达 '\0',就还有可能作为匹配起点
{
s1 = cur; // 让 s1 从当前起点 cur 开始和 str2 比较
s2 = str2; // 每次尝试匹配时,s2 都从 str2 开头开始
while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2) // 两边都没结束且当前字符相等,就继续向后比
{
s1++; // str1 的比较指针后移一个字符
s2++; // str2 的比较指针后移一个字符
}
if (*s2 == '\0') // 如果 s2 走到了 '\0',说明 str2 已经全部匹配完成
return (char*)cur; // 返回本次匹配的起始位置 cur(即第一次出现的位置)
cur++; // 本次起点不匹配:起点后移一个字符,继续尝试下一位置
}
return NULL; // 扫描完 str1 仍未找到 str2,返回 NULL
}
这是在我原有的注释的基础上用ChatGpt润色的,还是比较清晰易懂,那我们来分析一下思路。
首先断言判断入参指针不能为空,为空就中止;
然后根据strstr语义,如果要查找的子串为空则返回原串首地址(由于入参时是const char*类型,所以需要强制转换再返回);
再定义三个指针,为后面的查找做准备(各自作用见注释);
循环是实现该函数的关键,我们用指针cur(每次匹配指向匹配起点)所指向的内容为判断条件,只要不为空,就可以继续匹配;
循环体内的小循环就类似于strcmp了,相同且都不为空字符临时指针就向后移一位继续匹配,否则跳出循环;
跳出循环后判断是否是因为s2遍历完了,若是则匹配成功返回cur(即此次匹配的初始位置),若不是则cur向后移以下一个字符作为匹配起点以此循环;
如果cur指向空字符说明扫描完 str1 仍未找到 str2,返回 NULL。
这就是这个函数的模拟思路,为了看得更清晰我每一步另起一行,能搞懂三个新定义的指针的作用理解为起来就比较简单了。不懂的可以画一画图
6、strncpy和strncat


strncpy和strncat就是strcpy和strcat的升级版。我们看到它们两个都多了一个size_t类型的参数,它是用来控制长度的,拷贝几个或者连接几个都由这个参数做决定。当然语义上也有所不同,比如strncpy的第三个参数大于源字符串的长度则会在目标字符串后面补足够多的空字符'\0',直到等于第三个参数。strncat不管连接几个字符都会在后面补一个空字符。这种小区别不展开多说,用多了就会知道的,这两个用起来就会更加的安全,因为可以控制长度。下面我直接给出这两个函数的模拟实现代码,基本思路不变只增加了关于新参数的使用
//模拟strncpy
char* my_strncpy(char* dest, const char* src, size_t n)
{
assert(dest != NULL);
assert(src != NULL);
char* ret = dest;
while (n > 0 && *src != '\0')
{
*dest++ = *src++;
n--;
}
while (n > 0)
{
*dest++ = '\0';
n--;
}
return ret;
}
//模拟strncat
char* my_strncat(char* dest, const char* src,size_t n)
{
assert(dest != NULL);
assert(src != NULL);
char* p = dest;
while (*dest != '\0')
{
dest++;
}
while (n > 0 && *src != '\0')
{
*dest++ = *src++;//赋值'\0'前的字符
n--;
}
*dest++ = '\0';
return p;
}
新参数使得这两个函数更加的安全,对于理解也没有太大阻碍还是很好看懂的。
三、结语
关于这几个库函数我只是在理想条件下进行的简单模拟实现,所以还可能有考虑不到位的地方,请见谅。如果这篇文章对你有帮助,不如一键三连,方便找到,实在感谢!!!欢迎各位在评论区交流分享,下篇文章再见!
更多推荐



所有评论(0)