【C语言】深剖字符串函数和内存函数4

简介: 【C语言】深剖字符串函数和内存函数

3. 内存操作函数


3.1 memcpy

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


   对指定单位数据进行拷贝,处理不重叠的拷贝。


函数细节


   函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。

   这个函数在遇到’\0’ 的时候并不会停下来,只有当元素拷贝完成后才会停下来。

   memcpy需要拷贝所有的类型的数据,所以用void*指针。

   count单位为字节。


使用方法

int main()
{
  int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int arr2[5] = { 0 };//把arr1的前五个拷贝到arr2中
  //把前五个数据的内存拷贝到arr2中
  //memcpy
  memcpy(arr2, arr1, 20);//20个字节,五个整形
  int i = 0;
  for (i = 0; i < 5; i++)
  {
    printf("%d ", arr2[i]);
  }
  return 0;
}


运行结果:

779ee71b59c4c48c4b32a18c35f6577b.png


模拟实现

图:

6a108b1bfc4ee269fc44187469527c48.png

void* my_memcpy(void* dest, const void* src, size_t count)
{
  assert(dest && src);
  void* ret = dest;//拷贝一份地址
  while (count--)
  {
    *(char*)dest = *(char*)src;//void类型指针使用时需要强制类型转换
    //plan1
    /*dest = (char*)dest + 1;
    src = (char*)src + 1;*/
        //plan2
    ((char*)dest)++;
        //(char*)dest++;//err
    //后置++针对的是dest,因为++优先级比较高,
        //但是void*是不能++的
    /**所以在没有计算之前,它就已经出现了错误,没有机会类型转换
    *但如果在(char*)dest加上一个括号,形成聚组操作符
    *让它先强制类型转换,那么就没什么问题了*/
    ((char*)src)++;
        //plan3
    /*
    ++(char*)dest;
    ++(char*)src;
    */
        //而前置++则可以,因为操作符的结合性只在数据和两个操作符相接的时候
    //才会涉及到结合性,当前情况并不涉及结合性,所以我们的数据肯定是先和char*结合
    //先被强制类型转换了,再进行++,现在针对的就是强转后的类型
  }
  return ret;//返回void*指针
}
int main()
{
  int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int arr2[5] = { 0 };
  my_memcpy(arr1, arr2, 20);
  for (int i = 0; i < 10; i++)
  {
    printf("%d ", arr1[i]);
  }
  return 0;
}


运行结果:eb3eeaeccec0d9973e5d4772a26137d0.png


函数缺陷


那么我们实现的memcpy是否可以处理重叠的拷贝(同一块内存空间中的数据拷贝)?

例如将一个数组从1开始的五个元素拷贝到第三个元素往后五个元素的位置:

ce1f818017cd6f291490fb3b8f8a1cae.png

测试:

void* my_memcpy(void* dest, const void* src, size_t count)
{
  assert(dest && src);
  void* ret = dest;
  while (count--)
  {
    *(char*)dest = *(char*)src;
    dest = (char*)dest + 1;
    src = (char*)src + 1;
  }
  return ret;
}
int main()
{
  int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
  my_memcpy(arr1 + 2, arr1, 20);
  for (int i = 0; i < 10; i++)
  {
    printf("%d ", arr1[i]);
  }
  return 0;
}

运行结果:

be842d7e2de28c078ac9680202dd1912.png


分析;


我们发现这是完全不行的,因为在同一块内存中,它们使用的是相同的空间,在对内存进行修改时,也会把原本的数据修改,当下标为2的位置的数据被改变后,当拷贝第三个元素时,原先的数据3已经被修改成了1,所以内存空间在拷贝时,数据全为1/2,无法完成拷贝。

那么如何完成对相同内存的拷贝?别急,马上就为您解答!



3.2 memmove

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


对指定单位数据进行拷贝,重叠和不重叠的都能搞定。


函数细节


  • 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
  • 如果源空间和目标空间出现重叠,就得使用memmove函数处理。


使用方法

int main()
{
  int arr1[10] = { 1,2,3,4,5,6,7,8,9, 10 };
  memmove(arr1 + 2, arr1, 20);//可以实现重叠内存的拷贝
  int i = 0;
  int sz = sizeof(arr1) / sizeof(arr1[0]);
  for (i = 0; i < sz; i++)
  {
    printf("%d ", arr1[i]);
  }
  return 0;
}


运行结果:0bba901ac090515e7bd24ac609b29511.png


模拟实现


memmove可以从前往后拷贝,也可以从后向前拷贝.

