【c语言】字符串函数和内存函数

简介: 本文介绍了C语言中常用的字符串函数和内存函数,包括`strlen`、`strcpy`、`strcat`、`strcmp`、`strstr`、`strncpy`、`strncat`、`strncmp`、`strtok`、`memcpy`、`memmove`和`memset`等函数的使用方法及模拟实现。文章详细讲解了每个函数的功能、参数、返回值,并提供了具体的代码示例,帮助读者更好地理解和掌握这些函数的应用。

前言

       在编程的过程中,我们经常要对字符串和内存进行各种各样的处理,c语言提供了一系列字符串函数和内存函数,便于我们对字符串或者内存空间进行操作。本篇文章我们就来学习其中的一些函数。


一、字符串函数

1.strlen的使用和模拟实现

       c语言中,strlen函数用于计算一个字符串的长度。它的原型如下:


size_t  strlen ( const char * str ) ;


这里需要注意的是:

1.字符串以\0为结束标志,这个函数返回值是字符串的长度,不包括\0。

2.传参时要传入字符、0串的首元素地址,指向的字符串要以\0为结尾。

3.函数返回值类型是size_t,是一个无符号整数

4.strlen函数使用需要引头文件string.h


接下来我们试着使用一下strlen函数:

#include <stdio.h>
#include<string.h>
 
int main()
{
    char str[] = "hello";
    int ret = strlen(str);
    printf("%d\n", ret);
    return 0;
}

运行结果:



可以看到,"hello"一共有五个字符,所以字符串的长度就是5。也就是说,这个函数的原理就是统计出\0之前的字符个数。这样,我们尝试模拟实现一下它:


方法1:计数器

size_t my_strlen(const char* str)
{
    size_t count = 0;//定义变量统计字符个数
    while (*str != '\0')
    {
        str++;
        count++;
    }
    return count;
}

这里我们定义了一个变量count,负责统计\0之前的字符个数。当指针走到\0处时,循环停止。


方法2:递归

size_t my_strlen(const char* str)
{
    size_t count = 0;//定义变量统计字符个数
    while (*str != '\0')
    {
        str++;
        count++;
    }
    return count;
}

方法3:指针减指针

size_t my_strlen(const char* str)
{
    char* p = str;
    while (*str++);//指针持续往后走,知道遇到\0为止
    return str - p - 1;//后置++使得指针多走了一步,多减一个1
}

这里先记录一下首元素地址,然后让str跑到字符串末尾,再减去首元素地址就得到两地址之间的元素个数,也就是字符串长度。


2.strcpy的使用和模拟实现

       strcpy这个函数的作用是将源字符串中的内容拷贝到目标字符串中。它的原型如下:


char* strcpy ( char* dest, const char* src ) ;


这里需要注意:

1.第一个参数是目标字符串的首地址,第二个参数是源字符串的首地址。

2.源字符串必须以\0结尾。

3.目标字符串的空间必须足够大,能够包含整个源字符串;目标字符串不能是常量字符串。

4.函数返回值是目标字符串的首元素地址。

5.源字符串中的\0也会一同拷贝。


我们尝试使用一下它:

#include <stdio.h>
#include <string.h>
 
int main()
{
    char str1[] = "xxxxxxxxxxxx";
    char str2[] = "hello";
    printf("%s\n", strcpy(str1, str2));
    return 0;
}

运行结果:



模拟实现:

char* my_strcpy(char* dest,const char* src)
{
    char* ret = dest;//记录首元素地址
    while (*dest++ = *src++);
    return ret;
}

这里我们首先记录了原字符串的首地址,然后循环赋值完成拷贝,最后返回首地址。


3.strcat的使用和模拟实现

       strcat函数的作用是将源字符串内容追加到目标字符串上。原型如下:


char * strcat ( char * dest, const char * src ) ;


要注意的是:

1.第一个参数是目标字符串的首地址,第二个参数是源字符串的首地址。

2.源字符串和目标字符串都必须以\0结尾。

3.目标字符串的空间必须足够大;目标字符串不能是常量字符串。

4.函数返回值是目标字符串的首地址。


接着我们使用一下这个函数:

#include <stdio.h>
#include <string.h>
 
int main()
{
    char str1[20] = "hello ";
    char str2[] = "world";
    printf("%s\n", strcat(str1, str2));
    return 0;
}

运行结果:



模拟实现:

char* my_strcat(char* dest, const char* src)
{
    char* ret = dest;//记录目标字符串首地址
    while (*dest)
    {
        dest++;
    }//使指针指向目标字符串末尾
    while (*dest++ = *src++);//循环追加,直到源字符串末尾
    return ret;
}

4.strcmp的使用和模拟实现

       strcmp函数是一个很重要的函数,它用于比较两个字符串的大小。字符串的比较在很多实例中会使用到,它的比较规则如下



