memcpy、memmove、memcmp、memset函数的使用说明和模拟实现

在前面的文章中,我已经对字符串函数进行了详细的介绍和模拟实现,今天,我来讲解另一类函数----------内存函数。





memcpy函数

请添加图片描述
由msdn查询可以得知,memcpy函数的返回类型和参数类型分别是:void* memcpy(void* dest,const void* src,size_t num)

下面,我来介绍memcpy函数的返回值:

请添加图片描述
由msdn查询可以得知,memcpy函数返回参数dest指针。

在前面我就已经提到了这篇文章要介绍的是内存函数,顾名思义就是对内存进行一系列操作的函数,而内存往往会出现各种各样的类型,如int、char、float、double、long、long long等,所以memcpy的返回类型和参数类型都是void* 、const void* ,以应对不同的类型。

我来对函数的几个参数进行介绍吧。
第一个参数是dest,是memcpy函数要拷贝到的目的地
第二个参数是src,是memcpy准备拷贝的内容
第三个参数是num,单位是字节,是参考要从src中拷贝多少个字节的内容

接下来,我来举几个memcpy函数的使用例子。

memcpy函数作用于整形的类型:

#include<stdio.h>
#include<string.h>
int main()
{
	int arr1[] = {1,2,3,4,5,6,7,8,9,10};
	int arr2[20] = {0};
	memcpy(arr2,arr1,20);
	return 0;
}

调试观察arr2的内容。

拷贝前:
请添加图片描述

拷贝后:

请添加图片描述

memcpy函数作用于浮点型的类型

#include<stdio.h>
#include<string.h>
int main()
{
	float arr1[] = {1.0f,2.0f,3.0f,4.0f,5.0f};
	float arr2[20] = {0};
	memcpy(arr2,arr1,8);
	return 0;
}

调试观察arr2的内容。

拷贝前:
请添加图片描述

拷贝后:
请添加图片描述

接下来,我来模拟实现memcpy函数。

#include<stdio.h>
#include<assert.h>
void* my_memcpy(void* dest,const void* src,size_t num)
{
    void* ret = dest;
	while(num--)
	{
		*(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,20);
	return 0;
}

依然通过调试观察arr2的内容。

覆盖前
请添加图片描述

覆盖后:
请添加图片描述

由此可以得知,memcpy函数的模拟实现符合我们的要求。

在上面的模拟实现memcpy函数的代码中,会不会有人奇怪,为什么不把while语句改得更加简便呢?如下:

#include<stdio.h>
#include<assert.h>
void* my_memcpy(void* dest,const void* src,size_t num)
{
    void* ret = dest;
	while(num--)
	{
		*(char*)dest++ = *(char*)src++;//演示错误的代码
	}
	return dest;
}
int main()
{
	int arr1[] = {1,2,3,4,5,6,7,8,9,10};
	int arr2[20] = {0};
    my_memcpy(arr2,arr1,20);
	return 0;
}

请添加图片描述
在vs运行便知,此次改动导致了代码无法正常跑动。那么代码问题出现在哪里呢?其实,这正是强制转换为(char*)后,来到++自增的时候,强制转换已经失效了,而对一个(void*)进行自增,毫无疑问这是一个错误的行为。

那么我就要挑选一个合适的方法来进行上面的操作,并且要适合于所有的编辑器,所以,最后挑选了下面这种方法。

dest = (char*)dest + 1;
src = (char*)src + 1;

所以,小伙伴以后遇到这种问题,就可以采用这种写法。

memcpy函数的缺陷
请添加图片描述
由msdn观察memcpy函数的标注可以得知:memcpy函数将src的num个字节拷贝到dest字节中,如果源地址和目的地址重叠,则不保证复制重叠区域内源地址的原始字节后再被覆盖。使用memmove函数来处理重叠区域。

这句话是什么意思呢?别急,让我来慢慢分析,并且作为memmove函数的引入。

memcpy函数自己给自己追加

如下面的代码:

#include<stdio.h>
#include<string.h>
int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10};
	memcpy(arr+2,arr,20);
    return 0;
}

