字符串函数、字符函数、内存函数的使用及其模拟实现(2)

简介: 字符串函数、字符函数、内存函数的使用及其模拟实现(2)

字符串查找函数

strstr

函数功能

查找子串,查找一个字符串中是否包含子串。

函数参数

char * strstr ( const char *str1, const char * str2); 
# char* 函数返回值,返回字符串中子串的起始地址,若找不到,则返回NULL;
# char* str1 要搜索的字符串;
# char* str2 子串

函数使用

#include <stdio.h>
#include <string.h>  //strstr对应头文件
int main()
{
  char arr1[] = "abbbcdef";
  char arr2[] = "bcd";
  char* ret = strstr(arr1, arr2);
  printf("%s\n", ret);
  return 0;
}

2020062310470442.png

模拟实现(重要)

#include <stdio.h>
#include <assert.h>
char* my_strstr(const char* str1, const char* str2)
{
    assert(str1 && str2);
  //如果中途匹配失败需要回到str2的起始地址,所以用其他变量标识str2,保证str2的首地址不会丢失
  const char* p1 = str1;
  const char* p2 = str2;  
  const char* mark = str1;  //用来标记每次第一个字符成功匹配的位置
  while (*mark != '\0')
  {
    p1 = mark;  //从mark处开始往后匹配
    p2 = str2;  //每次匹配后p2回到str2开头
    while (*p1 == *p2 && *p1 != '\0' && *p2 != '\0')  //一直往后匹配,直到遇到不相等的字符
    {
      p1++;
      p2++;
    }
    if (*p2 == '\0')  //如果不相等处p2为'\0'(子串全部匹配成功),则返回Mark处地址
      return (char*)mark;
    mark++;  //否则,说明这一次匹配失败,从mark后面一个字节处开始重新匹配
  }
  return NULL;  //字符串找完都没有子串就返回空指针
}
int main()
{
  char arr1[] = "abbbcdef";
  char arr2[] = "bcdq";
  char* ret = my_strstr(arr1, arr2);
  printf("%s\n", ret);
  return 0;

2020062310470442.png

注意事项

  • 被查找的字符串和子串都不能是空串,且都以’\0’结尾。
  • 如果查找成功,返回字符串中子串所在位置的首地址,如果查找失败,则返回NULL。

注:我们上面模拟实现的查找子串的函数效率比较低,如果要追求高效率,则需要使用KMP算法,有关KMP算法的相关知识,我会在后面的文章中进行介绍。

strtok

函数功能

字符串分割,把一个字符串按照分割标志分割为几个字符串。

函数参数

char * strtok ( char * str, const char * sep );
# char* 函数返回值,strtok函数会找到str中的下一个标记,并将其用'\0'结尾,返回一个指向这个标记的指针;
# char* str 指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记;
# char* sep 一个字符串,定义了用作分隔符的字符集合;

函数使用

#include <stdio.h>
#include <string.h>
int main()
{
  char* sep = "@.";
  char email[] = "1684277750@qq.com";
  char tmp[20] = "";
  //由于strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都会临时拷贝一份,操作拷贝的数据
  strcpy(tmp, email);
  printf("%s\n", strtok(tmp, sep));  //第一次第一个参数传递被切割字符串的首地址
  printf("%s\n", strtok(NULL, sep));   //第二次及以后第一个参数传递空指针(strtok会记住上一次切割的位置)
  printf("%s\n", strtok(NULL, sep));
  return 0;
}

2020062310470442.png

这里我们知道目标字符串会被分隔符切割为三个字符串,所以这里我们调用了三次strtok函数,但是当我们不知道目标字符串的内容时,这种方法显然就不能用了;那么我们该如何正确的使用strtok函数呢?


我们知道,strtok函数第一次调用时需要传递目标字符串的地址,其余调用都只需要传递NULL即可,那么我们可以利用这个特点结合for循环的特性来正确调用strtok函数。

#include <stdio.h>
#include <string.h>
int main()
{
  char* sep = "@.";
  char email[] = "1684277750@qq.com";
  char tmp[20] = "";
  //由于strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都会临时拷贝一份,操作拷贝的数据
  strcpy(tmp, email);
  char* ret = NULL;  //用来保存strtok函数的返回值
  for (ret = strtok(tmp, sep);   //初始化部分:第一次传递tmp的地址
    ret != NULL;               //判断部分:只要strtok的返回值ret不为空,说明继续分割
    ret = strtok(NULL, sep))   //调整部分:第二次及以上传递NULL
  {
    printf("%s\n", ret);  //ret不为空,说明字符串没被分割完,则打印
  }
  return 0;
}

2020062310470442.png

注意事项


    sep参数是个字符串,定义了用作分隔符的字符集合;

第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标 记;

strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注: 由于strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都会临时拷贝一份,操作拷贝的数据 )

strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置;

strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记;

如果字符串中不存在更多的标记,则返回 NULL 指针;

strerror

函数功能

C语言有一系列的库函数,当这些库函数调用失败时,会返回相应的错误码,而strerror函数的作用就是获取错误码对应的错误信息的首地址,让使用者知道程序发生错误的原因。

函数参数

char * strerror ( int errnum );
# char* 函数返回值,返回错误码对应的错误信息的字符串的地址;
# int errnum 错误码

函数使用

#include <stdio.h>  
#include <string.h>  //strerror的头文件
int main()
{
  printf("%s\n", strerror(0));
  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;
}

2020062310470442.png

这里有一个问题,C语言中那么多的错误信息,那么我们需要记住每一个错误信息对应的错误码吗?其实,C语言中设置了一个全局的用于存放错误码的变量errno,只要调用C语言库函数发生错误,那么errno就会记录相应的错误码,所以strerror函数和errno一般都是配合使用的。

#include <stdio.h>
#include <string.h>  //strerror对应头文件
#include <errno.h>   //errno对应头文件
int main()
{
  FILE* pf = fopen("test.txt", "r");
  if (pf == NULL)
  {
    printf("%s\n", strerror(errno));
    return 1;
  }
  else
  {
    printf("文件打开成功!\n");
  }
  return 0;
}

2020062310470442.png

字符函数

字符分类函数

image.png

字符转换函数

函数 返回值
tolower 返回对应小写字母的ASCII值
toupper 返回对应大写字母的ASCII值

内存操作函数

前面我们学习的strcpy、strcat、strcmp、strncpy、strncat、strncmp等函数都是字符串函数,只能对字符串进行相关操作,如果要对其他数据类型,如整形、字符、结构体等进行类似操作的话,就需要学习内存操作函数,常见的内存操作函数有memcpy、memmove、memcmp、memset。

memcpy

函数功能

内存拷贝,将一块内存中num个字节的内容拷贝到另一块内存中,常用来处理不重叠内存数据的拷贝。

函数参数

void * memcpy ( void * destination, const void * source, size_t num );
# void* 函数返回值,返回dest内存空间的地址;
# void* destination 目标内存空间地址;
# void* source 源空间地址;
# size_t num 要拷贝的字节数;

函数使用

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

2020062310470442.png

模拟实现

#include <stdio.h>
#include <assert.h>
void* my_memcpy(void* dest, const void* src, size_t num)
{
  assert(dest && src);
  void* ret = dest;  //保存目标空间的地址
  while (num--)   //以字节为单位进行拷贝
  {
    *(char*)dest = *(char*)src;  //强转为char*类型后赋值
        //这里不要写成(char*)dest++,在某些编译器下会报错,因为强制类型转是一种临时效果
    dest = (char*)dest + 1;  
    src = (char*)src + 1;
  }
  return ret;
}
int main()
{
  int arr1[] = { 1,2,3,4,5,6 };
  int arr2[10] = { 0 };
  my_memcpy(arr2, arr1, 6 * sizeof(int));
  for (int i = 0; i < 6; i++)
  {
    printf("%d ", arr2[i]);
  }
  return 0;
}

2020062310470442.png

注意事项(重要)

在C语言标准中,memcpy只负责处理内存不重叠的数据,内存重叠的数据的拷贝是memmove函数负责实现的,即下面这种情况在C语言标准中memcpy函数是不能实现的:

memcpy(arr1 + 2, arr1, 4 * sizeof(int));

2020062310470442.png

从上面我们memcpy的模拟实现中也可以看出,memcpy是从前向后拷贝的,这就导致在拷贝重叠内存数据时会发生数据覆盖(即arr1[2]中的数据在前面赋值中被改为1,导致将arr[2]中的数据赋给arr[4]时不是4,而是1),但是在VS下的memcpy函数是具备拷贝重叠数据的能力的,也就是说,VS下的memcpy函数同时实现了memmove函数的功能,但是其他编译器下的memcpy函数是否也具备memmove函数功能是未知的,所以我们在处理重叠内存数据拷贝的时候尽量还是使用memmove函数,以免发生错误。

memmove

函数功能

内存移动,将一块内存数据中的内容移动覆盖至另一块内存数据,常用来处理重叠内存数据的拷贝。

函数参数

# memmove 函数的参数和 memcpy 函数完全相同
void * memmove ( void* destination, const void * source, size_t num );

函数使用

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

2020062310470442.png

模拟实现(重要)

思路分析:

在memcpy中我们提到在拷贝重叠内存的数据时会发生内存覆盖的情况,其实这种覆盖分为两种情况:

(1):dest的地址大于src的地址

