C语言字符函数和内存函数

简介: 本节内容重点就是学会使用一些字符函数和内存函数,并且去模拟几个重要的函数,了解和体会这个函数是怎么样去实现的。不知道各位寒假在家学习的怎样,一定要坚持学习啊。好了,话不多说,我们直接开始学习我们的函数吧。

@TOC

字符函数和内存函数

本节内容重点就是学会使用一些字符函数和内存函数,并且去模拟几个重要的函数,了解和体会这个函数是怎么样去实现的。不知道各位寒假在家学习的怎样,一定要坚持学习啊。好了,话不多说,我们直接开始学习我们的函数吧。

字符函数

求字符串长度

strlen

strlen其实是我们的老朋友了,但是今天我们还是来复习一下它,我们首先来看函数的参数和返回类型,

size_t strlen ( const char * str );
使用strlen需要注意的点:
  • 字符串中一定要有\0,因为字符串是以\0作为结束标志。
  • strlen统计的是字符串中\0之前的字符个数

关于strlen的简单使用就不再举例了。

但是我们还发现一个问题就是,库里面的strlen返回类型实际上是size_t,我们知道size_t其实就是unsigned int(无符号整型),我们可以猜测一下,在函数设计时,设计者可能想的是strlen来求字符串长度,长度大小那么一定为正数,所以设计成了size_t。但是我们在模拟strlen的时候,我们一般将返回类型设为int,这是为什么呢?我们来看下面这样一段代码:

#include <stdio.h>
#include <string.h>
int main()
{
   
   
    if (strlen("abc") - strlen("abcdef") > 0)
    {
   
   
        printf(">\n");
    }
    else
    {
   
   
        printf("<=\n");
    }
    return 0;
}

效果:image-20230111125653304

我猜你想说的一定是-3肯定小于0,打印<=嘛,如果你是这样想的,那么恭喜你,你就错了。实际上我们的结果是输出>,这时候我们就要思考为什么了,实际上还是因为库里面strlen返回值是size_t的问题,两个无符号整型相减得到的一定是无符号整型,将-3的二进制序列看做一个无符号数的话,那值可就大了去了。所以最终结果输出的是>,所以模拟实现strlen的话,返回值设计为int还是size_t没有就绝对的好坏,要根据具体情况来判断。

长度不受限制的字符串函数

strcpy

字符串拷贝函数,我们先看它的函数参数和返回类型,

char * strcpy ( char * destination, const char * source );

然后看一下它的信息:

image-20230111121156563

可以看到使用其实就是将source(源头)字符串的地址和destination(目标)字符串的地址传过去,然后它会帮你拷贝,最后返回目标字符串的起始地址。

使用strcpy需要注意的点:
  • 源字符串必须以 '\0' 结束。
  • 会将源字符串中的 '\0' 拷贝到目标空间。
  • 目标空间必须足够大,以确保能存放源字符串。
  • 目标空间必须可变。

使用的一个简单举例:

int main()
{
   
   
    char arr1[20] = "xxxxxxxxxxxxxxx";
    char* arr2 = "abcdef";//注意常量字符串不能被修改
    strcpy(arr1, arr2);
    printf("%s\n", arr1);
    return 0;
}

效果:image-20230111125607715

strcat

首先看函数参数和返回类型,

char * strcat ( char * destination, const char * source );

然后看信息:

image-20230111125212763

可以看到实际上strcat和strcpy还是很类似的,都是将目标地址和源头地址传过去,最后将目标字符串起始地址返回。只是效果不同,strcat是在目标字符串的末尾去进行追加。

使用strcat需要注意的点:
  • 源字符串必须以 '\0' 结束。
  • 目标空间必须有足够的大,能容纳下源字符串的内容。
  • 目标空间必须可修改。
  • !!!strcat这函数是不能用来字符串自己给自己追加的

使用简单举例:

//strcat——追加字符串
#include <stdio.h>
#include <string.h>
int main()
{
   
   
    char arr1[20] = "hello ";//注意要保证目标空间足够大
    char arr2[] = "world!!";
    strcat(arr1, arr2);
    printf("%s\n", arr1);
    return 0;
}

效果:image-20230111125716927

到这里看起来一切合情合理,非常顺利,但是其实是有问题的,strcat这函数是不能用来字符串自己给自己追加的,因为我们是从\0开始的,你上来直接把\0结束标志给干掉了,然后后面就会一直追加,程序就停不下来了。

strcmp

先看函数参数和返回类型,字符串比较函数,

int strcmp ( const char * str1, const char * str2 );

再看函数信息:

image-20230111132041283

传给strcmp两个字符串的地址,如果第一个字符串小于第二个字符串返回小于0的数字,相等返回0,大于返回大于0的数字。

需要注意的点:
  • 标准规定:
    第一个字符串大于第二个字符串,则返回大于0的数字
    第一个字符串等于第二个字符串,则返回0
    第一个字符串小于第二个字符串,则返回小于0的数字
  • strcmp比较两个字符串是根据第一个不同字符的ASCII码值来比较大小的,千万不要认为是字符串长度。

简单使用举例:

//strcmp——字符串比较
#include <stdio.h>
#include <string.h>
int main()
{
   
   
    char* arr1 = "abcdef";
    char* arr2 = "abq";//c小于q
    printf("%d\n",strcmp(arr1, arr2));
    return 0;
}

效果:image-20230111132906577

长度受限制的字符串函数

strncpy

strncpy实际上是同strcpy基本类似,只是最后多了一个函数参数代表个数:

char * strncpy ( char * destination, const char * source, size_t num );
需要注意的是:
  • 拷贝num个字符从源字符串到目标空间。
  • 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。

简单使用举例:

当num小于源字符串长度

//strncpy
#include <stdio.h>
#include <string.h>
int main()
{
   
   
    char arr1[] = "xxxxxxxxxx";
    char arr2[] = "abcdef";
    strncpy(arr1, arr2,3);
    printf("%s\n", arr1);
    return 0;
}

效果:image-20230111135822542

当num大于源字符串长度:

//strncpy
#include <stdio.h>
#include <string.h>
int main()
{
   
   
    char arr1[] = "xxxxxxxxxx";
    char arr2[] = "abcdef";
    strncpy(arr1, arr2, 7);
    printf("%s\n", arr1);
    return 0;
}

效果:image-20230111135956321

strncat

类似的,strncat也是增加了一个参数代表追加个数:

char * strncat ( char * destination, const char * source, size_t num );

需要注意的点是:当追加完要求的三个时,字符串后面会自动放一个\0。

例如:

//strncat
#include <stdio.h>
#include <string.h>
int main()
{
   
   
    char arr1[20] = "hello\0xxxxx";
    char arr2[] = "world!!";
    strncat(arr1, arr2, 3);
    //strncat(arr1, arr2, 7);
    printf("%s\n", arr1);
    return 0;
}

效果:image-20230111141004043

strncmp

同理,strncmp也是比strcmp多了一个函数参数num表示比较前num个字符:

int strncmp ( const char * str1, const char * str2, size_t num );

简单使用举例:

/*strncmp  example*/
#include <stdio.h>
#include <string.h>
int main()
{
   
   
    char arr1[] = "abcdef";
    char arr2[] = "abcdq";
    printf("%d\n", strncmp(arr1, arr2, 5));
    printf("%d\n", strncmp(arr1, arr2, 4));
    return 0;
}

效果:image-20230111182734869

字符串查找

strstr

先看函数参数和返回值,字符串查找函数。

const char * strstr ( const char * str1, const char * str2 );
      char * strstr (       char * str1, const char * str2 );

再看函数信息:

image-20230111184353699

传递两个字符串地址,在arr1中查找arr2是否存在,若存在则返回arr1中第一次找到arr2的地址,若没找到则返回空指针。