在上面的代码中,memcpy函数把一个整形数组的内容拷贝到自己本身,满足了msdn标注中的memcpy函数作用于重叠区域。

memcpy函数中是一个字节一个字节进行拷贝的,而为了容易分析,在后面的分析中,我采用一个整形一个整形进行拷贝

由上面的代码可以得知,memcpy函数准备拷贝20个字节的内容,所以,memcpy函数将拷贝5个整形。

我们来预测一波这段代码会出现什么样的结果吧。我使用两个指针,一个是dest,一个是src,与memcpy函数参数相对应,dest指向目的地,src指向源头。(假设重叠区域内源地址的原始字节先被复制后再被覆盖)

请添加图片描述
刚开始,dest指针和src指针的指向与上面的图片一样。

memcpy函数开始拷贝,首先,将src的整形内容拷贝拷贝dest的位置(这里以一个整形的视角来进行拷贝,而不是一个字节,方便分析)如下图:
请添加图片描述
接下来,dest指针和src指针开始向后走一步,如图:
请添加图片描述
接下来,继续把src一个整形的内容拷贝到dest的位置(这里以一个整形的视角来进行拷贝,而不是一个字节,方便分析)。
请添加图片描述
接下来,src指针和dest指针继续向后面走一步。
请添加图片描述
接下来,继续把src一个整形的内容拷贝到dest的位置(这里以一个整形的视角来进行拷贝,而不是一个字节,方便分析)。
请添加图片描述
接下里,dest指针和src指针继续向后面走一步。
请添加图片描述
接下来,继续把src一个整形的内容拷贝到dest的位置(这里以一个整形的视角来进行拷贝,而不是一个字节,方便分析)。

请添加图片描述
接下来,src指针和dest指针各往后面走一步。
请添加图片描述
接下来,继续把src一个整形的内容拷贝到dest的位置(这里以一个整形的视角来进行拷贝,而不是一个字节,方便分析)。
请添加图片描述
直到现在,memcpy函数拷贝5个整形(20个字节)的任务就完成了。综上分析,arr的内容应该为1 2 1 2 3 4 5 8 9 10。

但是遗憾的是。memcpy函数是不会让重叠区域内源地址的原始字节先被复制再被覆盖,所以在上面的代码中,运行结果中arr的内容会变为1 2 1 2 1 2 1 8 9 10。

而这些运行结果是怎么来的呢?别急,听我分析。(依然假设memcpy函数一次拷贝一个整形,方便分析

如下图,下划线表示这些元素将要被覆盖。
请添加图片描述
开始第一次覆盖,3将被替换成1,继续向下替换。
请添加图片描述
开始第二次覆盖,4将被替换成2,继续向下替换。
请添加图片描述
开始第三次覆盖,5将被替换成1,继续向下替换。
请添加图片描述
开始第四次覆盖,6将被替换成2,继续向下替换。
请添加图片描述
开始第五次覆盖,7将被替换成1,替换结束。
请添加图片描述
最后结果如下:
请添加图片描述
观察可以发现,结果的确是我们前面提到的 1 2 1 2 1 2 1 8 9 10。但是毫无疑问的是这种结果是错误的,因为原本的3,4,5,6,7的元素在没有被复制之前就已经被覆盖了,导致了结果都是1,2循环,这个就是memcpy函数的缺陷,不会让重叠区域内源地址的原始字节先被复制再被覆盖。

有的小伙伴就会反驳了,他把这些代码复制到有些编辑器上面,运行结果是1 2 1 2 3 4 5 8 9 10,然后就得出结论,memcpy函数可以让重叠区域内源地址的原始字节先被复制再被覆盖。

其实,这种说法是错误的。C语言规定,memcpy函数只需要实现不重叠内存的拷贝,而memmove函数需要实现内存不重叠和内存重叠的拷贝。但是有的编辑器已经将memcpy函数功能扩大到与memmove函数功能一样,导致了上面的结果,并不是所有编辑器都会出现这种结果。

接下来,我来讲讲memmove函数如何实现这种重叠内存的拷贝吧。

memmove函数

请添加图片描述在msdn查询memmove函数的返回类型、参数类型、返回值都与memcpy函数一样,我就不再过多的强调了。

我接着来讲memmove函数的功能实现吧,我依然选择上面提到的一串代码,我把它复制下来。

#include<stdio.h>
#include<string.h>
int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10};
	memmove(arr+2,arr,20);
    return 0;
}