想象一下,当dest < src时,如果从后向前拷贝,是否就把重叠部分原先要拷贝的元素覆盖掉了?比如要将34567拷贝到12345的位置,先将7拷贝,是否将源位置的5给覆盖了?所以当dest < src时要从前往后拷贝。

当dest > srtc && dest < src + count时,如果从前往后拷贝,比如要将34567拷贝到原先4开始向后五个元素处,先拷贝3就把要拷贝的4给覆盖了,这样就错误了。所以当dest > src && dest < src + count时,需要从后往前拷贝。

而当dest > src + count时,则随便从前往后或从后往前,因为目标位置和源位置不可能会有重叠处,随便拷贝。


图:


e7b947d35b3506533051e242143ff23b.png


而关于memmove又有两种实现方式:


1.左边采用从前向后拷贝的方式,中间和右边采用从后向前拷贝的方式。

这个方法比较简单,只需要给出dest < src的情况即可。


2.左边和右边采用从前向后拷贝的方式,中间采用从后向前拷贝的方式。

这个方法复杂些,区别中间的区间即可,这时dest > src 并且 dest需要小于src+字节数。


void* my_memmove(void* dest, const void* src, size_t count)
{
  assert(dest && src);
  void* ret = dest;
  //1
  //左边采用从前向后拷贝的方式,中间和右边采用从后向前拷贝的方式
  if (dest < src)
  {
    //从前向后
    while (count--)
    {
      *((char*)dest) = *((char*)src);
      dest = (char*)dest + 1;
      src = (char*)src + 1;
    }
  }
  else
  {
    //从后向前 
    while (count--)
    {
      *((char*)dest + count) = *((char*)src + count);
      //从最后元素的最后一个字节开始向前拷贝,count会不断调整
    }
  }
  //2
  //左边和右间采用从前向后拷贝的方式,中间采用从后向前拷贝的方式
  //if (dest > src && dest < ((char*)src + count))
  //{
  //  //从后向前
  //  while (count--)
  //  {
  //    *((char*)dest + count) = *((char*)src + count);
  //  }
  //}
  //else
  //{
  //  //从前向后
  //  while (count--)
  //  {
  //    *((char*)dest) = *((char*)src);
  //    dest = (char*)dest + 1;
  //    src = (char*)src + 1;
  //  }
  //}
  return ret;
}
int main()
{
  int arr1[10] = { 1,2,3,4,5,6,7,8,9, 10 };
  //从前往后拷贝或者从后向前拷贝
  my_memmove(arr1 + 2, arr1, 20);
  int i = 0;
  int sz = sizeof(arr1) / sizeof(arr1[0]);
  for (i = 0; i < sz; i++)
  {
    printf("%d ", arr1[i]);
  }
  return 0;
}



运行结果:

efc77d066d914fb5cee96845bedbbe4d.png


memcpy的意义


可能到这里大家可能会有点疑惑,既然memmove既包含了memcpy的功能,甚至还能处理memcpy无法处理的情况,那么memcpy这个函数还有存在的意义吗?


实际上这只是片面的看法,虽然memmove比memcpy更加好用,但是memcpy更加重要,也许memcpy比memmove先出现,之前别人都是使用的memcpy;


而且语言是迭代更替的,是一代一代发展的,很多使用旧版本习惯的人可能习惯于使用memcpy,而且比如一个产品之前就是使用的memcpy,如果出现了memmove还要将版本全部重改一遍吗,如果盲目删除这个语法,就会带来很大的问题。


并且实际上vs中其实把memcpy进行了优化,我们模拟实现的memcpy不能实现相同内存的拷贝,但是使用库里的memcpy可以实现:

4ca4908692871f253a65b0f618ed6165.png


3.3 memcmp

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


   对指定单位内容进行比较


函数细节


   比较从ptr1和ptr2指针开始的num个字节

   第一组数据的一个字节内容若小于第二组数据的一个字节内容返回小于0的值

   第一组数据的一个字节内容若等于第二组数据的一个字节内容返回0

   第一组数据的一个字节内容若大于第二组数据的一个字节内容返回大于0的值


使用方法

int main()
{
  int arr1[] = { 1,2,3,4,5 };
  int arr2[] = { 1,2,3,4,0x11223305 };
  int ret = memcmp(arr1, arr2, 17);
  printf("%d\n", ret);
  ret = memcmp(arr1, arr2, 20);
  printf("%d\n", ret);
  return 0;
}



运行结果:

a52c4e634e5deef27a4c58f85a4d204a.png


分析:

5翻译成16进制位0x00000005,小端存储为05 00 00 00,和0x11223305小端存储时,前17个字节相同,从第18个字节开始不相同。

