【C进阶】——内存操作函数memcpy、memmove、memcmp、memset详解及其模拟实现

简介: 【C进阶】——内存操作函数memcpy、memmove、memcmp、memset详解及其模拟实现

这篇文章给大家介绍一些C语言4个常见的内存操作函数以及它们的模拟实现,一起来学习吧!!!

1.内存块拷贝函数——memcpy

我们一起来认识一下:

222dbc766db44e03bcc08a293c72d096.png

1.1 函数介绍

看到memcpy的参数,大家有没有感到似曾相识呢?

是不是跟strncpy的参数比较相似啊,我们来对比一下:

0b4a910fb6124582a9ca896a9d2ecb63.png

看它们的前两个参数及返回类型,唯一的区别就是一个是char* ,而一个是void*。

因为strcpy是char *,所以strcpy只能拷贝字符类型的数据。

而memcpy是void *,我们知道void *可以接收任何类型变量的地址,因此,对于memcpy,不管内存块种放的是什么类型的数据,使用memcpy都可以拷贝(将source指向空间的内容拷贝到destination指向的空间中去),参数size_t num 则用来指定想要拷贝的数据的字节个数。


我们看一下cplusplus对于memcpy的介绍:

323fcfc5257f47679d5249d70e29d0a6.png

接下来我们一起来练习一下它的使用。

我们先来尝试一下拷贝整形数据:

#include <stdio.h>
#include <string.h>
int main()
{
  int arr1[] = { 1,1,1,1,1,1,1,1 };
  int arr2[] = { 5,5,5,5,5 };
  memcpy(arr1, arr2, 20);
  return 0;
}

我们把arr2中的前20个字节的内容拷贝到arr1中。

看看arr1发生变化没:

a4cda15a9594466cbe3954c5d0049ed3.png

当然拷贝任何类型的数据都是可以的,我们再来试一下浮点型:

int main()
{
  double arr1[] = { 1.0,1.2,1.3,1.4,1.5,1.6,1.8,1.9 };
  double arr2[] = { 8.8,7.5,6.3,9.9 };
  memcpy(arr1, arr2, 24);
  return 0;
}

把arr2中的前20个字节(3个double变量的大小)的内容拷贝到arr1中

f189799425b249ef991d2f0c000f60af.png

1.2 memcpy的模拟实现

我们已经明白这个函数是怎么工作了,那现在我们就来模拟实现一下memcpy。

那我们应该怎么去实现呢?


其实思路很简单,我们的目的是把源空间的num个字节的内容拷贝到目标空间里,那我们就可以这样做:

使用一个while循环,让它循环num次,每次我们拷贝一个字节的内容。

那现在又有一个问题,因为memcpy可以拷贝任何类型的数据,所以它的参数是void *,但是我们知道void *的指针是不能直接解引用的,那我们怎么做才能让它一次访问一个空间呢?

当然是有办法的,我们可以把void *强制类型转换为char *的指针,而char *的指针每次解引用恰好能访问一个字节的内容。

好,梳理好思路,我们就来实现一下:

#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;
    dest = (char*)dest+1;
    src = (char*)src + 1;
  }
  return ret;
}

注意:dest = (char*)dest+1; src = (char*)src + 1;

不要写成这样:((char*)dest)++;((char*)src)++;

因为这样的写法在某些编译器上可能通不过。

我们来看看效果:

cf00b00eb2824206b9041d53e25c0ae6.png

没毛病。


1.3 思考

相信大家已经对memcpy了解的差不多了,那我们接下来再来思考一个问题:


我们刚才演示的是把一个数组的一些数据拷贝到另一个数组里面去了。是在两块不同的内存块进行操作的。

那我们能不能在同一个数组中,把前面的数据拷贝到后面的空间中呢?(也就是说,源空间和目标空间是有重叠的)

这样可以吗?我们可以来试一下:

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

像这样,把arr中的1,2,3,4,5放到4,5,6,7,8的位置。变成1,2,3,1,2,3,4,5,9,10。

我们来看看能不能实现:

912db3e853504742b97e337917542cea.png

我们发现不行,没有达到我们想要的结果:


为什么呢?

因为memcpy在实现的时候是从前向后拷贝的,如果我们想把 arr中的1,2,3,4,5放到4,5,6,7,8的位置。

当我们把1,2,3拷贝到3,4,5的位置之后,我们再去拷贝后面4,5的时候,会发现4,5已经被覆盖成1,2了。

3974623490f944b4b05f96c064c507fc.png所以这样是不行的。

那有没有什么好的解决办法呢?当然有,我们接着往下看。

2.移动内存块(可拷贝重叠内存块)——memmove