前面我已经说过,memmove函数是可以实现内存重叠和内存不重叠的拷贝的,所以在这段代码运行后出现的结果将是1 2 1 2 3 4 5 8 9 10,那么这个结果是怎么出现的呢?这就要引入两个新的名词了,从后往前拷贝和从前往后拷贝。(从后向前拷贝是先拷贝src需要拷贝的最后一个元素,从前向后拷贝就是先拷贝src需要拷贝的第一个元素

上面这段代码之所以可以运行出正确的结果,正是使用了从后向前拷贝。

由代码可以得知,将要拷贝20个字节,也就是5个整形的数据(这里要记住src也就是源空间在dest也就是目的地的前面
请添加图片描述
既然要拷贝5个整形的内容,那么1,2,3,4,5这些元素都要被拷贝,那么这次我选择从后向前拷贝,也就是先拷贝5,再拷贝4,再拷贝3。

第一次拷贝,7将被替换成5,继续向前替换。
请添加图片描述
第二次拷贝,6将被替换成4,继续向前替换。请添加图片描述
第三次拷贝,5将被拷贝成3,继续向前拷贝。
请添加图片描述
第四次拷贝,4将被拷贝成2,继续向前拷贝。
请添加图片描述
第五次拷贝,3被替换成1,继续向前拷贝。
请添加图片描述
结果如下:
请添加图片描述
这个就是memmove函数运行后的结果,完全符合重叠区域内源地址的原始字节先被复制再被覆盖后的结果。

接下里,我来举一个只适合从前向后拷贝的例子。

#include<stdio.h>
#include<string.h>
int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10};
	memmove(arr,arr+3,20);
    return 0;
}

我先证明一下从后向前拷贝是不适合此次拷贝的。

先画出需要拷贝的内容和目的地。(这里需要注意src也就是要被拷贝的内容在dest也就是目的地的后面
请添加图片描述
接下来,开始从后向前拷贝,先找到需要拷贝内容的最后一个元素进行拷贝。
请添加图片描述
由黄色线可以发现,5将被8所替代,但是5本身也是需要被拷贝的内容呀,在还没有被复制的情况下,就被覆盖掉,显然,这个方法是不符合memmove函数的要求的。

那么,我来尝试一下从前向后拷贝。找到需要拷贝的内容的第一个元素进行拷贝。
请添加图片描述
如上图,1将被替换为4。
请添加图片描述
如上图,2将被替换为5。
请添加图片描述
如上图,3将被替换为6。
请添加图片描述
如上图,4将被替换为7。
请添加图片描述
如上图,5将被替换为8。
请添加图片描述
结果为:4 5 6 7 8 6 7 8 9 10。

从前向后替换,全程都没出现需要拷贝的内容在还没有被复制的情况下就被覆盖掉的情况。

综上,我们对memmove函数就有了更加深刻的理解,它主要靠的两种拷贝方法,一种是从前向后拷贝,一种是从后向前拷贝。并且还要判断什么时候用从前向前拷贝,什么时候用从后向前拷贝,如果我们把这一点弄清楚了,那么我们对于memmove函数的理解就更加深刻了,并且还可以模拟实现该函数。

规律:
1.当dest也就是目的地在src也就是源地址的左边时,采用从前向后拷贝,如第二个例子。
2.当dest也就是目的地在src也就是源地址的右边时,采用采用从后向前拷贝,如第一个例子。

接下来,我来模拟实现memmove函数。

#include<stdio.h>
#include<assert.h>
void* my_memmove(void* dest,const void* src,size_t num)
{
    void* ret = dest;
	assert(dest != NULL);
	assert(src != NULL);
	if(dest < src)      //从前往后拷贝
	{
	    while(num--)
		{
		    *(char*)dest = *(char*)src;
			dest = (char*)dest + 1;
			src = (char*)src + 1;
		}
		return ret;
	}
	else                //从后向前拷贝
	{
	    while(num--)    //假如20个字节进去,后置减减就变成了19
		{
		    *((char*)dest+num) = *((char*)src+num);      //dest和src各自加19个字节找到需要被覆盖或者需要被拷贝的最后一个字节
		}
		return ret;
	}
}
int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10};
	my_memmove(arr,arr+3,20);
	return 0;
}