在了解了比较规则之后,我们来看一下函数原型:


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


1.参数传入要比较的两个字符串的首地址。

2.两个字符串都要以\0结尾。

3.如果str1大于str2,函数返回正数,否则返回负数,相等则返回0。


我们写一个程序使用该函数,实现两个字符串比较:

#include <stdio.h>
#include <string.h>
 
int main()
{
    char str1[] = "abcd";
    char str2[] = "abdc";
    int flag = strcmp(str1, str2);
    if (flag > 0)
    {
        printf("str1大于str2\n");
    }
    else if (flag < 0)
    {
        printf("str1小于str2\n");
    }
    else
    {
        printf("两者相等\n");
    }
    return 0;
}

运行结果:



接着,我们尝试模拟实strcmp:

int my_strcmp(const char* str1, const char* str2)
{
    while (*str1 == *str2)
    {
        if (*str1 == '\0')//处理出现空字符串或者字符比较到末尾的情况
        {
            return 0;
        }
        str1++;
        str2++;
    }
    return *str1 - *str2;
}

5.strstr的使用和模拟实现

       接下来,我们学习一下strstr函数。strstr函数的作用是判断一个字符串是否是另一个字符串的子字符串(是否为包含关系)


它的原型如下:


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


1.两个字符串必须要以\0结尾。

2.函数的返回值是str2在str1中第一次出现的位置,如果没找到,返回空指针。

我们尝试使用这个函数:

#include <stdio.h>
#include <string.h>
 
int main()
{
    char str1[] = "123456789";
    char str2[] = "3456";
    printf("%s\n", strstr(str1, str2));
    return 0;
}

运行结果:



可以看到,函数确实找到了第二个字符串在第一个字符串中的位置。接下来我们模拟实现一下它:

char* my_strstr(const char* str1, const char* str2)
{
    char* cur = str1;
    if (*str2 == '\0')//空字符串的情况
    {
        return str1;
    }
    while (*cur != '\0')
    {
        char* s1 = cur;
        char *s2 = str2;
        while (*s1 && *s2 && (*s1 - *s2) == '\0')//遍历相等的部分
        {
            s1++;
            s2++;
        }
        if (*s2 == '\0')//遍历结束后如果str2已结束,说明是子串
        {
            return cur;
        }
        cur++;
    }
    return NULL;
}

6.strncmp、strncpy、strncat的使用

       这三个函数是在原来功能的基础之上,限定了比较/拷贝/追加的字符个数。函数的参数部分多了一个num,表示限定的字符个数。举个例子:

#include <stdio.h>
#include <string.h>
 
int main()
{
    char str1[] = "xxxxxxxxx";
    char str2[] = "hehe";
    strncpy(str1, str2, 2);
    printf("%s\n", str1);
    char str3[20] = "hello ";
    char str4[] = "world";
    strncat(str3, str4, 3);
    printf("%s\n", str3);
    return 0;
}

运行结果:



可以看到,程序根据我们限定的字符个数完成了拷贝和追加。


7.strtok的使用

       strtok函数也叫做字符串分割函数。顾名思义,它的作用就是根据给出的特定字符来分割字符串。下面是它的原型:


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


这里需要注意以下几点:

1.第一个参数是要被分割的字符串,第二个参数是由分隔符组成的字符串。

2.strtok函数会在str中寻找限定的分隔符,遇到分隔符就会在字符串中将其变为\0,完成分割。函数返回上一次分割后\0的下一个字符的地址;如果是第一次分割,则返回字符串首地址。

3.对一个字符串进行多次分割时,需要多次调用该函数:第一次调用时str传入要被分割的字符串,函数会使用静态变量保存被分割的位置,之后调用时str要传入NULL,函数就会从保存的位置开始进行分割。

4.当字符串按照分隔符完成了所有分割之后,就会返回NULL。

我们写代码体现一下它的使用效果:

#include <stdio.h>
#include <string.h>
 
int main()
{
    char str[] = "123.12.14.1.50";
    char* p = NULL;
    printf("%s\n", str);
    for (p = strtok(str, "."); p != NULL; p = strtok(NULL, "."))
    {
        printf("%s\n", p);
    }
    return 0;
}

运行结果:



可以看到字符串成功被分割成几部分。不难发现,strtok函数可以和for循环结合使用,达到分割字符串的效果


二、内存函数

       在学习了这些字符串函数之后,我们可以发现,它们虽然实用,但是却只能对字符串进行操作。那么是否有一些函数可以让我们对任何类型也进行相似操作呢?它就是内存函数。这些内存函数的使用也需要引头文件string.h


1.memcpy的使用和模拟实现

       memcpy函数也叫做内存拷贝函数,它能够将特定字节的内存值拷贝到其他内存位置中。它的原型:


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