不知道大家有没有注意到,其实在上面对memcopy的介绍中就提到如何解决重叠内存块的拷贝问题了。

61d441f38ce44e7eb31b4876c6396713.png

2.1 函数介绍

那我们就来了解一下memmove:

736889fa11fb425386b4a02cdfb78b03.png

我们可以发现memcopy跟memmove的参数及返回类型其实是一样的,只不过memmove的功能更强大,可以实现重叠内存块的拷贝,或者说,它可以对内存块进行移动。

6204ecfcb37a498e8ce240a5a37440e4.png

2.2 memmove的模拟实现

我们来继续讨论上面的那个问题:

在同一个数组中,把前面的数据拷贝(移动)到后面的空间中,或者把后面的数据拷贝(移动)到前面(即源空间和目标空间是有重叠的)。

还来看上面的例子:

int arr[] = { 1,2,3,4,5,6,7,8,9,10 };

把arr中的1,2,3,4,5放到4,5,6,7,8的位置。变成1,2,3,1,2,3,4,5,9,10。

我们已经测试过了,用我们模拟实现的my_memcpy是不行的,因为在从前向后拷贝的时候后覆盖掉4,5。

e6f9147b09744504948ce7cb544e37ec.png

既然从前向后拷贝不行,那我们从后向前拷会不会就可以了呢?

试一下:

1a2157c63a69409faefd05d5b8b394b4.png

这样好像确实可以,那好了,那以后我们都从后向前拷贝就行了,是这样吗?

好像还是会出现问题

我们来看这种情景,还是这个数组:

int arr[] = { 1,2,3,4,5,6,7,8,9,10 };

如果我们相把5,6,7,8,9放到2,3,4,5,6的位置,从后向前拷贝能实现吗?

我们来试试看:

27d4904eeec747e696e29fb67046757b.png

这样的话从前往后又不行了,当我们拷贝5,6是发现5,6已经被8,9覆盖了。

这种情况下,我们又需要从前往后拷了。

4aa50a60ca12474d8313660ad3a561c3.png

那这样的话,有时候需要从前向后,有时候需要从后向前,怎么搞呢?


其实对比上面两次出现的情况,我们可以发现:

注:数组随着元素下标的递增地址是从小到大的。

当源空间的起始地址dest小于目标空间的起始地址src时,我们需要从前向后拷贝。

当源空间的起始地址dest大于目标空间的起始地址src时,我们需要从后向前拷贝。


那我们就可以模拟实现memmove了。


在函数内部,我们只需判断一下,dest和src的大小就行了,然后决定从前向后还是从后向前拷贝。

void* my_memmove(void* dest, const void* src, size_t num)
{
  assert(dest && src);
  void* ret = dest;
  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;
}

我们来测试一下,还是上面的那个例子:

86314666e43c463b88136f5672d02be8.png

这次就达到效果了。

3. Visual Studio 对memcpy的实现

大家有没有注意到:


刚才在 1.3 测试把arr中的1,2,3,4,5放到4,5,6,7,8的位置。变成1,2,3,1,2,3,4,5,9,10。

我们用的是自己模拟实现的memcpy—— my_memcpy来测试的,当然它没有实现这种重叠内存块的拷贝。


那我们现在尝试用库函数memcpy自身来测试一下,看能不能实现这个重叠内存块的拷贝:

4a4a669fa6294234879af811648b9de9.png

可以啊,我们发现memcpy也可以实现对重叠内存块的拷贝啊。那是不是我们模拟实现的没有达标呢?

其实不是的。


C语言标准规定的就是:


对于memcpy,只要能实现对不重叠的内存块的拷贝就行了;

但是对于memmove来说,需要它能够实现对重叠的内存块的拷贝;


但是


我们也看到了,Visual Studio对于memcpy功能的实现是比较强大的,达到了和memmove一样的标准

但我们不能指望所有的编译器提供的memcpy都能够实现对重叠内存块的拷贝。

这一点给大家说一下。

4. 内存比较函数—— memcmp

2540395649cc48a3b155a3eb4342a75b.png

4.1函数介绍

memcmp的参数,其实和strncmp是非常相似的:

d2bbdaf9d3b442ebb56d29c282a9a9f9.png