模拟实现

int my_memcmp(const void* arr1, const void* arr2, size_t count)
{
    assert(arr1 && arr2);
  while (count--)
  {
    if (*(char*)arr1 != *(char*)arr2)
    {
      return (*(char*)arr1) - (*(char*)arr2);
    }
    arr1 = (char*)arr1 + 1;
    arr2 = (char*)arr2 + 1;
  }
  return 0;
}
int main()
{
  int arr1[] = { 1,2,3,4,5 };
  int arr2[] = { 1,2,3,4,0x11223305 };
  int ret = my_memcmp(arr1, arr2, 17);
  printf("%d\n", ret);
  return 0;
}


运行结果:

00b3b1ca29b0e34be9a6c832e37bb8cf.png


3.4 memset

void *_memccpy( void *dest, const void *src, int c, unsigned int count );


对指定单位进行内存设置,设置的元素相同


函数细节


  • 内存设置的元素相同
  • 以字节为单位来初始化内存单元
  • 接收数据的最大值为ff,也就是一个字节的最大数据



使用方法

int main()
{
  int a[] = { 1,2,3,4,5 };
  memset(arr, 1, 20); 
    int sz = sizeof(arr)/sizeof(arr[0]);
    for(int i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
  return 0;
}



分析:


memset操作的是字节,将20个字节单位改成1,那么设置的元素就不是1,而是16进制位0x01010101的元素。

调试结果:

2fb29104e3bdca68fe64a59f5416201c.png

运行结果:

435036a6d89152f0404ce836a6da0494.png


模拟实现

void* my_memset(void* dest, int c, size_t count)
{
  assert(dest);
  void* ret = dest;
  while (count--)
  {
    *(char*)dest = c;
    dest = (char*)dest + 1;
  }
  return ret;
}
int main()
{
  int arr[] = { 1,2,3,4,5 };
  my_memset(arr, 1, 20);
  return 0;
}


运行结果:

19f1dbc7cdcca0b0a2250a8d7dd74260.png



4. 结语


到此本篇博客到此结束!本篇博客对常见的字符串和内存函数作出了一定归纳,相信通过这篇文章,大家也对于这些函数也有了一定的理解,当然可能也有一些不到位的地方,如有错误,还请指正!


相关文章
|
1月前
|
存储 C语言
`scanf`是C语言中用于按格式读取标准输入的函数
`scanf`是C语言中用于按格式读取标准输入的函数,通过格式字符串解析输入并存入指定变量。需注意输入格式严格匹配,并建议检查返回值以确保读取成功,提升程序健壮性。
728 0
|
3月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
146 26
|
3月前
|
安全 C语言
C语言中的字符、字符串及内存操作函数详细讲解
通过这些函数的正确使用,可以有效管理字符串和内存操作,它们是C语言编程中不可或缺的工具。
257 15
|
8月前
|
人工智能 Java 程序员
一文彻底搞清楚C语言的函数
本文介绍C语言函数:函数是程序模块化的工具,由函数头和函数体组成,涵盖定义、调用、参数传递及声明等内容。值传递确保实参不受影响,函数声明增强代码可读性。君志所向,一往无前!
229 1
一文彻底搞清楚C语言的函数
|
9月前
|
存储 编译器 C语言
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
220 3
|
9月前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
417 23
|
9月前
|
算法 C语言
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
本文档介绍了如何编写两个子函数,分别求任意两个整数的最大公约数和最小公倍数。内容涵盖循环控制与跳转语句的使用、最大公约数的求法(包括辗转相除法和更相减损术),以及基于最大公约数求最小公倍数的方法。通过示例代码和测试说明,帮助读者理解和实现相关算法。最终提供了完整的通关代码及测试结果,确保编程任务的成功完成。
326 15
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
|
9月前
|
C语言
【C语言程序设计——函数】亲密数判定(头歌实践教学平台习题)【合集】
本文介绍了通过编程实现打印3000以内的全部亲密数的任务。主要内容包括: 1. **任务描述**:实现函数打印3000以内的全部亲密数。 2. **相关知识**: - 循环控制和跳转语句(for、while循环,break、continue语句)的使用。 - 亲密数的概念及历史背景。 - 判断亲密数的方法:计算数A的因子和存于B,再计算B的因子和存于sum,最后比较sum与A是否相等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台对代码进行测试,预期输出如220和284是一组亲密数。 5. **通关代码**:提供了完整的C语言代码实现
170 24
|
9月前
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
398 16

热门文章

最新文章