简单使用举例:

/*strstr  example*/

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

int main()
{
   
   
    char* arr1 = "abbcdef";
    char* arr2 = "bbc";
    char* arr3 = "bbcq";
    printf("%s\n", strstr(arr1, arr2));
    printf("%s\n", strstr(arr1, arr3));
    return 0;
}

输出:image-20230111185344987

strtok

这个函数可以说是一个很奇葩的函数,它的作用是将一个字符串分割出来。

char * strtok ( char * str, const char * delimiters );

看函数描述:

image-20230111211204473

还是比较长的,我们来解释一下,就是将一个字符串中的分隔符单独放到一个数组里面,将该数组作为第二个函数参数传给strtok,第一个是想要分割的数组,这样strtok函数能够找到字符串中的分隔符改为\0,将这一部分分割出来,返回这一部分的起始地址。如果没有找到分隔符的话,则返回空指针(NULL)。

需要注意的点:
  • strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。
  • strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。
  • strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
  • strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。如果字符串中不存在更多的标记,则返回 NULL 指针。

简单使用举例:

/*strtok  example*/
#include <stdio.h>
#include <string.h>
int main()
{
   
   
    char arr1[] = "lzuobing@handsome.net";
    char* p = "@.";
    char buf[50] = {
   
    0 };
    strcpy(buf, arr1);
    printf("%s\n", strtok(buf, p));
    printf("%s\n", strtok(NULL, p));
    printf("%s\n", strtok(NULL, p));
    printf("%s\n", strtok(NULL, p));
    return 0;
}

输出:image-20230111212059599

但是我们如果每次都这么写确实有点挫,事实上我们通常会巧妙的利用for循环来实现输出,代码如下:

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

int main()
{
   
   
    char arr1[] = "lzuobing@handsome.net";
    char* p = "@.";
    char buf[50] = {
   
    0 };
    strcpy(buf, arr1);
    for (char* ret = strtok(buf, p); ret != NULL; ret = strtok(NULL, p))
    {
   
   
        printf("%s\n", ret);
    }
    return 0;
}

错误信息报告

strerror

strerror这个函数说实话也是一个比较特别的函数,我们先来看一下函数参数和返回值:

char * strerror ( int errnum );

再看一下函数详细信息:

image-20230112144533602

这个函数就是给它一个错误码作为函数参数,然后返回一个char*的指针,用来翻译成错误信息,

简单使用举例:

#include <stdio.h>
#include <string.h>
//c库函数在使用是出错的话,会返回错误码
//strerror可以将错误码翻译为错误信息
int main()
{
   
   
    printf("%s\n", strerror(1));
    printf("%s\n", strerror(2));
    printf("%s\n", strerror(3));
    printf("%s\n", strerror(4));
    printf("%s\n", strerror(5));
    return 0;
}

输出:image-20230112145106215

这样我们只是简单举了这样一个例子来解释strerror这个函数,但是在实际情况下不是这样使用的,我们需要用的errno这个宏,

为了实际展示strerror是怎么用的,我们再来简单了解一个函数:文件打开函数fopen,

FILE * fopen ( const char * filename, const char * mode );

image-20230112150441016

这里我们简单看一下即可,fopen函数就是用来打开一个文件,如果打开成功返回一个FILE*的一个指针,如果打开失败则返回一个空指针。

我们来让它打开失败一次用strerror来看一下错误信息:

#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
   
   
    //打开文件
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL)//打开失败
    {
   
   
        printf("%s\n", strerror(errno));
        return 1;
    }
    //关闭文件
    fclose(pf);
    return 0;
}

输出:image-20230112151455258

可以看到错误信息是目录中没有文件,因为我们当前目录下是没有test.txt文件的,如果你创建一个再去运行就没有这个报错了。

实际上还有一个函数叫perror,这个函数是能够直接打印出错误信息,而strerror是先将错误码转换为错误信息然后自己去实现打印

