【C语言】- memcpy函数和memmove函数的使用及模拟

今天我来向大家介绍两种重要的内存操作函数memcpy函数和memmove函数。

简单观察一下这两个函数你会发现,它们起始的三个字母都是mem.这里的mem实际上是memory的缩写,memory这个词最常见的意思是记忆,而在计算机中它被翻译为内存。所以如果以后大家遇到以这个三个字母mem为开头的函数,那基本上都是和内存相关的。

闲话不多数,下面直接来介绍这两种函数。

memcpy

函数定义:

在缓冲区之间复制字符。

函数声明:

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

这里的source是被复制源空间地址,destination是复制的目的空间地址,num是从source位置开始向后复制多少个字节。

函数使用:

  • 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
  • 函数的调用要引头文件<string.h>

接下来我们通过几段代码来看看memcpy函数究竟是如何使用的。

代码1:

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

int main()
{
	int arr1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	int arr2[20] = {0};

	memcpy(arr2, arr1, 16);

	return 0;
}

这里我创建了两个整型数组arr1和arr2,通过观察memcpy函数的调用可以看出,源内存起始地址为arr1的起始地址,目的内存起始地址是arr2的起始地址,复制的字节数是16.

也就是说我们要从arr1的首地址开始复制16个字节的内容到arr2中去。

这里大家应该知道,一个整型元素占4个字节,所以16个字节的内容就是4个整型元素,那么我们所要的结果就应该是把arr1起始位置往后的4个整型元素1、2、3、4复制到arr2起始位置开始的16个字节空间中。

接下来我们就通过调试来观察arr2内存的变换,看看结果是不是和memcpy函数功能一样。

memcpy函数调用前
在这里插入图片描述
可以看到arr2数组的前四个位置的内容还是0.

memcpy函数调用后:
在这里插入图片描述
可以看到,memcpy函数调用后成功的把arr1数组的前四个元素复制到arr2数组中去了。

接下来我们再来看一组代码。
代码2:

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

int main()
{
	int arr1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	int arr2[20] = {0};

	memcpy(arr2, arr1+2, 16);

	return 0;
}

这次大家注意一下,我在调用memcpy函数时,源地址是arr1+2.我们知道arr是一个整型指针,加2向后跳2个整型元素到数组arr1第三个整型元素的地方。然后从此位置开始向后复制16个字节的内容,也就是将4个整型元素3、4、5、6拷贝到arr2起始地址开始向后16个字节。

memcpy函数调用后:
在这里插入图片描述
可以看到这次成功的将arr1里的3、4、5、6复制到arr2中去了。当然复制的方式还有许多,这里我就不一 一列举了,如果大家感兴趣可以自己慢慢调试。我想通过这两段代码大家一定会对memcpy函数的使用有一定的了解。

函数模拟:

学会memcpy函数的使用之后,接下来我们就来试着来模拟实现一下memcpy函数。

memcpy函数的模拟还是比较简单的,首先要得到三个参数,参数的类型也是需要我们注意的。

看看上面函数的声明部分,目的地址和源地址的参数都是用void*来接收的,void*的详细作用我曾在上篇文章《【C语言】- 回调函数之qsort函数使用及冒泡法模拟》中介绍过,这里就不再详细介绍了。

根据C语言库里所给的memcpy函数功能,是可以实现任意类型数据的拷贝。所以函数需要来接收任意类型的指针,于是就用void*指针来接收。

当然,也需要拿到第三个参数也就是要复制的字节个数,这里一定要注意复制的单位是字节。

接下来就是一个字节一个字节的拷贝了,总共拷贝所需拷贝的字节数即可。

具体的算法思想就是这样,接下来我们通过实现代码,再来具体分析一下:

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

void* my_memcpy(void*dest, const void*src, size_t count)
{
	void*ret = dest;
	assert(dest);
	assert(src);

	while (count--)
	{
		//拷贝一个字节
		*(char*)dest = *(char*)src;
		dest = (char*)dest+1;
		src = (char*)src+1;
	}
	return ret;
}

int main()
{
	int arr1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	int arr2[20] = {0};
  
	my_memcpy(arr2, arr1+2, 16);

	return 0;
}

分析:首先得到三个参数,上面已经分析过了。

接下来创建一个变量存储目的地址的起始地址,原因是在下面数据拷贝的时候,目的地址的起始位置会发生变化。而memcpy复制完成之后我们还要来看目的空间的变化情况,所以这个函数会返回目的空间的起始地址。因此为了方便起见,我们在一开始将目的地址的起始位置保存下来。

接下断言我们所收到的指针不是一个空指针,这也是指针传参的常规操作了。

然后就是拷贝部分了,拷贝的次数不难分析就是我们所接收到的第三个参数字节数,这里采用(count- -)的形式来控制复制的次数。

拷贝的时候我们要注意,我们用void*接收的两个地址是不能被解引用的,所以需要将它强制类型转换成其他类型才能进行解引用操作。由于我们是一个字节一个字节的拷贝,所以可以将它强制类型转换成字符型指针,然后解引用进行赋值操作。

每一次赋值结束后要将两个地址向后跳一个字节,需要注意的是这里的跳1可不能写成dest++,因为dest是void*类型的,不能进行加减操作。那么有人说是不是可以写成这种(char*)dest++,这样也是不可以的,因为操作符后置++的优先级高于强制类型转换操作符,所以表达式执行的时候是先给dest自加1,再强制类型转换,dest此时又是void*类型不能加减运算,所以就会出错。因此,这里在指针向后跳的时候可以采用代码中所写的方式,或者写成这种形式++(char*)dest,让变量dest先和强制类型转换操作符结合转换成char*类型,再自加1也是可以的。

