妈妈放心系列2:库函数和内存函数的模拟实现(C语言)

目录

库函数

strlen

模拟实现1

模拟实现2

模拟实现3

strcpy

模拟实现

简化

strcat

模拟实现

VS2022独有的功能(其实不太清楚其他版本行不行)

strcmp

模拟实现

strstr

模拟实现1

模拟实现2(KMP算法)

strncpy

strncat

strncmp

strtok

strerror

使用范例

内存函数

memcpy

VS2022独有功能(其实不太清楚其他版本行不行)

模拟实现

memmove

模拟实现

memset

memcmp


本文依然是由浅到深的复习库函数和内存函数的知识。妈妈放心系列确保每位初学者都能看懂并且会应用内存函数和库函数。本文认为模拟实现是重点!!!

库函数

库函数是数据库中的函数,头文件为#include<string.h>,当然不写这个头文件也没有问题,系统会默认你调用的是库里的函数。库函数也是与字符串密切相关的函数

strlen

#include<stdio.h>
#include<string.h>
int main()
{
    char arr[] = { "abcdef"};
    /*char arr[] = { 'a', 'b', 'c' ,'\0' };*/
    /*char arr[] = { 'a', 'b', 'c' };*/
    size_t len = strlen(arr);
    printf("%zd\n", len);
    return 0;
}

strlen是怎么算字符串长度的就不用我讲了吧。并且在Cplusplus.com这个网站中很容易就可以查到strlen的返回类型是无符号整型。所以是size_t,打印也是用的%zd。

模拟实现1

对这个函数的模拟实现主要是设计一个函数my_strlen来统计出计算的个数!

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

先写好主函数部分,很简单相信有一点基础的你一定可以自己完成。

#include<assert.h>
size_t my_strlen(const char* str)
{
  
 /*assert(str != NULL);*/
    assert(str);//断言指针时可以只写一个指针不用!=NULL
    size_t count = 0;
    while (*str)
    {
        str++;
        count++;
    }
    return count;
}

接着只要写出函数部分就大功告成了。传入的数组用str接收。因为字符串后有一个\0所以当str加到指向\0时,*str就等于0了。循环就停止了。这样每次str加1并完成判断后用count计算一次,最后输出的count就是strlen计算的字符串的长度了。

模拟实现2

还可以用指针减指针的方法来模拟实现!!!

size_t my_strlen(const char* str)
{
    /*assert(str != NULL);*/
    assert(str);//断言指针时可以只写一个指针不用!=NULL
    const char* start = str;
    while (*str)
    {
        str++;
    }
    return str - start;//用指针减指针
}

模拟实现3

运行递归的思路是不是更加巧妙呢?

//用递归解答
size_t my_strlen(const char* str)
{
    if (*str == '\0')//\0是一个字符所以要一个‘’
    {
        return 0;
    }
    return 1 + my_strlen(str + 1);
}
//my_strlen("abcdef");
//1+my_strlen("bcdef");
//1+1+my_strlen("cdef");
//1+1+1+my_strlen("def");
//1+1+1+1+my_strlen("ef");
//1+1+1+1+1+my_strlen("f");
//1+1+1+1+1+1+my_strlen("");
//1+1+1+1+1+1+0;

很好理解,因为每次计数都取决于前一个的计算结果,这不就是递归的条件吗?

strcpy

strcpy是字符串拷贝函数,将一个字符串拷贝到另一个空间(覆盖在另一个字符串上面)上面。

int main()
{
    char arr1[] = { "xxxxxxxxxxxx" };
    char arr2[] = { "abc" };
//会把\0拷过去
    strcpy(arr1, arr2);
    printf("%s\n", arr1);
    return 0;
}

接下来看模拟实现。

模拟实现

//模拟实现strcpy函数
void my_strcpy(char* dest, char* src)
{
    while (*src != '\0')
    {
        *dest = *src;
        src++;
        dest++;
    }
    *dest = *src;
}

int main()
{
    char arr1[20] = { 0 };
    char arr2[] = "abcdef";
    my_strcpy(arr1, arr2);
    printf("%s\n", arr1);
    return 0;
}

这个思路有类似于双指针的思路,用两个指针直接历遍两个数组,每次历遍都交换一次直到要拷贝的数组所对应的指针遇到\0。

既然思路这么简单,我们就想一想可不可以进行代码的简化。

简化

实际上while循环里的内容是有点啰嗦的,可以如下简化:

char* my_strcpy(char* dest, char* src)//由Cplusplus.com网站查询可知库函数和内存函数的返回类型都是void*