#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
   
   
    //打开文件
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL)//打开失败
    {
   
   
        perror((char*)pf);
        //printf("%s\n", strerror(errno));
        return 1;
    }
    //关闭文件
    fclose(pf);
    return 0;
}

我们还是上面这个例子,直接用perror来看一下:image-20230112153655574

也是可以的,一个是直接就打印,所以有时候你并不想将错误信息打印出来这个函数就不是特别好了,没有strerror灵活。

字符函数:

这些函数看一下知道即可,有时候用这些函数可能会比较方便,例如判断是否是大写字符或小写字符。

函数 如果他的参数符合下列条件就返回真
iscntrl 任何控制字符
isspace 空白字符:空格‘ ’,换页‘\f’,换行'\n',回车‘\r’,制表符'\t'或者垂直制表符'\v'
isdigit 十进制数字 0~9
isxdigit 十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F
islower 小写字母a~z
isupper 大写字母A~Z
isalpha 字母a~z或A~Z
isalnum 字母或者数字,a~z,A~Z,0~9
ispunct 标点符号,任何不属于数字或者字母的图形字符(可打印)
isgraph 任何图形字符
isprint 任何可打印字符,包括图形字符和空白字符

内存函数

memcpy

内存拷贝函数,可以拷贝任意类型的内容,先看函数参数和返回类型:

void * memcpy ( void * destination, const void * source, size_t num );

再看函数信息:

image-20230112170906048

传递目标地址和源头地址,传递要拷贝的内容大小num个字节,memcpy会帮你把内存拷贝过去最后返回目标起始地址。

简单使用举例:

/*memcpy example*/
#include <stdio.h>
#include <string.h>

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

memmove

image-20230112184013641

实际上是memcpy一样的功能,和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。 参考下面模拟实现memcpy有详细解释。

memcmp

和strcmp类似,也是一个比较函数,只是通过每一个字节依次比较,

int memcmp ( const void * ptr1, const void * ptr2, size_t num );

image-20230112192856656

1中的内容小于2中的内容,返回小于0的数;相等返回0;大于则返回大于0的数。

简单使用举例:

/*memcmp example*/
#include <stdio.h>
#include <string.h>
int main()
{
   
   
    int arr1[] = {
   
    1,2,3 };
    int arr2[] = {
   
    1,2,3 };
    int arr3[] = {
   
    1,2,9 };
    printf("%d\n", memcmp(arr1, arr3,12));
    printf("%d\n", memcmp(arr1, arr2,12));
    printf("%d\n", memcmp(arr3, arr2,12));
    return 0;
}

输出:image-20230112193048180

memset

内存设置函数,先看函数参数和返回值:

void * memset ( void * ptr, int value, size_t num );

image-20230112194044052

该函数以字节为单位来将内存中的值来修改为你想要的内容。

简单使用举例:

/*memset example*/
#include <stdio.h>
#include <string.h>
int main()
{
   
   
    char arr[] = "hello world!!";
    memset(arr, 'x', 5);
    printf("%s\n", arr);
    return 0;
}

输出:image-20230112194228208

但是这个函数有几个需要注意的点,例如下面情况:

image-20230112194458741

可以看到似乎出问题了,并没有像我们想象的一样全部初始化为1,一定要牢记:memset是以字节为单位进行修改的,我们打开内存看一下你就明白了:

可以看一下内存中的值:image-20230112194721423

每一个字节都是1,当然是一个很大的数字了。

另外还需要注意的是函数第二个参数ASCII码值不能超过255,因为是以字节为单位进行修改的,8个比特位即使是无符号数最大也就255。

通常来说我们一般初始化是用的最多的情况,也就是每一个字节修改成0;

所以使用函数时要注意每一个函数,看使用合不合理。

库函数的模拟实现

模拟实现strlen

我们模拟strlen有三种思路,一是计数器直接计数,二是使用递归的方式,三是指针相减。

第一种方法:

这种方法是最简单的方法,也是最容易理解的方法。