运行结果如下:

请添加图片描述由运行结果可以得知,memmove函数模拟满足我们的要求。



memcmp函数

请添加图片描述
由msdn查询可以得知,memcmp函数的返回类型和参数类型是:int memcmp(const void* ptr1,const void* ptr2,size_t num)

因为是内存函数,所以返回类型和参数类型设置成void* 或者是const void*,可以作用于不同类型的数据。

接下来,继续查询memcmp函数的返回值
请添加图片描述
观察可以发现,memcmp函数返回的是ptr1与ptr2的关系

ptr1 > ptr2,返回大于0的数据
ptr1 = ptr2,返回等于0的数据
ptr1 < ptr2,返回小于0的数据

接下来,我来举一个memcmp函数的使用例子。

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

运行结果如下:
请添加图片描述

接下来,我来模拟实现该函数。

#include<stdio.h>
#include<assert.h>
int my_memcmp(const void* ptr1,const void* ptr2,size_t num)
{
    assert(ptr1 != NULL);
	assert(ptr2 != NULL);
	while(num--)
	{
	    if(*(char*)ptr1 != *(char*)ptr2)
		{
		    return *(char*)ptr1 - *(char*)ptr2;
		}
		ptr1 = (char*)ptr1 + 1;
		ptr2 = (char*)ptr2 + 1;
	}
	return 0;
}
int main()
{
	int arr1[] = {1,2,3,4,5,6,7,8,9,10};
	int arr2[] = {1,2,3,4,5,6,7,8,0,0};
	printf("%d\n",my_memcmp(arr1,arr2,40));
    return 0;
}

运行结果如下:
请添加图片描述
由运行结果可以得知,该函数满足我们的要求。(注意这里的返回值不一定要1,或者0,或者-1,只要满足返回值要求就可以。



memset函数

请添加图片描述
在msdn查询可以得知,memset函数的返回类型和参数类型是:void* memset (void* ptr,int value,size_t num)

ptr参数是要修改的内存的最前面的地址
value参数是要初始化的结果
num参数是要修改的字节数

接下来,我来举一个memset函数的使用例子。

#include<stdio.h>
#include<string.h>
int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10};
	memset(arr,0,20);
    return 0;
}

调试观察arr数组的内容:

memset函数设置前:
请添加图片描述

memset函数设置后:
请添加图片描述

对于memset函数,我还要提出几个使用上面的注意事项。

1.memset函数赋值非char类型的数据
对于赋值非char类型数据时,如int,我们应当赋值-1和0这两种数据,或者其他高字节和低字节相同的数据。(-1在内存中存储是补码,全1,所以高字节和低字节是相同的;0在内存中的存储是全0,所以高字节和低字节也是相同的)

memset函数赋值char类型的数据
因为char是一个字节,而memset函数在赋值时也是一个字节进行赋值,所以什么数据都可以。

例子:

#include<stdio.h>
#include<string.h>
int main()
{
	char arr[] = "abcdef";
	memset(arr,'6',6);
	return 0;
}

覆盖前:
请添加图片描述

覆盖后:
请添加图片描述

接下来,我来模拟实现memset函数。

#include<stdio.h>
#include<assert.h>
void* my_memset(void* e1,int val,size_t num)
{
    void* ret = e1;
	assert(e1 != NULL);
	while(num--)
	{
	    *(char*)e1 = (char)val;
		e1 = (char*)e1 + 1;
	}
	return ret;
}
int main()
{
	char arr[] = "abcdef";
	my_memset(arr,'6',6);
	return 0;
}

覆盖前:
请添加图片描述

覆盖后:
请添加图片描述

由运行结果可以得知,该模拟实现memset函数满足了我们的要求。

今天的内存函数就讲到这里。关注点一点,后期更精彩。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值