{
  
 /*assert(dest != NULL);
    assert(src != NULL);*/

    char* ret = dest;
    assert(dest && src);
  
 while (*dest++ = *src++)
    {
        ;
    }

    return ret;
}

int main()
{
    char arr1[20] = { 0 };
    char arr2[] = "abcdef";
    printf("%s\n", my_strcpy(arr1, arr2));

    return 0;
}

这样改是不是非常巧妙,对比++,*绝对是先运算的。while循环里没有语句就写一个;就行

strcat

strcat是字符串移接函数,在一个字符串后追加一个字符串。一般是不能自己追加自己,但是在VS2022可以实现自己追加自己。

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

模拟实现

char* my_strcat(char* dest, const char* src)
{
    char* ret = dest;
    assert(dest && src);
 
   //1. 找到目标空间的\0
    while (*dest)
    {
        dest++;
    }

    //2. 拷贝
    while (*dest++ = *src++)
    {
        ;
    }

    return ret;
}

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

    return 0;
}

由于思路和strcpy差不多,所以这里就不再作解释了。

VS2022独有的功能(其实不太清楚其他版本行不行)

下面演示VS2022独有功能:strcat自己追加自己:

int main()
{
    char arr1[20] = "hello ";
    strcat(arr1, arr1);
    printf("%s\n", arr1);

    return 0;
}

strcmp

strcmp是字符串长度比较函数比较的是每位字母的ASCLL码值,strcmp(arr1, arr2)对于strcmp的两个参数,如果arr1等于arr2,就返回0,arr1大于arr2,就返回大于0(一般是1),反之则返回小于0(一般为-1)。根据这个特性,请看模拟实现。

int main()
{
    int ret = strcmp("abq", "abcdef");
    if (ret > 0)
        printf("大于\n");
    else if (ret == 0)
        printf("等于\n");
    else
        printf("小于\n");

    return 0;
}

模拟实现

int my_strcmp(char* p, char* t)
{
    while (*p == *t)//以等于为突破口
    {
        if (*p == '\0')
        {
            return 0;
        }
        p++;
        t++;
    }
    return *p - *t;//比的是askll码
}

int main()
{
    int ret = my_strcmp("abcdef", "abc");
    if (ret > 0)
        printf("大于\n");
    else if (ret == 0)
        printf("等于\n");
    else
        printf("小于\n");
    return 0;
}

这个的模拟实现会比较难一点,也是用两个指针同时同步来遍历整个数组,如果有一个地方不相等就可以比较出两个字符串的大小,如果相等就进入循环如果历遍到\0了还是相等的话,就说明两个字符串不仅相等里面的元素还是一样的。

strstr

strstr 返回字符串在另外一个字符中第一次出现的位置,就是从那个要找字符串的字符串开始打印原字符串
由于需要判断找不找得到这个字符串,所以返回类型为指针,如果strstr返回一个空指针,就说明找不到了。反之就是找到了。

int main()
{
    char arr1[] = "abcdefabcdef";
    char arr2[] = "def";
//p指到d的位置,从d开始打印,相当于把def提前了
    char* p = strstr(arr1, arr2);
    if (p != NULL)
    {
        printf("%s\n", p);
    }
    else
    {
        printf("找不到");
    }
    return 0;
}

模拟实现1

strstr的模拟实现比较困难的点是要找到字符串和原字符串的类型很多。我们使用暴力求解的方法来模拟实现

//暴力求解
char* my_strstr(const char* str1, const char* str2)
{
    const char* cur = str1;
    const char* s1 = NULL;
    const char* s2 = NULL;

    assert(str1 && str2);//这边说一下,空字符串数组并不等于空指针,所以这一行的断言和下一行if对str2的判断没有冲突。NULL就是对数字0进行了强制类型转换。


    if (*str2 == '\0')//如果要找的数组第一个元素就是\0的话,就不用找了,没有元素无法查找,就返回原数组
    {
        return (char*)str1;//不变动str1的好处
    }

    while (*cur)
    {
        s1 = cur;//先存储cur每次加加后的地址,防止后续的操作使cur发生变动
        s2 = str2;
        while (*s1 && *s2 && *s1 == *s2)//任何有一个条件不成立都会跳出循环的
        {
            s1++;
            s2++;
        }
        if (*s2 == '\0')//如果历遍完s2,这是由于while循环里变动的是s1和s2所以cur还是在原本相等前面的位置,这是返回cur就可以了。
        {
            return (char*)cur;
        }
        cur++;//变动cur完成历遍,找到需要找的数组s2
    }
    return NULL;//两个指针都历遍完了
}

