这篇文章给大家介绍一些C语言4个常见的内存操作函数以及它们的模拟实现,一起来学习吧!!!
1.内存块拷贝函数——memcpy
我们一起来认识一下:
1.1 函数介绍
看到memcpy的参数,大家有没有感到似曾相识呢?
是不是跟strncpy的参数比较相似啊,我们来对比一下:
看它们的前两个参数及返回类型,唯一的区别就是一个是char* ,而一个是void*。
因为strcpy是char *,所以strcpy只能拷贝字符类型的数据。
而memcpy是void *,我们知道void *可以接收任何类型变量的地址,因此,对于memcpy,不管内存块种放的是什么类型的数据,使用memcpy都可以拷贝(将source指向空间的内容拷贝到destination指向的空间中去),参数size_t num 则用来指定想要拷贝的数据的字节个数。
我们看一下cplusplus对于memcpy的介绍:
接下来我们一起来练习一下它的使用。
我们先来尝试一下拷贝整形数据:
#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发生变化没:
当然拷贝任何类型的数据都是可以的,我们再来试一下浮点型:
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中
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)++;
因为这样的写法在某些编译器上可能通不过。
我们来看看效果:
没毛病。
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。
我们来看看能不能实现:
我们发现不行,没有达到我们想要的结果:
为什么呢?
因为memcpy在实现的时候是从前向后拷贝的,如果我们想把 arr中的1,2,3,4,5放到4,5,6,7,8的位置。
当我们把1,2,3拷贝到3,4,5的位置之后,我们再去拷贝后面4,5的时候,会发现4,5已经被覆盖成1,2了。
所以这样是不行的。
那有没有什么好的解决办法呢?当然有,我们接着往下看。
2.移动内存块(可拷贝重叠内存块)——memmove
不知道大家有没有注意到,其实在上面对memcopy的介绍中就提到如何解决重叠内存块的拷贝问题了。
2.1 函数介绍
那我们就来了解一下memmove:
我们可以发现memcopy跟memmove的参数及返回类型其实是一样的,只不过memmove的功能更强大,可以实现重叠内存块的拷贝,或者说,它可以对内存块进行移动。
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。
既然从前向后拷贝不行,那我们从后向前拷会不会就可以了呢?
试一下:
这样好像确实可以,那好了,那以后我们都从后向前拷贝就行了,是这样吗?
好像还是会出现问题:
我们来看这种情景,还是这个数组:
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
如果我们相把5,6,7,8,9放到2,3,4,5,6的位置,从后向前拷贝能实现吗?
我们来试试看:
这样的话从前往后又不行了,当我们拷贝5,6是发现5,6已经被8,9覆盖了。
这种情况下,我们又需要从前往后拷了。
那这样的话,有时候需要从前向后,有时候需要从后向前,怎么搞呢?
其实对比上面两次出现的情况,我们可以发现:
注:数组随着元素下标的递增地址是从小到大的。
当源空间的起始地址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; }
我们来测试一下,还是上面的那个例子:
这次就达到效果了。
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自身来测试一下,看能不能实现这个重叠内存块的拷贝:
可以啊,我们发现memcpy也可以实现对重叠内存块的拷贝啊。那是不是我们模拟实现的没有达标呢?
其实不是的。
C语言标准规定的就是:
对于memcpy,只要能实现对不重叠的内存块的拷贝就行了;
但是对于memmove来说,需要它能够实现对重叠的内存块的拷贝;
但是
我们也看到了,Visual Studio对于memcpy功能的实现是比较强大的,达到了和memmove一样的标准
但我们不能指望所有的编译器提供的memcpy都能够实现对重叠内存块的拷贝。
这一点给大家说一下。
4. 内存比较函数—— memcmp
4.1函数介绍
memcmp的参数,其实和strncmp是非常相似的:
它们的区别在于strncmp只能比较字符类型的数据(因为函数参数设计的是
char*
)而对于memcmp来说,它不管内存中放的是什么类型的数据,都可以进行比较,因为memcmp的参数设计的是
void*
。
除此之外,它们没什么不同,都是一个字节一个字节的进行比较,如果相同,继续比较下一个字节的内容,直至比较完num个字节的内容,返回值也一样:
我们练习一下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)上采用的是小端存储,即是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中
我们看看结果是不是-1呢?
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; }
测试一下效果:
没问题,和memcmp一样的结果。
5. 内存设置函数—— memset
5.1函数介绍
memset 是用来干嘛的呢?
它可以把指定内存块的前 num 个字节设置为指定的值。
解释一下:
参数ptr 用来接收我们想要修改的内存块的地址,value接收我们想要设置的值,num用于指定想要设置的字节数,函数最终返回指针ptr 。
我们来练习一下memset 的使用:
int main() { int arr1[] = { 1,2,3,4,5 }; memset(arr1, 0, 8); return 0; }
把数组arr1的前8个字节内容设置为0。
看看效果:
当然value的值我们传字符也是可以的,只不过是以整型的形式传递而已。
当然字符的话是以其对应的ASCII码值作为设置的值
我们来试一下:
int main() { int arr1[] = { 1,2,3,4,5 }; memset(arr1, 'a', 9); return 0; }
将arr1的前9个字节设置为字符’a’.
看看结果:
每一个字节都变成了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; }
看看效果:
可以达到与memset一样的效果。
好了,以上就是本篇文章的全部内容,欢迎大家指正!!!