2020062310470442.png

如图,如果这时我们从前往后移动的话,那么4就会覆盖掉6,从而导致将6赋给8变成4赋给8,所以我们应该从后往前移。

(2):dest的地址小于src的地址

2020062310470442.png

如果这时我们从后往前移动的话,那么7就会覆盖掉5,导致将5赋给2的时候变成7赋给2,所以这里我们应该从前往后移动。

代码实现:

#include <stdio.h>
#include <assert.h>
void* my_memmove(void* dest, void* src, size_t num)
{
  assert(dest && src);
  void* ret = dest;  //保存目标空间的地址
  if (dest < src)
  {
    //前 -> 后 等价于memcpy
    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 arr[] = { 1,2,3,4,5,6,7,8,9 };
  my_memmove(arr + 1, arr + 3, 4 * sizeof(int));
  for (int i = 0; i < 9; i++)
  {
    printf("%d ", arr[i]);
  }
  return 0;
}

2020062310470442.png

20200623104134875.png

memcmp

函数功能

内存比较,比较两块内存中前num个字节的大小。

函数参数

int memcmp ( const void * ptr1, const void * ptr2, size_t num );
# int 函数返回值;
# void* ptr1 void* ptr2 要比较的两块内存;
# size_t num 要比较的字节数;

函数返回值

>0 : ptr1 大于 ptr2;
=0 : ptr1 等于 ptr2;
<0 : ptr1 小于 ptr2;··

函数使用

#include <stdio.h>
#include <string.h>  //memcmp对应头文件
int main()
{
  int arr1[] = { 1,2,3,4,5 };
  int arr2[] = { 1,2,3 };
  int ret = memcmp(arr1, arr2, 3 * sizeof(int));
  printf("%d\n", ret);
  return 0;
}

2020062310470442.png

模拟实现

#include <stdio.h>
#include <assert.h>
int my_memcmp(const void* ptr1, const void* ptr2, size_t num)
{
  assert(ptr2 && ptr2);
  size_t count = 0;  //用来标记相等的字节数
  while (*(char*)ptr1 == *(char*)ptr2 && count < num)  //一直循环,直到找到不相等的字节数据
  {
    count++;  //进入一次循环表示有一对字节相等,count++
    ptr1 = (char*)ptr1 + 1;
    ptr2 = (char*)ptr2 + 1;
  }
  if (count < num)  //如果count小于num,说明两块内存的前num个字节中有不相等的数据
    return *(char*)ptr1 - *(char*)ptr2;  //直接返回数据的差值
  else
    return 0;  //count等于num,说明相等
}
int main()
{
  int arr1[] = { 1,2,3,4,5 };
  int arr2[] = { 1,2,3 };
  int ret = my_memcmp(arr1, arr2, 3 * sizeof(int));
  printf("%d\n", ret);
  return 0;
}

2020062310470442.png

memset

函数功能

内存设置,把一块内存中num个字节的内容设置为指定的数据。

函数参数

void *memset( void *dest, int c, size_t count );
# void* 函数返回值,返回目标空间的地址;
# int c 函数参数,指定你想要初始化的数据;
# size_t count 函数参数,指定初始化的字节数

函数使用

#include <stdio.h>
#include <string.h>  //memset对应头文件
int main()
{
  char str[] = "hello world";
  memset(str, 'x', 5);
  printf("%s\n", str);
  return 0;
}

2020062310470442.png

模拟实现

#include <stdio.h>
#include <assert.h>
void* my_memset(void* dest, int c, size_t num)
{
  assert(dest != NULL);
  void* ret = dest;  //记录目标空间的地址
  while (num--)  //循环将num个字节的内容初始化为指定值
  {
    *(char*)dest = c;
    dest = (char*)dest + 1;
  }
  return ret;
}
int main()
{
  char str[] = "hello world";
  my_memset(str, 'x', 5);
  printf("%s\n", str);
  return 0;
}

2020062310470442.png



相关文章
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
29天前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
25天前
|
存储 C语言
【c语言】字符串函数和内存函数
本文介绍了C语言中常用的字符串函数和内存函数,包括`strlen`、`strcpy`、`strcat`、`strcmp`、`strstr`、`strncpy`、`strncat`、`strncmp`、`strtok`、`memcpy`、`memmove`和`memset`等函数的使用方法及模拟实现。文章详细讲解了每个函数的功能、参数、返回值,并提供了具体的代码示例,帮助读者更好地理解和掌握这些函数的应用。
21 0
|
4月前
|
存储 分布式计算 Hadoop
HadoopCPU、内存、存储限制
【7月更文挑战第13天】
283 14
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
376 0
|
24天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
50 1
|
28天前
|
存储
共用体在内存中如何存储数据
共用体(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原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配