int main()
{
    char arr1[] = "abcdef";
    char arr2[] = "abcdef";
    char* ret = my_strstr(arr1, arr2);
    if (ret != NULL)
        printf("%s\n", ret);
    else
        printf("找不到\n");

    return 0;
}

//测试用例1
//abcdef
//cde

//测试用例2
//abcdef
//cbq

//测试用例3
//abbbcdef
//bbc

//测试用例4
//abcdef
//abcdef

//测试用例5
//abcdef
//\0

上面的这种方法写起来比较繁琐,但是相对使用KMP算法比较容易理解,可以自己去学习KMP算法是怎么实现strstr的模拟实现的。

模拟实现2(KMP算法)

strncpy

strncpy加了一个n的含金量就是可以限定字符个数拷贝。

int main()
{
    char arr1[20] = "xxxxxxxxxx";
    char arr2[] = "hello";
    strncpy(arr1, arr2, 3);
//尽可能的不要越界
    printf("%s", arr1);
    return 0;
}

strncpy的模拟实现和strcpy的差不多,可以自己试试,这里就不写了!!!

strncat

strncat加了一个n的含金量就是可以限定字符个数的追加。

int main()
{
    char arr1[20] = "hello\0xxxxxxxxxxx";
    char arr2[] = "world";
//从\0开始接
    strncat(arr1, arr2, 7);
    printf("%s", arr1);
    return 0;
}

strncat的模拟实现和strcat的差不多,可以自己试试,这里就不写了!!!

strncmp

strncmp加了一个n的含金量就是可以限定字符个数的比较。

int main()
{
    char arr1[] = "abcdf";
    char arr2[] = "abx";
    int ret = strncmp(arr1, arr2, 2);
//只比较两个字符
    printf("%d", ret);
    return 0;
}

strncmp的模拟实现和strcmp的差不多,可以自己试试,这里就不写了!!!

接下来介绍两个看起来没什么用的两个库函数(选择性理解)

strtok

#include<stdio.h>
#include<string.h>
//strtok是分割字符串函数
int main()
{
    char arr[] = "zhangsan@163.com#hehe";
    char arr2[30] = {0}; //zhangsan\0163\0com
    strcpy(arr2, arr);
    printf("%s\n", arr);
    const char* p = "@.#";
    char* s = NULL;//这个是初始化,s是每次分完的首元素地址
    for (s = strtok(arr2, p); s != NULL; s = strtok(NULL, p))
    {
        printf("%s\n", s);//第2次第一个参数变成NULL
    }
    return 0;
}

strtok会去找特殊符号在字符串的位置,直到\0为止,s = strtok(NULL, p)如果是还要找下一个特殊字符,那就是将strtok的第一个参数置成空指针(这个是语法知识不要问为什么)。

strerror

strerror是显示错误的函数,能将错误信息装入strerror里,再打印出来,每个数字编号都表示一种错误信息。

//strerror是显示错误的函数
int main()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d: %s\n", i, strerror(i));
    }
    return 0;
}

使用范例

#include <stdio.h>
#include <string.h>
#include <errno.h>
//error的头文件
int main()
{
    FILE* pFile;
//forpen是打开文件函数,返回类型为FILE,只有两个参数,一个是文件名,另一个是文件的使用方式。
    pFile = fopen("unexist.ent", "r");//r是只读文件的意思
    if (pFile == NULL)
        printf("Error opening file unexist.ent: %s\n", strerror(errno));
//strerror的错误类型放在error里
        /*perror("Error opening file unexist.ent");*/一般是用这个
    return 0;
}

以上用例为文件操作的内容,以后会讲,如果是正常的判断文件是否为空,最后一定不会return 0。没有创建这个文件所以报错了。

以下为所以常用的文件使用方式

接下来进入内存函数的部分,内存函数要学的一共有4个。

内存函数

内存函数的头文件依然是#include<string.h>,是作用在内存中的函数,所以是以字节为单位在进行比较和拷贝等操作的一类函数。

memcpy

memcpy的作用是拷贝一个字符串到另一个字符串上的特定位置。

#include<stdio.h>
#include<string.h>
int main()
{
    int arr1[10] = { 0 };
    int arr2[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    //将arr2中的1 2 3 4 5,拷贝到arr2中
    memcpy(arr1, arr2, 5 * sizeof(int));//以字节为单位进行拷贝的,拷贝什么都行
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", arr1[i]);
    }
    return 0;
}

memcpy可以指定字节个数进行拷贝,和strncpy的功能基本一致。所以还是不要自己拷贝自己,虽然VS2022没有报错。