以上就是模拟实现memcpy函数的全部思想。

还有一个情况我需要提及一下,不知道大家有没有见到过strcpy这个函数。这个函数是进行字符串拷贝的函数,当拷贝遇到’\0’是会结束。我们需要知道的是,memcpy函数遇到’\0’是不会停止的,希望大家不要将这两个函数混淆了。

学会memcpy函数的使用和模拟之后,我想请大家思考一下下面这个问题:

如下图:
在这里插入图片描述
我说我们可不可以在一个数组arr中,用memcpy函数把前四个元素的内容复制到第三个元素开始往后的四个元素内容处。

行不行来分析一下,前面我们对memcpy函数是如何实现的已经十分清楚了。复制的时候首先把第一个元素的内容1,放到第三个元素位置处。然后把第2个元素的内容2,放到第4个元素位置处.接下来要把第3个元素的内容3,放到第5个元素位置处.但是这个时候问题来了,第一步我们将1放到第三个元素位置处之后,第三个位置原本的内容3已经不见了,所以无法再往后复制了。很明显这种情况memcpy函数是无法解决的。

所以得出结论:如果source和destination有任何的重叠,复制的结果都是未定义的。

当然,既然memcpy函数无法解决有混叠位置的处理,那么C语言中有没有一个函数可以处理这种目标位置和源位置重叠的问题呢?当然是有的,下面我们将要介绍的memmove函数就可以很好的解决这个问题。

memmove

定义:

将一个缓冲区移到另一个缓冲区。

函数声明:

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

函数使用:

  • 和memcpy的差别就是memmove函数处理的原内存块和目标内存块是可以重叠的。

  • 如果源空间和目标空间出现重叠,就得使用memmove函数处理。

  • 这个函数的调用要引头文件<string.h>.

接下来我们还是通过代码来看看memmove函数是如何使用的。

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

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

	memmove(arr+2, arr, 16);

	return 0;
}

简单分析可知,这里创建了一个数组arr,memmove函数的三个参数中,目的空间的起始地址是arr+2,源空间的起始地址是arr,正如我在上图所提到的情况。将数组前四个元素的内容复制到3456元素所在的内容去。

我们还是通过调试来观察。

调用memmove函数前:
在这里插入图片描述
我们要将绿色圈中的内容拷贝到紫色圈中去,此时还未发生变化。

调用memmove函数后:
在这里插入图片描述
可以看到成功将前四个元素复制到了第三个位置开始处。

相信通过这段代码大家一定可以很好的学会如何调用memmove函数。

函数模拟:

学会memmove函数的使用之后,接下来我们就再来看看如何来模拟实现这个函数。

这个函数要模拟实现起来就比上面的memcpy函数要困难一些了。

不知道有没有人想到这一点,刚才你在用memcpy函数实现混叠拷贝的时候是从前往后拷贝的,因此这个第三个位置和第四个位置的内容就会发生变化。但如果你能从后往前拷贝,不就不会出现这种情况了吗?如下图:
在这里插入图片描述

仔细一想也是哎,我先把4和3拷贝过去,然后轮到1和2的时候,1和2的内容并未发生变化,显然这种逻辑是可行的。

但是大家有没有想过,假如此时又要把从第三个位置开始的四个元素拷贝到前四个元素,如果你还是从后往前拷贝,是不是拷贝到3和4的时候此处的内容已经发生变化了。

相信大家已经想到了,这里一定要进行分类讨论,接下来我们就来讨论一下。

首先我们来讨论什么时候是从后往前拷贝的,是不是只要目标空间的起始地址大于源空间的起始地址,我们就要从后往前拷贝。(当然这里具体指的是有混叠部分的两块空间,试想如果没有混叠部分,那么我们想怎样拷贝就怎样拷贝都行)

然后如果目标空间的起始地址小于源空间的起始地址,我们就要从前往后拷贝。

知道了这一点,也就了解到了memmove函数实现的主要算法。

接下来我们就来看看memmove函数具体是如何使用代码来实现的,再加以分析.

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

void* my_memmove(void* dest, const void* src, size_t count)
{
	assert(dest);
	assert(src);

	void* ret = dest;
	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);
		}
	}
	return ret;
}

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

	my_memmove(arr+2, arr, 16);

	return 0;
}

分析:参数部分和前面的memcpy函数是一样的,然后函数前面的断言和存放目的空间起始地址也是一样的,这里就不再多提了。

然后看当目标地址小于起始地址时,要从前往后拷贝。从前往后的拷贝方式很容易理解,和前面memcpy函数的拷贝方法是一样的。

这里可能有人会有疑问,你在比较目的地址和源地址的时候不要强制类型转换的吗?不是说void*的指针不能解引用和加减操作吗?注意,void*指针虽然不能解引用和加减操作,但是是可以比较大小的。

接下来再看,当目标地址大于源地址时,我们要从后往前拷贝。从后往前拷贝实现起来其实很简单,但也比较巧妙,所以这里一定要好好的理解。

拷贝的次数依旧是count次,所以使用count- -来控制次数即可。然后接下来我们要拿到所操作空间的最后一个字节的地址,实际上给起始地址加上一个count就可以拿到了。而且地址的跳变也不用写了,因为在while循环的判断框中每次都会给count的值减1,对应的所拷贝的地址也会减1.所以只需要一条语句就可以实现从后往前的拷贝了。

以上就是memmove函数模拟实现的全部思想。

最后再多说一句,在一些编译器中memcpy函数是可以实现memmove函数功能的,但实际这只是编译器对memcpy函数的功能进行了扩展,而在C语言的标准规定里,memcpy函数所实现的功能就是我在前面介绍的。

最后希望这篇文章可以帮助到大家。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值