//第一种方法——计数器
#include <assert.h>
int my_strlen(const char* str)
{
   
   
    assert(str!=NULL);//判断指针合法性
    int count = 0;//计数器
    while (*str)
    {
   
   
        count++; 
        str++;
    }
    return count;
}

第二种方法:

这种递归法如果你是第一次看可能理解不了,可以看之前我写过的这篇文章,里面有详细解释:

(C语言底层逻辑剖析函数篇(其三),函数递归与迭代超详解,斐波那契数列递归经典例题,汉诺塔问题,青蛙跳台阶_比昨天强一点就好的博客-CSDN博客

//第二种方法——递归
#include <assert.h>
int my_strlen(const char* str)
{
   
   
    assert(str != NULL);
    if (*str!='\0')
    {
   
   
        return 1 + my_strlen(str + 1);
    }
    else
    {
   
   
        return 0;
    }
}

第三种方法:

这种方法唯一需要注意的就是理解一下,两指针相减,得到的是中间的元素个数。

//第三种方法——指针相减
#include <assert.h>
int my_strlen(const char* str)
{
   
   
    assert(str != NULL);
    const char* start = str;//记录起始地址
    while (*str)
    {
   
   
        str++;
    }
    return (int)(str - start);//指针相减得到的是中间元素个数
}

模拟实现strcpy

一个字符一个字符拷贝即可,一直到源头字符串的\0拷贝过去,需要注意的点就是最后要返回目标字符串的起始地址,所以要提前记录一下。

//模拟实现strcpy
#include <assert.h>
char* my_strcpy(char* des, const char* source)
{
   
   
    assert(des && source);//判断指针合法性
    char* ret = des;//记录目标字符串起始地址
    while (*des++ = *source++)//拷贝
    {
   
   
        ;
    }
    return ret;//返回目标起始地址
}

模拟实现strcat

关键是要想清楚要从目标字符串的末尾\0开始追加,一直追加到原字符串的末尾\0。

//模拟实现strcat
#include <assert.h>
char* my_strcat(char* des, const char* source)
{
   
   
    assert(des && source);//判断指针合法性
    char* ret = des;//记录起始地址
    //1.找到目标字符串\0
    while (*des)
    {
   
   
        des++;
    }
    //2.追加
    while (*des++ = *source++)
    {
   
   
        ;
    }
    return ret;
}

模拟实现strcmp

按照顺序一个字符一个字符依次比较其ASCII码值即可。

//模拟strcmp
#include <stdio.h>
#include <assert.h>
int my_strcmp(const char* arr1,const char* arr2)
{
   
   
    assert(arr1&&arr2);
    while (*arr1==*arr2)
    {
   
   
        if (*arr1 == '\0')
        {
   
   
            return 0;
        }
        arr1++;
        arr2++;
    }
    /*if (*arr1 < *arr2)
        return -1;
    else
        return 1;*/
    return *arr1 - *arr2;
}

模拟实现strstr

要去模拟这个函数其实不是特别容易,我们需要考虑两种情况:

一种情况较为简单,没有任何重复的元素,直接寻找一遍即可;另一种情况有些复杂,如果中间有重复的元素,则需要用多个指针来实现。

我们可以通过画图来解释:

image-20230111200546714

第一种情况用指针去寻找bbc过程中其实容易出现问题,当我们找到第三个b时,发现不是我们要找的c,但是这时候指针已经往后走了,怎么办,所以这时候我们最后委托两个指针去向后遍历,并且还需要一个指针cp来记录一下开始判断的位置。

//模拟strstr

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

char* my_strstr(const char* str1,const char* str2)
{
   
   
    assert(str1 && str2);//判断指针合法性
    if (*str2 == '\0')
    {
   
   
        return (char*)str1;//str2为空字符串不做任何处理,直接返回str1
    }
    const char* s1 = str1;//委托两个指针s1,s2
    const char* s2 = str2;
    const char* cp = str1;//记录开始判断的地址
    //重点是理解下面思路
    while (*cp)
    {
   
   
        s1 = cp;
        s2 = str2;
        while (*s1 !='\0' && *s2 !='\0' && *s1 == *s2)
        {
   
   
            s1++;
            s2++;
        }
        if (*s2 == '\0')
        {
   
   
            return (char*)cp;
        }
        cp++;
    }
    return NULL;
}

模拟实现memcpy

最简单的思路:一个字节一个字节拷贝即可,

#include <assert.h>
void* my_memcpy(void* des, const void* source, int num)
{
   
   
    assert(des && source);//判断指针合法性
    void* ret = des;//记录目标起始地址
    while (num--)
    {
   
   
        *(char*)des = *(char*)source;
        des = (char*)des + 1;//一定注意这里的写法,不要写成*des++,强制类型转换是临时性的
        source = (char*)source + 1;//
    }
    return ret;
}

我们可以测试一下:image-20230112173014852

当我们这样两组单独的数据去测试的时候看起来没有任何问题,我们再换一种情况,假设我们要将12345拷贝放到34567处,有重复情况时:

看起来似乎就有问题了,并不是我们想要的结果,所以我们上面最简单的思路其实是存在一些问题的。哪里有问题呢,我们来分析一下:

左图是问题分析,右边是解决办法的分情况讨论,

我们这样详细分析清楚之后,实际上真正想要引出的是memmove这个函数,因为memmove实际上就是解决了这个重叠的问题。

模拟实现memmove

我们可以根据以上分析的思路来实现memmove:

//模拟实现memmove
#include <assert.h>

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

我相信肯定会有人有疑问啊,memmmove看起来完全就是memcpy的升级版,那么memcpy有什么存在必要呢,这些函数其实都是很多年前设计出的了,我们现在也只能猜测,也许当时是先出的memcpy,后来有人发现了重叠的问题,然后设计出一个memmove,但其实,现在有的平台上的memcpy已经将重叠的问题解决了,例如VS,gcc等,所以其实两种都可以的,但是还有一些环境并没有将memcpy的问题解决,所以我们这两个最好都要记住。

相关文章
|
2天前
|
C语言
C语言—内存函数的实现和模拟实现(内存函数的丝绸之路)
C语言—内存函数的实现和模拟实现(内存函数的丝绸之路)
17 0
|
2天前
|
C语言
C语言—字符函数与字符串函数(字符问题变简单的关键之技)
C语言—字符函数与字符串函数(字符问题变简单的关键之技)
4 0
|
2天前
|
C语言
C语言——函数递归
C语言——函数递归
4 0
|
2天前
|
C语言
C语言—函数(大化小方式的心脏)
C语言—函数(大化小方式的心脏)
2 0
|
3天前
|
程序员 编译器 C语言
C语言----动态内存分配(malloc calloc relloc free)超全知识点
C语言----动态内存分配(malloc calloc relloc free)超全知识点
14 6
|
3天前
|
存储 程序员 编译器
C语言:动态内存管理
C语言:动态内存管理
11 1
|
3天前
|
存储 编译器 程序员
C语言:数据在内存中的存储
C语言:数据在内存中的存储
15 2
|
3天前
|
存储
浮点数在内存中的存储
浮点数在内存中的存储
25 0
|
3天前
|
存储
数据在内存中的存储之整数存储
数据在内存中的存储之整数存储
21 0
|
3天前
|
存储 监控 NoSQL
Redis处理大量数据主要依赖于其内存存储结构、高效的数据结构和算法,以及一系列的优化策略
【5月更文挑战第15天】Redis处理大量数据依赖内存存储、高效数据结构和优化策略。选择合适的数据结构、利用批量操作减少网络开销、控制批量大小、使用Redis Cluster进行分布式存储、优化内存使用及监控调优是关键。通过这些方法,Redis能有效处理大量数据并保持高性能。
22 0