VS2022独有功能(其实不太清楚其他版本行不行)

int main()
{
    int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
    memcpy(arr1+2, arr1, 5 * sizeof(int));
    int i = 0;
    for (i = 0; i < 10; i++)
//不要自己拷贝自己,虽然没有报错
    {
        printf("%d ", arr1[i]);
// 1 2 1 2 3 4 5 8 9 10
    }

    return 0;
}

模拟实现

void* my_memcpy(void* dest, const void* src, size_t num)
{
    assert(dest && src);
    void* ret = dest;
    while (num--)
    {
        *(char*)dest = *(char*)src;
        src = (char*)src + 1;
        dest = (char*)dest + 1;
    }
    return ret;
}

int main()
{
    int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int arr2[10] = { 0 };
    my_memcpy(arr1, arr2, 5 * sizeof(int));
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", arr1[i]);
    }

    return 0;
}

memcpy模拟实现的思想和strcpy的差不多,看不懂的就回去之前将strcpy的地方,因为void*的指针不能直接加减运算,所以先转化成char*(由于是一个字节一个字节的拷贝所以char*合适)就可以加减了

memmove

memmove也是拷贝一个字符串到另一个字符串的特定位置上。和memcpy的功能差不多。不过可以自己拷贝自己了。memmove专门用来自己拷贝自己。

int main()
{
    int arr1[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    int i = 0;
    memmove(arr1 + 3, arr1, 6 * sizeof(int));
    for (i = 0; i < 10; i++)
    {
        printf("%d ", arr1[i]);
    }
    return 0;
}

模拟实现

模拟实现memmove和模拟实现memcpy差不多,但是由于这次实现的是自己拷贝自己,就是说原函数和将要拷贝的函数都是同一个,所以就有拷贝位置不同之分。

void* my_memmove(void* dest, const void*src, size_t num)
{
    void* ret = dest;
    assert(dest && src);
    if (dest < src)
    {
        //前->后
        while (num--)
        {
            *(char*)dest = *(char*)src;
            dest = (char*)dest + 1;
            src = (char*)src + 1;
        }
    }
    else
    {
        //后->前
        while (num--)
        {
            *((char*)dest+num) = *((char*)src + num);
        }
    }
    return ret;
}

int main()
{
    int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
    my_memmove(arr1, arr1+2, 5 * sizeof(int));//这两个的作用差不多,memmove可以实现自己拷贝自己
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", arr1[i]);// 3 4 5 6 7 6 7 8 9 10
    }

    return 0;
}

如果dest <= src说明要拷的字符串的起点比原字符串的起点靠前,这样从前往后拷贝就不会出现因为拷完若干个后将还未拷贝的字符覆盖了。如果dest > src则相反。这也是为什么memcpy和strcpy不能自己拷贝自己的原因。

memset

memset是内存重设置函数,可以将可变动的字符设置成若干个你想要的字符。

#include<stdio.h>
#include<string.h>
#include<assert.h>

//memset - 内存设置函数
int main()
{
    char arr[] = "hello word";

    //int arr[10] = { 1 };//尽量不要用int为单位,因为memset是以字节为单位进行设置的,char只占一个字节比较好操作。
    //memset 在设置的时候,是以字节为单位来设置的

    memset(arr, 'x', 5);//将arr中的5个字符设置为x,5为5个字节
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%c", arr[i]);
    }
    return 0;
}

#include<stdio.h>
#include<string.h>
#include<assert.h>

//memset - 内存设置函数
int main()
{
    int arr[10] = { 1 };
 
   //memset 在设置的时候,是以字节为单位来设置的
    memset(arr, 'x', 4);//将arr中的1个字符的4个字节设置为x
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

这个函数不需要模拟实现(太简单了)!!!

memcmp

memcmp是所占内存大小比较函数,和strcmp的功能类似,用于比较两个字符串的大小,但是memcmp是以字节为单位进行比较的。

//memcmp是内存比较函数,以字节为单位进行比较,和strcmp返回值一致
int main()
{
    int arr1[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    int arr2[5] = { 1, 2, 3, 6, 8 };
    int ret = memcmp(arr1, arr2, 17);
    printf("%d", ret);
    return 0;
}

严格来说,memcmp在这里由于是比较的是整型字符,1的内存存储形式为 01 00 00 00,由于01后面的都是00,所以memcmp的第三个参数写16到19都可以得到答案为-1。

这个函数不需要模拟实现(太简单了)!!!

本文到这里就结束了,如果觉得本文对你有帮助,请多点点关注,多点赞。我太喜欢点赞了!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值