其中的前两个参数分别表示目标空间和源空间的首地址,第三个参数表示要拷贝的字节数。函数的返回值是目标空间的首地址。我们来使用一下它:

#include <stdio.h>
#include <string.h>
 
int main()
{
    int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int arr2[10] = { 0 };
    memcpy(arr2, arr1, 5 * sizeof(int));
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", arr2[i]);
    }
    return 0;
}

运行结果:



可以看到,有五个元素的内容被拷贝到arr2当中。


接下来我们尝试模拟实现

void* my_memcpy(void* dest, const void* src, size_t num)
{
    void* ret = dest;
    while (num--)
    {
        *(char*)dest = *(char*)src;
        (char*)dest += 1;//这里由于强制类型转换是临时的,不能直接写++
        (char*)src += 1;
    }
    return ret;
}

可以看出,我们这里使用了泛型编程的思想,将void*指针强转为char*类型,然后根据给定的字节数一个字节一个字节赋值。


2.memmove的使用和模拟实现

       对于刚才的memcpy函数,如果想要拷贝的两个内存空间之间出现重叠情况,那么函数就无法完成正常的拷贝。而memmove函数就弥补了这个缺陷。原型如下:


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


它的参数含义和返回值和memcpy是完全相同的。我们尝试使用它来拷贝重叠的内存:

#include <stdio.h>
#include <string.h>
 
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    memmove(arr, arr + 2, 5 * sizeof(int));
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

运行结果:



可以看到,它成功地完成了重叠内存的拷贝。那我们是否可以模拟实现这个函数呢?


我们首先来思考一下,现在有一个数组arr,它的内容如下:



现在我们想要将1,2,3,4,5这五个元素拷贝到3,4,5,6,7的位置上,能否完成呢?


将1赋值到3的位置上,将2赋值到4的位置上,到赋值3的时候,就会发现,此时的3已经被覆盖成1了,这样下去,最终结果就会变成1,2,1,2,1,2,1,8,9,10。


聪明的你肯定会想到,既然从前往后不行,那么我们就从后往前进行拷贝:先将5赋值到7的位置,再将4赋值到6的位置,以此类推...你就会发现结果就是1,2,1,2,3,4,5,8,9,10,说明成功了。


难道所有的情况都可以通过从后往前拷贝的方法赋值成功吗?当我们反过来将3,4,5,6,7拷贝到1,2,3,4,5的位置上,我们就发现从后往前的拷贝就不行了,就要用从前往后拷贝的方式。


所以我们就可以得出以下结论:在有重叠空间的情况下,如果dest的地址小于src,那么就从前向后拷贝;如果dest的地址大于src,就从后向前拷贝。没有重叠空间时怎样都行。图示:



这里由于右边非重叠空间的部分和中间部分是连续的,所以我们使用从后向前拷贝的方法会更加方便编写代码


代码如下:

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

3.memset的使用

       memset函数也叫做内存设置函数,它可以对特定区域的内存全部赋成你想要的值。它的原型如下:


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


它的第一个参数是要设置的内存空间的首元素地址,第二个参数是要赋的值,第三个参数是内存空间的大小(字节)。

它的返回值是内存空间的首元素地址。


我们来使用一下这个函数:

#include <stdio.h>
#include <string.h>
 
int main()
{
    char str[] = "hello world";
    memset(str, 'x', 6);
    printf(str);
    return 0;
}

运行结果:



可以看到,前六个字符都被赋值成了'x'。


总结

       本篇文章我们学习了字符串函数和内存函数的相关知识,它们在我们的编程当中十分常见和实用。同时,我们也学会了如何思考问题,解决问题。之后博主会更新数据存储方式相关的内容。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

相关文章
|
7天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
23 6
|
21天前
|
C语言
【c语言】动态内存管理
本文介绍了C语言中的动态内存管理,包括其必要性及相关的四个函数:`malloc`、``calloc``、`realloc`和`free`。`malloc`用于申请内存,`calloc`申请并初始化内存,`realloc`调整内存大小,`free`释放内存。文章还列举了常见的动态内存管理错误,如空指针解引用、越界访问、错误释放等,并提供了示例代码帮助理解。
33 3
|
20天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
23天前
|
C语言
【c语言】qsort函数及泛型冒泡排序的模拟实现
本文介绍了C语言中的`qsort`函数及其背后的回调函数概念。`qsort`函数用于对任意类型的数据进行排序,其核心在于通过函数指针调用用户自定义的比较函数。文章还详细讲解了如何实现一个泛型冒泡排序,包括比较函数、交换函数和排序函数的编写,并展示了完整的代码示例。最后,通过实际运行验证了排序的正确性,展示了泛型编程的优势。
19 0
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
370 0
|
22天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
46 1
|
27天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
1月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
1月前
|
存储 编译器
数据在内存中的存储
数据在内存中的存储
41 4
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配