它们的区别在于strncmp只能比较字符类型的数据(因为函数参数设计的是char*

而对于memcmp来说,它不管内存中放的是什么类型的数据,都可以进行比较,因为memcmp的参数设计的是void*

除此之外,它们没什么不同,都是一个字节一个字节的进行比较,如果相同,继续比较下一个字节的内容,直至比较完num个字节的内容,返回值也一样:

dbc809aa9ed3489aa92a950e36f166e1.png

我们练习一下memcmp的使用:

#include <stdio.h>
#include <string.h>
int main()
{
  int arr1[] = { 1,2,3,4,5 };
  int arr2[] = { 1,2,3,7,9 };
  int ret = memcmp(arr1, arr2, 13);
  printf("%d", ret);
  return 0;
}

思考一下结果是什么?

比较arr1和arr2的前13个字节的内容:

我们可以画一个图,分析一下arr1和arr2所占空间中放的内容

首先我用的编译器(vs2022)上采用的是小端存储,即是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中

306c9a6e640e4c7d9ab5cd1b0366a007.png

我们看看结果是不是-1呢?

a584f4ab55a94600b79346cd752fdfab.png

4.2 memcmp的模拟实现

memcmp的实现思路其实也很简单,我们就一个字节一个字节的比较,如果相等就继续比较下一个字节,不相等就返回对于的值,如果比完了num个字节都相等,就返回0。

当然这里还是需要把void *强制类型转换为char *的指针,使得每次解引用恰好能访问一个字节的内容。


看代码:

int my_memcmp(const void* s1, const void* s2, size_t num)
{
  assert(s1 && s2);
  while (num--)
  {
    if (*(char*)s1 > *(char*)s2)
      return 1;
    else if(*(char*)s1 < *(char*)s2)
      return -1;
    else
      {
        s1 = (char*)s1 + 1;
        s2 = (char*)s2 + 1;
      }
  }
  return 0;
}

测试一下效果:

45bef5477f304ecca3dc5d2e41d19a16.png

没问题,和memcmp一样的结果。

5. 内存设置函数—— memset

5.1函数介绍

memset 是用来干嘛的呢?

它可以把指定内存块的前 num 个字节设置为指定的值。

4e2f9f5f9d984b76b6f9a9fd136599d9.png

b0a8684e421e46c58cd9acc4e8748b58.png

解释一下:

参数ptr 用来接收我们想要修改的内存块的地址,value接收我们想要设置的值,num用于指定想要设置的字节数,函数最终返回指针ptr 。

我们来练习一下memset 的使用:

int main()
{
  int arr1[] = { 1,2,3,4,5 };
  memset(arr1, 0, 8);
  return 0;
}

把数组arr1的前8个字节内容设置为0。

看看效果:

9cba6d8705c94afc9764bafb996f7e48.png

当然value的值我们传字符也是可以的,只不过是以整型的形式传递而已。

当然字符的话是以其对应的ASCII码值作为设置的值

d3559c9557274a139e6104df57e09e99.png

我们来试一下:

int main()
{
  int arr1[] = { 1,2,3,4,5 };
  memset(arr1, 'a', 9);
  return 0;
}

将arr1的前9个字节设置为字符’a’.

看看结果:

16202b6f1c0e4392b5afa5c67a85b745.png

每一个字节都变成了61,因为编译器给我们展示出来的是16进制,而字符’a’的ASCII码值为97,转换为16进制就是61,结果没问题。


5.2 memset的模拟实现

思路很简单,可以用一个while循环,循环num次,每次设置一个字节,直至把num个字节的内容设置成参数value的值,返回指向内存块的指针ptr。


上代码:

void* my_memset(void* ptr, int value, size_t num)
{
  assert(ptr);
  void* ret = ptr;
  while (num--)
  {
    *(char*)ptr = value;
    ptr = (char*)ptr + 1;
  }
  return ret;
}

看看效果:

0ba87244920d48f7822ed536eb898c7c.png

可以达到与memset一样的效果。

好了,以上就是本篇文章的全部内容,欢迎大家指正!!!

213b3c93b18a4d6ab6f62452f96bf9ee.png


目录
相关文章
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
35 3
|
1月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
28天前
|
存储 C语言
【c语言】字符串函数和内存函数
本文介绍了C语言中常用的字符串函数和内存函数,包括`strlen`、`strcpy`、`strcat`、`strcmp`、`strstr`、`strncpy`、`strncat`、`strncmp`、`strtok`、`memcpy`、`memmove`和`memset`等函数的使用方法及模拟实现。文章详细讲解了每个函数的功能、参数、返回值,并提供了具体的代码示例,帮助读者更好地理解和掌握这些函数的应用。
23 0
|
4月前
|
存储 分布式计算 Hadoop
HadoopCPU、内存、存储限制
【7月更文挑战第13天】
291 14
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
381 0
|
27天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
57 1
|
1月前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
1月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
1月前
|
存储 编译器
数据在内存中的存储
数据在内存中的存储
42 4
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
55 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配