[C/C++] memcpy 与memmove
之前曾经有过一次面试,被问到memcpy 与memmove的区别并且手撸一个memmove的实现,当时根本没用过memmove函数,就根据字面意思猜,回答说“memmove用于内存移动,在将源地址数据拷贝到目的地址后,源地址数据会被自动删除……”,后面一查尴尬的一批。。。。
Anyway,现在重新整理下,memcpy 与memmove区别在于数据拷贝时是否能够处理内存重叠的问题,也就是踩地址的问题。对于正常使用new或者malloc来申请新内存空间肯定不会返回一个与已有内存重叠的地址,除非系统内存池损坏了(这也是我最初感觉memmove没意义的原因)。它的应用场景主要有以下:
使用场景
- 数组元素或字符串中的部分移动(与move对应上了有没有~)
- 结构体数组中数据项的移动
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello, World!";
// 删除字符串中的第 3 个字符(从 'l' 开始)
memmove(str + 2, str + 3, strlen(str) - 2);
// 输出修改后的字符串,Helo, World!
printf("%s\n", str);
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 假设我们要将数组中从索引 3 开始的 4 个元素向前移动
memmove(arr, arr + 3, 4 * sizeof(int));
// 输出数组内容:4 5 6 7 5 6 7 8 9 10
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]);
}
return 0;
}
memcpy 与memmove内部实现
同样是简单原理上的实现,glibc源码中会增加有内存对齐处理、奇偶长度分别处理等优化方式
memcpy
如下所示,memcpy逐字节拷贝,如果是安全函数,则会在拷贝之前判断源地址长度是否大于目的地址长度,来保证目的地址内存不会溢出。
void* mymemcpy(void* dest, void* sorc, size_t len)
{
unsigned char* dest_c = (unsigned char*) dest;
unsigned char* sorc_c = (unsigned char*) sorc;
while (len > 0)
{
*dest_c = *sorc_c;
dest_c++;
sorc_c++;
len--;
}
return dest;
}
memmove
memmove会增加考虑源地址与目的地址是否重叠的情况,如果两者重叠,存在两种情况:
- 源地址在前,目标地址在后:那么如果按照memcpy那样从前向后逐字节拷贝,就会存在元数据被覆盖,所以需要修改为从后向前拷贝;
- 目标地址在前,源地址在后:按照memcpy那样从前向后逐字节拷贝没有问题。
那么该如何判断源地址与目标地址是否重叠呢?这前提要清楚源地址和目的地址都是一段连续的长度为len的内存空间(不关系目的地址空间是否足够),所以只要两块内存首地址只差大于len,那么就不存在重叠(不用挨个比较源和目的地址各个字节地址是否相同。。。),然后根据差值的正负关系判断源地址与目的地址哪个在前。
void* mymemmove(void* dest, void* sorc, size_t len)
{
int dis = (int*)dest - (int*)sorc;
unsigned char* dest_c = (unsigned char*)dest;
unsigned char* sorc_c = (unsigned char*)sorc;
if (dis > 0 && dis < len)//目的地址大于源地址,源地址在前,目的地址在后
{
for (size_t i = len-1; i >0 ; i--)
{
dest_c[i] = sorc_c[i];
}
dest_c[0] = sorc_c[0];
}
else //不重叠,或者目的地址在前,源地址在后
{
for (size_t i = 0; i < len; i++)
{
dest_c[i] = sorc_c[i];
}
}
return dest;
}
需要注意的是,在使用memove后可能导致源地址数据被错误修改,所以如果不是目的明确,不建议继续使用源地址指针。